testsAndMisc-archive/bucket_catch/packages/frontend/src/hooks/puzzleRender.ts
Krzysztof kuhy Rudnicki e67dfb185d bucket_catch: split usePuzzleGameLoop.ts 386 -> 194 + two modules
puzzleGeometry.ts (127)  board constants, ScheduledPiece, and the Union-Find
                           interval clustering in assignXPositions that makes a
                           100% score achievable; plus aabbCollision
  puzzleRender.ts (110)    the four canvas draw functions

Both were module-private, so nothing outside the hook sees either seam.

One thing was reshaped rather than moved: the inline clearRect+gradient in the
tick became a named drawBackdrop, so the render module owns every canvas call.
That is why coverage goes 583/217/97/534 -> 584/217/98/535 -- one statement,
one function, branches unchanged.

eslint reports 45 errors, exactly as it did before the split, which is the
check that matters here: the extraction added none. Those 45 are pre-existing
and were unmasked by the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 10:05:37 +02:00

111 lines
2.9 KiB
TypeScript

import type { FallingPuzzleItem } from "../types";
import {
BASKET_HALF_WIDTH,
BASKET_HEIGHT,
PIECE_HALF_H,
PIECE_HALF_W,
} from "./puzzleGeometry";
/**
* Canvas drawing for the puzzle game.
*
* Nothing here reads or writes game state — each function takes what it needs
* and paints. Keeping the rendering separate from the tick means the loop in
* `usePuzzleGameLoop` reads as scheduling and collision, not as drawing calls.
*/
export function drawBasket(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
): void {
const h = BASKET_HEIGHT;
ctx.save();
ctx.strokeStyle = "#f472b6";
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(x - BASKET_HALF_WIDTH, y - h / 2);
ctx.lineTo(x - BASKET_HALF_WIDTH, y + h / 2);
ctx.lineTo(x + BASKET_HALF_WIDTH, y + h / 2);
ctx.lineTo(x + BASKET_HALF_WIDTH, y - h / 2);
ctx.stroke();
ctx.strokeStyle = "#e879f9";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(x - BASKET_HALF_WIDTH - 6, y - h / 2);
ctx.lineTo(x + BASKET_HALF_WIDTH + 6, y - h / 2);
ctx.stroke();
ctx.restore();
}
export function drawPiece(
ctx: CanvasRenderingContext2D,
item: FallingPuzzleItem,
img: HTMLImageElement | undefined,
caught: boolean,
): void {
const w = PIECE_HALF_W * 2;
const h = PIECE_HALF_H * 2;
const x = item.x - PIECE_HALF_W;
const y = item.y - PIECE_HALF_H;
ctx.save();
ctx.globalAlpha = caught ? 0.35 : 1;
if (img?.complete && img.naturalWidth > 0) {
ctx.drawImage(img, x, y, w, h);
} else {
ctx.fillStyle = "rgba(129, 140, 248, 0.5)";
ctx.fillRect(x, y, w, h);
}
ctx.strokeStyle = caught ? "#34d399" : "#f472b6";
ctx.lineWidth = caught ? 3 : 2;
ctx.strokeRect(x, y, w, h);
ctx.restore();
}
export function drawHUD(
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
caught: number,
total: number,
piecesDone: number,
): void {
ctx.save();
ctx.fillStyle = "rgba(0,0,0,0.45)";
ctx.roundRect(canvas.width - 160, 12, 148, 36, 8);
ctx.fill();
ctx.fillStyle = "#a5b4fc";
ctx.font = "bold 14px monospace";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(`✅ ${caught} / ${total}`, canvas.width - 20, 30);
ctx.restore();
const barW = 200;
const barX = (canvas.width - barW) / 2;
ctx.save();
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.roundRect(barX, 12, barW, 8, 4);
ctx.fill();
ctx.fillStyle = "#818cf8";
ctx.roundRect(barX, 12, barW * (piecesDone / total), 8, 4);
ctx.fill();
ctx.restore();
}
/** Paints the backdrop gradient the pieces fall against. */
export function drawBackdrop(
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
): void {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, "#0f0c29");
grad.addColorStop(1, "#302b63");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}