nuts / commit
gems updated
.gitignore | 1 + src/config.ts | 2 +- src/renderer.ts | 205 ++++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 163 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index f06235c..3d79ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +kenney_impact-sounds diff --git a/src/config.ts b/src/config.ts index 4647804..a31061b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,5 +28,5 @@ export const CONFIG = { orientationMaxTilt: 80, // degrees before clamping // Debug - logToVite: true, // POST logs to Vite dev server (/__log) — useful for phone debugging + logToVite: false, // POST logs to Vite dev server (/__log) — useful for phone debugging } as const diff --git a/src/renderer.ts b/src/renderer.ts index 100ae70..cb1c34f 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -30,55 +30,165 @@ function roundToNearest4(n: number): number { } // ── Gem palette ──────────────────────────────────────────────────────────── +type GemPts = Array<[number, number]> + interface GemDef { - sides: number - rotation: number hi: string; mid: string; shadow: string; rim: string facetHi: string; facetLo: string + outline: (cx: number, cy: number, r: number) => GemPts + /** Custom facet drawing; omit to use default alternating triangles from centre. */ + drawFacets?: (ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, pts: GemPts, g: GemDef) => void +} + +function polygonOutline(sides: number, rot: number): (cx: number, cy: number, r: number) => GemPts { + return (cx, cy, r) => { + const pts: GemPts = [] + for (let i = 0; i < sides; i++) { + const a = rot + (i / sides) * Math.PI * 2 + pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]) + } + return pts + } } const GEM_TYPES: GemDef[] = [ - // 0 — Diamond: icy white-blue, 4-sided ◇ + // 0 — Diamond: icy white-blue + // Crown = trapezoid (narrow table at top, wide girdle below) + // Pavilion = equilateral triangle hanging from girdle + // Girdle at cy − 0.3r → triangle height = 1.5r·(√3/2) = 1.3r → tip at cy + r ✓ { - sides: 4, rotation: Math.PI / 4, hi: '#F4FBFF', mid: '#B8D8F0', shadow: '#5880A8', rim: '#C8E4F4', - facetHi: 'rgba(255,255,255,0.55)', facetLo: 'rgba(70,120,170,0.35)' + facetHi: 'rgba(255,255,255,0.55)', facetLo: 'rgba(70,120,170,0.35)', + outline: (cx, cy, r) => [ + [cx - r * 0.30, cy - r], // TL (table left) + [cx + r * 0.30, cy - r], // TR (table right) + [cx + r * 0.75, cy - r * 0.3], // GR (girdle right) + [cx, cy + r], // Tip (culet) + [cx - r * 0.75, cy - r * 0.3], // GL (girdle left) + ], + drawFacets: (ctx, cx, cy, r, pts, g) => { + const [tl, tr, gr, tip, gl] = pts + const mid: GemPts[0] = [cx, cy - r * 0.3] // girdle midpoint + const fill = (a: GemPts[0], b: GemPts[0], c: GemPts[0], style: string) => { + ctx.beginPath() + ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.lineTo(c[0], c[1]) + ctx.closePath(); ctx.fillStyle = style; ctx.fill() + } + // Crown facets + fill(tl, tr, mid, g.facetHi) + fill(tl, gl, mid, g.facetLo) + fill(tr, gr, mid, g.facetLo) + // Pavilion facets + fill(gl, mid, tip, g.facetHi) + fill(gr, mid, tip, g.facetLo) + // Thin girdle line + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 0.8 + ctx.beginPath(); ctx.moveTo(gl[0], gl[1]); ctx.lineTo(gr[0], gr[1]); ctx.stroke() + }, }, - // 1 — Ruby: deep red, hexagon + + // 1 — Ruby: deep red, hexagon outside + hexagon inside, edge-to-edge spokes { - sides: 6, rotation: 0, hi: '#FF7090', mid: '#C01030', shadow: '#580010', rim: '#FF2040', - facetHi: 'rgba(255,130,150,0.55)', facetLo: 'rgba(55,0,10,0.45)' + facetHi: 'rgba(255,130,150,0.55)', facetLo: 'rgba(55,0,10,0.45)', + outline: polygonOutline(6, 0), + drawFacets: (ctx, cx, cy, r, pts, g) => { + const inner: GemPts = pts.map(([x, y]) => [cx + (x - cx) * 0.50, cy + (y - cy) * 0.50]) + // Six trapezoid bands between outer and inner hex + for (let i = 0; i < 6; i++) { + const [ox1, oy1] = pts[i], [ox2, oy2] = pts[(i + 1) % 6] + const [ix1, iy1] = inner[i], [ix2, iy2] = inner[(i + 1) % 6] + ctx.beginPath() + ctx.moveTo(ox1, oy1); ctx.lineTo(ox2, oy2); ctx.lineTo(ix2, iy2); ctx.lineTo(ix1, iy1) + ctx.closePath() + ctx.fillStyle = i % 2 === 0 ? g.facetHi : g.facetLo; ctx.fill() + } + // Inner hexagon rim + tracePoly(ctx, inner); ctx.strokeStyle = g.rim; ctx.lineWidth = 1.0; ctx.stroke() + // Spokes: outer vertex → inner vertex + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 0.8 + for (let i = 0; i < 6; i++) { + ctx.beginPath(); ctx.moveTo(pts[i][0], pts[i][1]); ctx.lineTo(inner[i][0], inner[i][1]); ctx.stroke() + } + }, }, - // 2 — Sapphire: deep blue, hexagon rotated + + // 2 — Sapphire: deep blue, hexagon rotated (default facets) { - sides: 6, rotation: Math.PI / 6, hi: '#80A8FF', mid: '#1838C8', shadow: '#060848', rim: '#3058F0', - facetHi: 'rgba(130,170,255,0.55)', facetLo: 'rgba(4,8,55,0.45)' + facetHi: 'rgba(130,170,255,0.55)', facetLo: 'rgba(4,8,55,0.45)', + outline: polygonOutline(6, Math.PI / 6), }, - // 3 — Emerald: deep green, octagon + + // 3 — Emerald: deep green, emerald cut — octagonal outline, rectangle table inside { - sides: 8, rotation: Math.PI / 8, hi: '#70E090', mid: '#108040', shadow: '#022810', rim: '#28B060', - facetHi: 'rgba(100,230,140,0.55)', facetLo: 'rgba(2,24,8,0.45)' + facetHi: 'rgba(100,230,140,0.55)', facetLo: 'rgba(2,24,8,0.45)', + outline: (cx, cy, r) => { + const hw = r * 0.62, hh = r * 0.88, cut = r * 0.18 + return [ + [cx - hw + cut, cy - hh], // top edge: left + [cx + hw - cut, cy - hh], // top edge: right + [cx + hw, cy - hh + cut], // right edge: top + [cx + hw, cy + hh - cut], // right edge: bottom + [cx + hw - cut, cy + hh], // bottom edge: right + [cx - hw + cut, cy + hh], // bottom edge: left + [cx - hw, cy + hh - cut], // left edge: bottom + [cx - hw, cy - hh + cut], // left edge: top + ] + }, + drawFacets: (ctx, cx, cy, r, pts, g) => { + // Inner rectangle (the "table") + const hw = r * 0.35, hh = r * 0.52 + const tbl: GemPts = [ + [cx - hw, cy - hh], [cx + hw, cy - hh], + [cx + hw, cy + hh], [cx - hw, cy + hh], + ] + // Four side trapezoids + const sides: Array<[GemPts[0], GemPts[0], GemPts[0], GemPts[0], string]> = [ + [pts[0], pts[1], tbl[1], tbl[0], g.facetHi], // top + [pts[2], pts[3], tbl[2], tbl[1], g.facetLo], // right + [pts[4], pts[5], tbl[3], tbl[2], g.facetHi], // bottom + [pts[6], pts[7], tbl[0], tbl[3], g.facetLo], // left + ] + for (const [a, b, c, d, col] of sides) { + ctx.beginPath() + ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.lineTo(c[0], c[1]); ctx.lineTo(d[0], d[1]) + ctx.closePath(); ctx.fillStyle = col; ctx.fill() + } + // Four corner triangles (the cut corners) + const corners: Array<[GemPts[0], GemPts[0], GemPts[0]]> = [ + [pts[1], pts[2], tbl[1]], + [pts[3], pts[4], tbl[2]], + [pts[5], pts[6], tbl[3]], + [pts[7], pts[0], tbl[0]], + ] + for (const [a, b, ic] of corners) { + ctx.beginPath() + ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.lineTo(ic[0], ic[1]) + ctx.closePath(); ctx.fillStyle = g.facetLo; ctx.fill() + } + // Inner rectangle outline + tracePoly(ctx, tbl); ctx.strokeStyle = g.rim; ctx.lineWidth = 1.0; ctx.stroke() + // Lines from outer cut-corner points to inner rect corners + ctx.strokeStyle = 'rgba(255,255,255,0.30)'; ctx.lineWidth = 0.8 + const outerCornerPts = [pts[1], pts[3], pts[5], pts[7]] + for (let i = 0; i < 4; i++) { + ctx.beginPath() + ctx.moveTo(outerCornerPts[i][0], outerCornerPts[i][1]) + ctx.lineTo(tbl[i][0], tbl[i][1]); ctx.stroke() + } + }, }, - // 4 — Amethyst: purple, hexagon + + // 4 — Amethyst: purple, hexagon (default facets) { - sides: 6, rotation: Math.PI / 12, hi: '#D080FF', mid: '#7020C0', shadow: '#280040', rim: '#A030E0', - facetHi: 'rgba(205,105,255,0.55)', facetLo: 'rgba(28,0,50,0.45)' + facetHi: 'rgba(205,105,255,0.55)', facetLo: 'rgba(28,0,50,0.45)', + outline: polygonOutline(6, Math.PI / 12), }, ] -function gemPolygon(cx: number, cy: number, r: number, sides: number, rot: number): Array<[number, number]> { - const pts: Array<[number, number]> = [] - for (let i = 0; i < sides; i++) { - const a = rot + (i / sides) * Math.PI * 2 - pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]) - } - return pts -} - function tracePoly(ctx: CanvasRenderingContext2D, pts: Array<[number, number]>): void { ctx.beginPath() ctx.moveTo(pts[0][0], pts[0][1]) @@ -102,11 +212,11 @@ function buildGemSprite(radius: number, gemVariant: number): HTMLCanvasElement { ctx.filter = `blur(${Math.ceil(pad * 0.55)}px)` ctx.globalAlpha = 0.5 ctx.fillStyle = '#000' - tracePoly(ctx, gemPolygon(cx + pad * 0.15, cy + pad * 0.35, radius * 0.9, g.sides, g.rotation)) + tracePoly(ctx, g.outline(cx + pad * 0.15, cy + pad * 0.35, radius * 0.9)) ctx.fill() ctx.restore() - const pts = gemPolygon(cx, cy, radius, g.sides, g.rotation) + const pts = g.outline(cx, cy, radius) // Base radial gradient fill const grad = ctx.createRadialGradient(cx - radius * 0.3, cy - radius * 0.35, radius * 0.05, cx, cy, radius) @@ -117,20 +227,25 @@ function buildGemSprite(radius: number, gemVariant: number): HTMLCanvasElement { ctx.fillStyle = grad ctx.fill() - // Alternating facet triangles + // Facets (clipped to outline) ctx.save() tracePoly(ctx, pts) ctx.clip() - for (let i = 0; i < pts.length; i++) { - const [ax, ay] = pts[i] - const [bx, by] = pts[(i + 1) % pts.length] - ctx.beginPath() - ctx.moveTo(cx, cy) - ctx.lineTo(ax, ay) - ctx.lineTo(bx, by) - ctx.closePath() - ctx.fillStyle = i % 2 === 0 ? g.facetHi : g.facetLo - ctx.fill() + if (g.drawFacets) { + g.drawFacets(ctx, cx, cy, radius, pts, g) + } else { + // Default: alternating triangles from centre to each edge + for (let i = 0; i < pts.length; i++) { + const [ax, ay] = pts[i] + const [bx, by] = pts[(i + 1) % pts.length] + ctx.beginPath() + ctx.moveTo(cx, cy) + ctx.lineTo(ax, ay) + ctx.lineTo(bx, by) + ctx.closePath() + ctx.fillStyle = i % 2 === 0 ? g.facetHi : g.facetLo + ctx.fill() + } } ctx.restore() @@ -140,11 +255,13 @@ function buildGemSprite(radius: number, gemVariant: number): HTMLCanvasElement { ctx.lineWidth = Math.max(1.5, radius * 0.04) ctx.stroke() - // Faint facet-edge lines from centre to vertices - ctx.strokeStyle = 'rgba(255,255,255,0.22)' - ctx.lineWidth = 1 - for (const [px, py] of pts) { - ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(px, py); ctx.stroke() + // Centre-to-vertex lines (only for default facet gems — custom ones handle their own) + if (!g.drawFacets) { + ctx.strokeStyle = 'rgba(255,255,255,0.22)' + ctx.lineWidth = 1 + for (const [px, py] of pts) { + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(px, py); ctx.stroke() + } } // Specular highlight — top-left soft glow