PURPOSE

Procedural low-poly 3D cube renderer used for destructible crates. Draws a unit cube with multi-axis tumble rotation, per-face flat shading (diffuse + specular + rim), screen-position perspective tilt, painter’s-algorithm back-to-front face sorting, and a golden stroke overlay. Matches the asteroid renderer’s visual quality so crates tumble visibly in 3D rather than reading as flat sprites.

OWNS

  • drawCrate3D — the only exported function; draws one crate per call into a 2D canvas context.
  • Internal cube mesh constants: CUBE_VERTS (8 vertices of a unit cube centered at origin, half-size 1) and CUBE_FACES (12 triangles, two per cube face, each tagged with a face normal and a faceIdx in 0–5 for per-face color variation).
  • Lighting constants LIGHT (normalized direction from LX, LY, LZ), AMBIENT, DIFFUSE, SPEC_STR, SPEC_POW, RIM_STR, RIM_POW.
  • Per-axis rotation helpers rotateX, rotateY, rotateZ and the composed rotateCube (X tilt then Y tilt then Z spin).
  • Shading helpers dot, hexToRgb, shadeCubeFace (Lambertian diffuse + Blinn-Phong specular + Fresnel rim + per-face tint shift + damage-flash blend toward white).
  • The virtual camera height constant PERSPECTIVE_HEIGHT (800 world units), kept in sync with draw-3d-terrain.ts.

READS FROM

  • ../core — imports camera, W, H for camera zoom and screen dimensions.
  • ./camera — imports the Camera class and uses Camera.toS(x, y) to convert world coordinates to screen coordinates.
  • Call-site arguments: world x/y, cubeSize, current rotAngle (continuously advancing Z spin), baseColorHex, edgeColorHex, optional flashAmount (damage flash, default 0), optional tiltX (default 0.5) and tiltY (default 0.3) for the per-crate fixed tilt.

PUSHES TO

  • The provided CanvasRenderingContext2D only. All output is direct 2D canvas draw calls. Mutates fillStyle, strokeStyle, lineJoin, lineWidth inside a ctx.save() / ctx.restore() pair.
  • No game state writes, no events, no telemetry, no module-level mutable state.

DOES NOT

  • Does not own or update crate game state (position, health, rotation angle, tilt axes) — caller passes those in each frame.
  • Does not handle input, collision, damage logic, or destruction effects.
  • Does not load textures or assets — colors come in as hex strings.
  • Does not allocate persistent buffers across calls; per-call arrays for projected vertices and sorted faces are recreated each invocation.
  • Does not draw any background, halo, glow, particle, or shadow — only the cube faces plus the golden overlay stroke pass.
  • Does not call any other renderer (no asteroid, no terrain, no HUD interaction).

Signals

None emitted. The module is a pure draw helper with no event bus, callback, or store interaction.

Entry points

  • drawCrate3D(ctx, x, y, cubeSize, rotAngle, baseColorHex, edgeColorHex, flashAmount?, tiltX?, tiltY?) — the sole export. Called per crate per frame by the destructible-crate rendering layer.

Pattern notes

  • Early-out: if the projected screenSize (cubeSize times camera zoom) is below 2 pixels, the function returns immediately without drawing.
  • Perspective tilt is computed from the crate’s screen offset from screen center divided by PERSPECTIVE_HEIGHT, producing perspTiltX = atan2(worldOffY, PERSPECTIVE_HEIGHT) and perspTiltY = atan2(-worldOffX, PERSPECTIVE_HEIGHT). This is added to the per-crate fixed tilt before the Z spin so crates near screen edges visibly lean toward the camera origin.
  • Rotation order is X tilt then Y tilt then Z spin, applied to every vertex and every face normal so backface culling and face sorting use the same rotated frame as the geometry.
  • Backface cull: faces with rotated normal z < -0.01 are skipped before adding to the sort list.
  • Face sort is painter’s algorithm — back-to-front by average rotated Z of the three triangle vertices.
  • Shading model in shadeCubeFace: ambient + diffuse-dot-clamped + Blinn-Phong specular against half-vector (LIGHT.x, LIGHT.y, LIGHT.z + 1) + Fresnel rim from 1 - |normal.z|. Brightness is clamped to 1 before applying the per-face tint shift of (faceIdx % 3) * 0.06 - 0.06. Final RGB is clamped to 0–255 and emitted as an rgb(...) string.
  • Damage flash blends each channel toward 255 by flashAmount (expected 0–1 range).
  • Two-pass stroke: first pass strokes each triangle with the supplied edgeColorHex at line width max(0.5, 0.8 * zoom); second pass strokes the same triangles with a fixed warm overlay rgba(255, 215, 100, 0.5) at max(0.5, 1.2 * zoom) for the golden shimmer.
  • Lighting constants and PERSPECTIVE_HEIGHT are duplicated from draw-3d-terrain.ts intentionally so crates and terrain share the same visual look; changing one without the other will break visual consistency.
  • The mesh is described per-triangle rather than per-quad, with two triangles sharing an explicit faceIdx so both halves of a cube face shade identically and receive the same per-face tint shift.