fuzzyworld

nuts / commit

changed densities

fd4adeec1065a6476e0b4df55b2872e2ee740360 · Pavel Safronov · 2026-04-12 12:28:47 -0700 · parents a2b3b1f4ca7a

changed densities
 src/physics.ts  | 15 ++++-----
 src/renderer.ts | 98 ++++++++++++++++++++++++++++++++++++++++++---------------
 2 files changed, 78 insertions(+), 35 deletions(-)

diff --git a/src/physics.ts b/src/physics.ts
index a1f787e..a5457b0 100644
--- a/src/physics.ts
+++ b/src/physics.ts
@@ -58,6 +58,7 @@ function spawnNut(x: number, y: number, nutRadius: number, colorIndex: number):
     }
 
     const body = Bodies.fromVertices(x, y, [verts], {
+        density: 0.0006,  // walnut ~0.6 g/cm³ — lighter than water
         restitution: CONFIG.restitution,
         friction: CONFIG.friction,
         frictionAir: CONFIG.frictionAir,
@@ -69,14 +70,8 @@ function spawnNut(x: number, y: number, nutRadius: number, colorIndex: number):
 function spawnMetalNut(x: number, y: number, nutRadius: number, colorIndex: number): NutBody {
     const variance = 1 + (Math.random() * 2 - 1) * CONFIG.nutRadiusVariance
     const radius = nutRadius * variance
-    const ry = radius / CONFIG.nutAspectRatio
-    const N = 20
-    const verts: Matter.Vector[] = []
-    for (let i = 0; i < N; i++) {
-        const a = (i / N) * Math.PI * 2
-        verts.push(Vector.create(Math.cos(a) * radius, Math.sin(a) * ry))
-    }
-    const body = Bodies.fromVertices(x, y, [verts], {
+    const body = Bodies.polygon(x, y, 6, radius, {
+        density: 0.006,  // ~6× default (0.001) — steel-like weight
         restitution: 0.92,
         friction: 0,
         frictionStatic: 0,
@@ -95,6 +90,7 @@ function spawnGem(x: number, y: number, radius: number, gemVariant: number): Nut
     const actualRadius = radius * variance
     const sides = GEM_SIDES[gemVariant % GEM_SIDES.length]
     const body = Bodies.polygon(x, y, sides, actualRadius, {
+        density: 0.0035,  // crystal/gemstone ~3.5 g/cm³
         restitution: 0.95,
         friction: 0,
         frictionStatic: 0,
@@ -105,9 +101,10 @@ function spawnGem(x: number, y: number, radius: number, gemVariant: number): Nut
 }
 
 function spawnBalloon(x: number, y: number, radius: number, colorIndex: number): NutBody {
-    const variance = 1 + (Math.random() * 2 - 1) * CONFIG.nutRadiusVariance * 0.5
+    const variance = 0.5 + Math.random() * 1.0  // 0.5× to 1.5× base radius
     const actualRadius = radius * variance
     const body = Bodies.circle(x, y, actualRadius, {
+        density: 0.00005,  // air-filled balloon — nearly weightless
         restitution: 0.8,
         friction: 0.01,
         frictionAir: 0.008,
diff --git a/src/renderer.ts b/src/renderer.ts
index af4ce56..e37b386 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -213,62 +213,108 @@ function buildSprite(radius: number, colorIndex: number): HTMLCanvasElement {
 
 function buildMetalSprite(radius: number, colorIndex: number): HTMLCanvasElement {
     const c = METAL_PALETTE[colorIndex % METAL_PALETTE.length]
-    const rx = radius
-    const ry = Math.round(radius / CONFIG.nutAspectRatio)
-    const pad = Math.ceil(rx * 0.35)
-    const sizeW = Math.ceil(rx * 2 + pad * 2)
-    const sizeH = Math.ceil(ry * 2 + pad * 2)
+    const pad = Math.ceil(radius * 0.35)
+    const size = Math.ceil(radius * 2 + pad * 2)
     const oc = document.createElement('canvas')
-    oc.width = sizeW
-    oc.height = sizeH
+    oc.width = size
+    oc.height = size
     const ctx = oc.getContext('2d')!
-    const cx = sizeW / 2
-    const cy = sizeH / 2
+    const cx = size / 2
+    const cy = size / 2
+    const holeR = radius * 0.38  // bolt hole radius
 
-    // Drop shadow
+    // Helper: hexagon path (pointy-top, 6 sides)
+    function hexPath(r: number, ox = 0, oy = 0) {
+        ctx.beginPath()
+        for (let i = 0; i < 6; i++) {
+            const a = (i / 6) * Math.PI * 2
+            i === 0
+                ? ctx.moveTo(ox + Math.cos(a) * r, oy + Math.sin(a) * r)
+                : ctx.lineTo(ox + Math.cos(a) * r, oy + Math.sin(a) * r)
+        }
+        ctx.closePath()
+    }
+
+    // Drop shadow – hex minus hole via evenodd winding
     ctx.save()
     ctx.filter = `blur(${Math.ceil(pad * 0.55)}px)`
     ctx.globalAlpha = 0.6
     ctx.fillStyle = '#000'
     ctx.beginPath()
-    ctx.ellipse(cx + pad * 0.1, cy + pad * 0.3, rx * 0.88, ry * 0.88, 0, 0, Math.PI * 2)
-    ctx.fill()
+    const sox = cx + pad * 0.08, soy = cy + pad * 0.28
+    for (let i = 0; i < 6; i++) {
+        const a = (i / 6) * Math.PI * 2
+        i === 0 ? ctx.moveTo(sox + Math.cos(a) * radius * 0.88, soy + Math.sin(a) * radius * 0.88)
+            : ctx.lineTo(sox + Math.cos(a) * radius * 0.88, soy + Math.sin(a) * radius * 0.88)
+    }
+    ctx.closePath()
+    ctx.arc(sox, soy, holeR * 0.85, 0, Math.PI * 2, true)
+    ctx.fill('evenodd')
     ctx.restore()
 
-    // Base metallic radial gradient – high contrast for chrome look
+    // Base metallic radial gradient
     const grad = ctx.createRadialGradient(
-        cx - rx * 0.3, cy - ry * 0.38, rx * 0.02,
-        cx, cy, rx
+        cx - radius * 0.3, cy - radius * 0.35, radius * 0.02,
+        cx, cy, radius
     )
     grad.addColorStop(0, c.hi)
     grad.addColorStop(0.25, c.mid)
     grad.addColorStop(0.6, c.shadow)
     grad.addColorStop(0.85, c.mid)
     grad.addColorStop(1, c.rim)
+    // Hex + hole (evenodd = hole is empty)
     ctx.beginPath()
-    ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2)
+    for (let i = 0; i < 6; i++) {
+        const a = (i / 6) * Math.PI * 2
+        i === 0 ? ctx.moveTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius)
+            : ctx.lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius)
+    }
+    ctx.closePath()
+    ctx.arc(cx, cy, holeR, 0, Math.PI * 2, true)
     ctx.fillStyle = grad
-    ctx.fill()
+    ctx.fill('evenodd')
 
-    // Rim stroke
-    ctx.lineWidth = Math.max(1.5, rx * 0.045)
+    // Outer rim stroke
+    hexPath(radius, cx, cy)
+    ctx.lineWidth = Math.max(1.5, radius * 0.045)
     ctx.strokeStyle = c.rim
     ctx.stroke()
 
-    // Broad specular band – the key metallic look
-    const specGrad = ctx.createLinearGradient(cx - rx * 0.5, cy - ry * 0.6, cx + rx * 0.1, cy + ry * 0.1)
+    // Broad specular band (clipped to hex, not over hole)
+    ctx.save()
+    ctx.beginPath()
+    for (let i = 0; i < 6; i++) {
+        const a = (i / 6) * Math.PI * 2
+        i === 0 ? ctx.moveTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius)
+            : ctx.lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius)
+    }
+    ctx.closePath()
+    ctx.arc(cx, cy, holeR, 0, Math.PI * 2, true)
+    ctx.clip('evenodd')
+    const specGrad = ctx.createLinearGradient(cx - radius * 0.5, cy - radius * 0.6, cx + radius * 0.1, cy + radius * 0.1)
     specGrad.addColorStop(0, 'rgba(255,255,255,0.0)')
     specGrad.addColorStop(0.3, 'rgba(255,255,255,0.45)')
     specGrad.addColorStop(0.6, 'rgba(255,255,255,0.22)')
     specGrad.addColorStop(1, 'rgba(255,255,255,0.0)')
-    ctx.beginPath()
-    ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2)
     ctx.fillStyle = specGrad
-    ctx.fill()
+    ctx.fillRect(0, 0, size, size)
+    ctx.restore()
+
+    // Hole inner bevel — dark outer ring + lighter inner ring
+    ctx.beginPath()
+    ctx.arc(cx, cy, holeR, 0, Math.PI * 2)
+    ctx.lineWidth = Math.max(1.5, radius * 0.06)
+    ctx.strokeStyle = c.rim
+    ctx.stroke()
+    ctx.beginPath()
+    ctx.arc(cx, cy, holeR - ctx.lineWidth * 0.6, 0, Math.PI * 2)
+    ctx.lineWidth = Math.max(1, radius * 0.03)
+    ctx.strokeStyle = 'rgba(255,255,255,0.25)'
+    ctx.stroke()
 
-    // Sharp bright specular dot
+    // Sharp bright specular dot (on upper-left face)
     ctx.beginPath()
-    ctx.ellipse(cx - rx * 0.28, cy - ry * 0.33, rx * 0.18, ry * 0.11, -0.4, 0, Math.PI * 2)
+    ctx.ellipse(cx - radius * 0.28, cy - radius * 0.3, radius * 0.16, radius * 0.10, -0.4, 0, Math.PI * 2)
     ctx.fillStyle = 'rgba(255,255,255,0.72)'
     ctx.fill()