148 lines
5.0 KiB
JavaScript
148 lines
5.0 KiB
JavaScript
/**
|
||
* ConnectorGeometry.js
|
||
*
|
||
* Draws a closed canvas-2D path for a single puzzle piece.
|
||
* The piece's grid cell occupies [tabSize, tabSize] to [tabSize+pieceW, tabSize+pieceH]
|
||
* within an off-screen canvas of size (pieceW+2*tabSize) × (pieceH+2*tabSize).
|
||
*
|
||
* Tab/blank bumps occupy the middle third of each internal edge.
|
||
* tabSize should be >= pieceW * 0.3 to give the tab room to protrude.
|
||
*/
|
||
|
||
/**
|
||
* Add a connector bump between two points in canvas space.
|
||
* @param {CanvasRenderingContext2D} ctx
|
||
* @param {number} x0,y0 - start of edge segment
|
||
* @param {number} x1,y1 - end of edge segment
|
||
* @param {number} dir - +1 = tab (protrudes to left of travel), -1 = blank (indents)
|
||
*
|
||
* "Left of travel" in canvas coords:
|
||
* Travelling right (+X): left = -Y (tab protrudes upward on canvas)
|
||
* Travelling down (+Y): left = +X
|
||
* etc.
|
||
* We want the tab to protrude *outward* from the piece, so callers pass the
|
||
* edge direction and we compute the perpendicular accordingly.
|
||
*/
|
||
function _addConnectorBump(ctx, x0, y0, x1, y1, dir) {
|
||
// Edge vector
|
||
const ex = x1 - x0;
|
||
const ey = y1 - y0;
|
||
const L = Math.sqrt(ex * ex + ey * ey);
|
||
|
||
// Unit edge and perpendicular (perpendicular points "outward" when dir=+1)
|
||
const ux = ex / L;
|
||
const uy = ey / L;
|
||
// Outward perpendicular = rotate edge 90° clockwise: (uy, -ux)
|
||
const px = uy * dir;
|
||
const py = -ux * dir;
|
||
|
||
// Bump parameters as fractions of edge length
|
||
const H = L * 0.28; // bump height
|
||
const R = L * 0.10; // knob half-width
|
||
|
||
// Bump occupies centre third: t ∈ [1/3, 2/3]
|
||
// Points along the edge
|
||
const bx0 = x0 + ux * (L / 3);
|
||
const by0 = y0 + uy * (L / 3);
|
||
const bx1 = x0 + ux * (L * 2 / 3);
|
||
const by1 = y0 + uy * (L * 2 / 3);
|
||
const bmx = x0 + ux * (L / 2); // midpoint of edge
|
||
const bmy = y0 + uy * (L / 2);
|
||
|
||
// Base of knob (at H * 0.55 along perpendicular)
|
||
const kbx = bmx + px * H * 0.55;
|
||
const kby = bmy + py * H * 0.55;
|
||
// Tip of knob (at H along perpendicular)
|
||
const ktx = bmx + px * H;
|
||
const kty = bmy + py * H;
|
||
|
||
// Draw: lead-in from bump start → left knob base → knob arc → right knob base → lead-out
|
||
// Lead-in cubic
|
||
ctx.bezierCurveTo(
|
||
bx0 + ux * L * 0.04 + px * H * 0.2, by0 + uy * L * 0.04 + py * H * 0.2, // cp1
|
||
kbx - ux * R * 1.2, kby - uy * R * 1.2, // cp2
|
||
kbx - ux * R, kby - uy * R // end
|
||
);
|
||
// Knob left arc
|
||
ctx.bezierCurveTo(
|
||
ktx - ux * R, kty - uy * R, // cp1
|
||
ktx - ux * R, kty - uy * R, // cp2 (same → straight-ish)
|
||
ktx, kty // end (tip)
|
||
);
|
||
// Knob right arc
|
||
ctx.bezierCurveTo(
|
||
ktx + ux * R, kty + uy * R, // cp1
|
||
ktx + ux * R, kty + uy * R, // cp2
|
||
kbx + ux * R, kby + uy * R // end
|
||
);
|
||
// Lead-out cubic
|
||
ctx.bezierCurveTo(
|
||
kbx + ux * R * 1.2, kby + uy * R * 1.2,
|
||
bx1 - ux * L * 0.04 + px * H * 0.2, by1 - uy * L * 0.04 + py * H * 0.2,
|
||
bx1, by1
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Build the full closed clip path for a single piece.
|
||
*
|
||
* @param {CanvasRenderingContext2D} ctx
|
||
* @param {number} gridCol
|
||
* @param {number} gridRow
|
||
* @param {number} cols - total columns in grid
|
||
* @param {number} rows - total rows in grid
|
||
* @param {number} pieceW - width of one grid cell in pixels
|
||
* @param {number} pieceH - height of one grid cell in pixels
|
||
* @param {{ top, right, bottom, left }} edges - 'flat'|'tab'|'blank' per side
|
||
* @param {number} tabSize - padding around cell in canvas (= max(pieceW,pieceH)*0.3 or similar)
|
||
*/
|
||
function buildPiecePath(ctx, gridCol, gridRow, cols, rows, pieceW, pieceH, edges, tabSize) {
|
||
const x0 = tabSize; // left edge of cell on canvas
|
||
const y0 = tabSize; // top edge of cell on canvas
|
||
const x1 = tabSize + pieceW; // right edge
|
||
const y1 = tabSize + pieceH; // bottom edge
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(x0, y0);
|
||
|
||
// TOP edge: left → right
|
||
if (edges.top === 'flat') {
|
||
ctx.lineTo(x1, y0);
|
||
} else {
|
||
// Tab protrudes upward (−Y), blank indents downward (+Y)
|
||
// "outward" for top edge = −Y → dir = +1 gives perpendicular toward −Y (correct for tab)
|
||
ctx.lineTo(x0 + (x1 - x0) / 3, y0);
|
||
_addConnectorBump(ctx, x0, y0, x1, y0, edges.top === 'tab' ? -1 : +1);
|
||
ctx.lineTo(x1, y0);
|
||
}
|
||
|
||
// RIGHT edge: top → bottom
|
||
if (edges.right === 'flat') {
|
||
ctx.lineTo(x1, y1);
|
||
} else {
|
||
ctx.lineTo(x1, y0 + (y1 - y0) / 3);
|
||
_addConnectorBump(ctx, x1, y0, x1, y1, edges.right === 'tab' ? +1 : -1);
|
||
ctx.lineTo(x1, y1);
|
||
}
|
||
|
||
// BOTTOM edge: right → left
|
||
if (edges.bottom === 'flat') {
|
||
ctx.lineTo(x0, y1);
|
||
} else {
|
||
ctx.lineTo(x1 - (x1 - x0) / 3, y1);
|
||
_addConnectorBump(ctx, x1, y1, x0, y1, edges.bottom === 'tab' ? -1 : +1);
|
||
ctx.lineTo(x0, y1);
|
||
}
|
||
|
||
// LEFT edge: bottom → top
|
||
if (edges.left === 'flat') {
|
||
ctx.lineTo(x0, y0);
|
||
} else {
|
||
ctx.lineTo(x0, y1 - (y1 - y0) / 3);
|
||
_addConnectorBump(ctx, x0, y1, x0, y0, edges.left === 'tab' ? +1 : -1);
|
||
ctx.lineTo(x0, y0);
|
||
}
|
||
|
||
ctx.closePath();
|
||
}
|