// Shared grid and drawing utilities for display and editor builds ;(function () { const COLS = 12; const ROWS = 16; const CELL = 8; // logical base cell const BASE = COLS * CELL; // 128 logical units const BACKGROUND_COLOR = '#F5F1EC'; const DEFAULT_BACKGROUND_STROKE = '#DDD7D3'; const BLOCK_COLORS = ['#014AA5', '#EA385B', '#009D70', '#FD7544']; const PADDING_PX = 2; // inner padding to avoid stroke clipping // Pattern helper: [1,1,2] repeating; even rows (0-based) start with double function getRowSegments(rowIndex) { let patternIndex = (rowIndex % 2 === 0) ? 2 : 0; // 2 => double first const segments = []; let col = 0; while (col < COLS) { const span = (patternIndex % 3 === 2) ? 2 : 1; const draw = Math.min(span, COLS - col); segments.push({ col, span: draw }); col += draw; patternIndex++; } return segments; } function computeCanvasSize(containerEl) { const b = containerEl.getBoundingClientRect(); return Math.floor(Math.min(b.width, b.height)); } function computeCanvasDims(containerEl) { const b = containerEl.getBoundingClientRect(); // Use container width to determine cell size to avoid 0-height issues const cell = Math.max(1, Math.floor(b.width / COLS)); return { width: COLS * cell, height: ROWS * cell, cellSize: cell }; } function cellSizeFromCanvasWidth(w) { return (w / BASE) * CELL; } function axisUnit(a, b) { const dx = b.x - a.x; const dy = b.y - a.y; if (Math.abs(dx) >= Math.abs(dy)) { return { x: Math.sign(dx) || 0, y: 0, len: Math.abs(dx) }; } else { return { x: 0, y: Math.sign(dy) || 0, len: Math.abs(dy) }; } } function normalizeAngle(a) { while (a <= -Math.PI) a += Math.PI * 2; while (a > Math.PI) a -= Math.PI * 2; return a; } // Polyline-based connector rendering (points in grid units) with rounded corners function drawConnectorPath(ctx, cellSize, points) { if (!points || points.length < 2) return; const rTarget = 16; const P = points.map(pt => ({ x: pt.x * cellSize, y: pt.y * cellSize })); ctx.save(); ctx.strokeStyle = '#000000'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.setLineDash([2, 6]); ctx.beginPath(); ctx.moveTo(P[0].x, P[0].y); for (let i = 0; i < P.length - 1; i++) { const a = P[i]; const b = P[i + 1]; const hasNext = (i + 2 < P.length); if (hasNext) { const c = P[i + 2]; const u1 = axisUnit(a, b); const u2 = axisUnit(b, c); // Straight segment if (u1.x === u2.x && u1.y === u2.y) { ctx.lineTo(b.x, b.y); } else { // Bend: trim and round with arcTo const len1 = u1.len; const len2 = u2.len; const r = Math.min(rTarget, len1 / 2, len2 / 2); const pb = { x: b.x - u1.x * r, y: b.y - u1.y * r }; const pa = { x: b.x + u2.x * r, y: b.y + u2.y * r }; ctx.lineTo(pb.x, pb.y); ctx.arcTo(b.x, b.y, pa.x, pa.y, r); } } else { ctx.lineTo(b.x, b.y); } } ctx.stroke(); // end caps ctx.setLineDash([]); ctx.beginPath(); ctx.fillStyle = '#000000'; ctx.arc(P[0].x, P[0].y, 4, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(P[P.length - 1].x, P[P.length - 1].y, 4, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawConnectorPathAnimated(ctx, cellSize, points, progress01) { if (!points || points.length < 2) return; const rTarget = 16; const P = points.map(pt => ({ x: pt.x * cellSize, y: pt.y * cellSize })); // Build polyline segments with rounded corners flattened into small line segments const segs = []; function pushLine(x1, y1, x2, y2) { const len = Math.hypot(x2 - x1, y2 - y1); if (len > 0) segs.push({ x1, y1, x2, y2, len }); } let curr = { x: P[0].x, y: P[0].y }; for (let i = 0; i < P.length - 1; i++) { const a = P[i]; const b = P[i + 1]; const hasNext = (i + 2 < P.length); if (hasNext) { const c = P[i + 2]; const u1 = axisUnit(a, b); const u2 = axisUnit(b, c); if (u1.x === u2.x && u1.y === u2.y) { pushLine(curr.x, curr.y, b.x, b.y); curr = { x: b.x, y: b.y }; } else { const len1 = u1.len; const len2 = u2.len; const r = Math.min(rTarget, len1 / 2, len2 / 2); const pb = { x: b.x - u1.x * r, y: b.y - u1.y * r }; const pa = { x: b.x + u2.x * r, y: b.y + u2.y * r }; // Straight to trimmed point pushLine(curr.x, curr.y, pb.x, pb.y); // Quarter-arc polyline const cx = b.x + (-u1.x + u2.x) * r; const cy = b.y + (-u1.y + u2.y) * r; const a1 = Math.atan2(pb.y - cy, pb.x - cx); const a2 = Math.atan2(pa.y - cy, pa.x - cx); let diff = a2 - a1; while (diff <= -Math.PI) diff += Math.PI * 2; while (diff > Math.PI) diff -= Math.PI * 2; const steps = Math.max(8, Math.ceil(Math.abs(diff) * r / 2)); let px = pb.x, py = pb.y; for (let s = 1; s <= steps; s++) { const tseg = s / steps; const ang = a1 + diff * tseg; const x = cx + r * Math.cos(ang); const y = cy + r * Math.sin(ang); pushLine(px, py, x, y); px = x; py = y; } curr = { x: pa.x, y: pa.y }; } } else { pushLine(curr.x, curr.y, b.x, b.y); curr = { x: b.x, y: b.y }; } } const total = segs.reduce((s, g) => s + g.len, 0); const t = Math.max(0, Math.min(1, progress01)); // easeInOutCubic const eased = (t < 0.5) ? (4 * t * t * t) : (1 - Math.pow(-2 * t + 2, 3) / 2); const drawLen = eased * total; ctx.save(); ctx.strokeStyle = '#000000'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.setLineDash([2, 6]); ctx.beginPath(); if (segs.length > 0) ctx.moveTo(segs[0].x1, segs[0].y1); let remain = drawLen; for (const s of segs) { if (remain <= 0) break; if (remain >= s.len) { ctx.lineTo(s.x2, s.y2); remain -= s.len; } else { const tpart = remain / s.len; const x = s.x1 + (s.x2 - s.x1) * tpart; const y = s.y1 + (s.y2 - s.y1) * tpart; ctx.lineTo(x, y); remain = 0; break; } } ctx.stroke(); // start/end caps animate radius ctx.setLineDash([]); const rBase = 4; // Smoothstep helper const smooth = (a, b, x) => { const u = Math.max(0, Math.min(1, (x - a) / Math.max(1e-6, b - a))); return u * u * (3 - 2 * u); }; // Start cap scales in at the beginning if (drawLen > 0) { const sStart = smooth(0.0, 0.2, t); if (sStart > 0) { ctx.beginPath(); ctx.fillStyle = '#000000'; ctx.arc(P[0].x, P[0].y, rBase * sStart, 0, Math.PI * 2); ctx.fill(); } } // End cap scales in near completion const sEnd = smooth(0.8, 1.0, t); if (sEnd > 0) { const pend = P[P.length - 1]; ctx.beginPath(); ctx.fillStyle = '#000000'; ctx.arc(pend.x, pend.y, rBase * sEnd, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } window.SHARED = { COLS, ROWS, CELL, BASE, BACKGROUND_COLOR, DEFAULT_BACKGROUND_STROKE, BLOCK_COLORS, PADDING_PX, getRowSegments, computeCanvasSize, cellSizeFromCanvasWidth, computeCanvasDims, drawConnectorPath, drawConnectorPathAnimated, }; })();