fuzzyworld

nuts / commit

gems updated

1f6f4741d372a67e14757616f79f7d520f2aa81c · Pavel Safronov · 2026-04-12 21:02:20 -0700 · parents aa6e5a990499

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