// Selection previews: labelled tags, copy and cut boxes, and paste ghosts. // a small labelled tag drawn on the world (screen pixels) function drawTag(text, col, x, y, sub) { const ctx = R.ctx, u = Math.max(1, R.canvas.width / window.innerWidth); renderMonoFont(); ctx.save(); ctx.font = `700 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle'; const tw = ctx.measureText(text).width; ctx.font = `500 ${Math.round(11 * u)}px ${R.monoFont}`; const sw = sub ? ctx.measureText(sub).width + 8 * u : 0; const h = Math.round(19 * u), pad = 7 * u, w = tw + sw + pad * 2; x = Math.max(2, Math.min(R.canvas.width - w - 2, x)); y = Math.max(2, Math.min(R.canvas.height - h - 2, y)); ctx.fillStyle = 'rgba(14,12,11,0.9)'; ctx.fillRect(x, y, w, h); ctx.fillStyle = `rgb(${col})`; ctx.fillRect(x, y, 3 * u, h); ctx.strokeStyle = `rgba(${col},0.8)`; ctx.lineWidth = u; ctx.strokeRect(x + u / 2, y + u / 2, w - u, h - u); ctx.font = `700 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.fillStyle = `rgb(${col})`; ctx.fillText(text, x + pad, y + h / 2 + u * 0.5); if (sub) { ctx.font = `500 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.fillStyle = 'rgba(231,220,203,0.9)'; ctx.fillText(sub, x + pad + tw + 8 * u, y + h / 2 + u * 0.5); } ctx.restore(); } // Copy (teal) and cut (red, hatched): the box being dragged, what it would pick up, and a tag const SEL_COL = { copy: '92,186,166', cut: '239,106,85' }; function drawSelection(mode, ax, ay) { const ctx = R.ctx, zp = R.zp, col = SEL_COL[mode], sd = GAME.selDrag; const sx = sd ? sd.x : ax, sy = sd ? sd.y : ay; const a = blockOf(Math.min(sx, ax), Math.min(sy, ay)), c = blockOf(Math.max(sx, ax), Math.max(sy, ay)); const bw = c.bx - a.bx + 1, bh = c.by - a.by + 1; drawGrid(blockOf(ax, ay).bx, blockOf(ax, ay).by, 7); ctx.save(); // what is inside: built blocks, and machines and molds that fit wholly inside let n = 0; if (sd) { ctx.fillStyle = `rgba(${col},${mode === 'cut' ? 0.42 : 0.3})`; for (let by = a.by; by <= c.by; by++) for (let bx = a.bx; bx <= c.bx; bx++) { if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue; const b = by * BW + bx; if (blockP[b] && blockIntegrity(b) >= 8) { const [x, y, w, h] = blockRectPx(bx, by, 1, 1); ctx.fillRect(x, y, w, h); n++; } } ctx.strokeStyle = `rgba(${col},0.95)`; ctx.lineWidth = Math.max(1, zp * 0.35); for (const mm of machines) { if (!mm || PIECE_BY_ID[mm.type].fixed) continue; const P = PIECE_BY_ID[mm.type], mbx = mm.x / BS, mby = mm.y / BS; if (mbx >= a.bx && mby >= a.by && mbx + P.bw - 1 <= c.bx && mby + P.bh - 1 <= c.by) { const [x, y, w, h] = blockRectPx(mbx, mby, P.bw, P.bh); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); n++; } } for (const mo of molds) { const md = MOLDS[mo.kind], mbx = mo.x / BS, mby = mo.y / BS; if (mbx >= a.bx && mby >= a.by && mbx + md.bw - 1 <= c.bx && mby + md.bh - 1 <= c.by) { const [x, y, w, h] = blockRectPx(mbx, mby, md.bw, md.bh); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); n++; } } } const [x, y, w, h] = blockRectPx(a.bx, a.by, bw, bh); ctx.fillStyle = `rgba(${col},0.08)`; ctx.fillRect(x, y, w, h); if (mode === 'cut') { // red hatching: this will be taken down ctx.save(); ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip(); ctx.strokeStyle = `rgba(${col},0.35)`; ctx.lineWidth = Math.max(1, zp * 0.25); const step = Math.max(8, zp * 2.5); ctx.beginPath(); for (let k = -h; k < w; k += step) { ctx.moveTo(x + k, y + h); ctx.lineTo(x + k + h, y); } ctx.stroke(); ctx.restore(); } ctx.strokeStyle = `rgba(${col},0.98)`; ctx.lineWidth = Math.max(1.5, zp * (mode === 'cut' ? 0.45 : 0.35)); ctx.setLineDash([zp * 1.2, zp * 0.8]); ctx.lineDashOffset = -(R.frame * 0.4) % (zp * 2); ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); ctx.lineDashOffset = 0; ctx.restore(); const label = mode === 'cut' ? '✂ CUT' : '⧉ COPY'; const sub = !sd ? 'drag a box' : n ? (mode === 'cut' ? `${plural(n, 'piece')} · let go to take down` : `${plural(n, 'piece')} · let go to copy`) : 'nothing built in here'; drawTag(label, col, x, y - Math.round(23 * Math.max(1, R.canvas.width / window.innerWidth)), sub); } // Pasting: the clipboard follows the cursor, snapped to the grid function drawPastePreview() { const ctx = R.ctx, zp = R.zp, bp = GAME.clip, { bx: ox, by: oy } = stampOrigin(bp.bw, bp.bh); drawGrid(aimBlock().bx, aimBlock().by, 7); ctx.save(); const [x, y, w, h] = blockRectPx(ox, oy, bp.bw, bp.bh); ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3); ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; ctx.fillRect(bx2, by2, bw2, bh2); } for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); } for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); } ctx.restore(); drawTag('PASTE', '92,186,166', x, y - Math.round(23 * Math.max(1, R.canvas.width / window.innerWidth)), 'click to place · R flips · Esc stops'); }