nuts / commit
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()