clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / src / app / dev-view.js
dev-view.js 5.2K
1import { drawBoard } from '../render/board.js';23const DEFAULT_COLORS = Object.freeze({4 YOU: '#f06d5f',5 PUSH: '#efb34a',6 STOP: '#625e88',7 WIN: '#2d9c88',8 SINK: '#6f9ca8',9 DEFEAT: '#b45273',10});1112function drawRounded(ctx, x, y, width, height, radius, fill, stroke) {13 ctx.beginPath();14 ctx.roundRect(x, y, width, height, radius);15 ctx.fillStyle = fill;16 ctx.fill();17 if (stroke) {18 ctx.strokeStyle = stroke;19 ctx.lineWidth = 1;20 ctx.stroke();21 }22}2324/**25 * Development canvas view for the DOM shell.26 *27 * The fullscreen player is rendered by Stage. This view exists for `?dev`,28 * where the app shell exposes the state inspector and level controls.29 */30export function createDevView({31 canvas,32 ctx = canvas?.getContext?.('2d'),33 getGame,34 getPalette = () => ({}),35 glyphs = {},36 colors = DEFAULT_COLORS,37 isPlayer = () => document.body.classList.contains('stage-player'),38} = {}) {39 function render(state = getGame()?.getState()) {40 if (!state || !ctx || !canvas) return;41 const width = canvas.width;42 const height = canvas.height;43 const pad = 28;44 const cell = Math.min(45 (width - pad * 2) / state.width,46 (height - pad * 2) / state.height,47 );48 const originX = (width - cell * state.width) / 2;49 const originY = (height - cell * state.height) / 2;50 const game = getGame();51 const palette = getPalette();5253 if (isPlayer()) {54 ctx.fillStyle = '#0F0E0D';55 ctx.fillRect(0, 0, width, height);56 drawBoard(ctx, state, { x: originX, y: originY, cell, game, palette, glyphs });57 return;58 }5960 ctx.clearRect(0, 0, width, height);61 ctx.fillStyle = '#fbf7ef';62 ctx.fillRect(0, 0, width, height);63 ctx.strokeStyle = 'rgba(102,91,72,.12)';64 ctx.lineWidth = 1;65 for (let x = 0; x <= state.width; x++) {66 ctx.beginPath();67 ctx.moveTo(originX + x * cell, originY);68 ctx.lineTo(originX + x * cell, originY + state.height * cell);69 ctx.stroke();70 }71 for (let y = 0; y <= state.height; y++) {72 ctx.beginPath();73 ctx.moveTo(originX, originY + y * cell);74 ctx.lineTo(originX + state.width * cell, originY + y * cell);75 ctx.stroke();76 }7778 const stacks = new Map();79 for (const object of state.objects) {80 const key = `${object.x},${object.y}`;81 if (!stacks.has(key)) stacks.set(key, []);82 stacks.get(key).push(object);83 }84 const objects = [...state.objects].sort((a, b) =>85 (a.kind === 'word' ? 1 : 0) - (b.kind === 'word' ? 1 : 0),86 );87 for (const object of objects) {88 const stack = stacks.get(`${object.x},${object.y}`);89 const stackIndex = stack.indexOf(object);90 const stackOffset = stack.length > 191 ? (stackIndex - (stack.length - 1) / 2) * cell * 0.1192 : 0;93 const x = originX + object.x * cell + stackOffset;94 const y = originY + object.y * cell - stackOffset * 0.35;95 const isWord = object.kind === 'word';96 const name = object.noun || object.text;97 const properties = game.properties(object);98 const accent = [...properties].map(property => colors[property]).find(Boolean) || '#766f64';99100 if (isWord) {101 drawRounded(ctx, x + cell * 0.08, y + cell * 0.18, cell * 0.84, cell * 0.62, cell * 0.11, '#fffdf8', accent);102 ctx.fillStyle = accent;103 ctx.font = `700 ${Math.max(14, cell * 0.2)}px ui-monospace, monospace`;104 ctx.textAlign = 'center';105 ctx.textBaseline = 'middle';106 ctx.fillText(object.text, x + cell / 2, y + cell * 0.49);107 ctx.fillStyle = 'rgba(80,70,55,.25)';108 ctx.font = `600 ${Math.max(8, cell * 0.09)}px ui-monospace, monospace`;109 ctx.fillText('WORD', x + cell / 2, y + cell * 0.84);110 continue;111 }112113 const emoji = palette[name] || glyphs[name];114 ctx.shadowColor = properties.includes('YOU') ? 'rgba(240,109,95,.38)' : 'transparent';115 ctx.shadowBlur = properties.includes('YOU') ? 10 : 0;116 ctx.font = `${Math.max(24, cell * 0.52)}px "Apple Color Emoji","Segoe UI Emoji",sans-serif`;117 ctx.textAlign = 'center';118 ctx.textBaseline = 'middle';119 if (emoji) ctx.fillText(typeof emoji === 'object' ? emoji.glyph : emoji, x + cell / 2, y + cell * 0.52);120 else {121 ctx.fillStyle = accent;122 ctx.font = `700 ${Math.max(13, cell * 0.18)}px 'DM Mono',monospace`;123 ctx.fillText(name, x + cell / 2, y + cell * 0.52);124 }125 ctx.shadowBlur = 0;126 ctx.fillStyle = accent;127 ctx.font = `700 ${Math.max(8, cell * 0.1)}px ui-monospace,monospace`;128 ctx.fillText(name, x + cell / 2, y + cell * 0.87);129 if (properties.length) {130 ctx.strokeStyle = accent;131 ctx.lineWidth = 2;132 ctx.beginPath();133 ctx.arc(x + cell / 2, y + cell / 2, cell * 0.36, 0, Math.PI * 2);134 ctx.stroke();135 }136 if (stack.length > 1) {137 ctx.fillStyle = '#fffaf2';138 ctx.beginPath();139 ctx.arc(x + cell * 0.82, y + cell * 0.2, cell * 0.12, 0, Math.PI * 2);140 ctx.fill();141 ctx.fillStyle = accent;142 ctx.font = `700 ${Math.max(9, cell * 0.1)}px 'DM Mono',monospace`;143 ctx.fillText(String(stack.length), x + cell * 0.82, y + cell * 0.2);144 }145 }146 }147148 return Object.freeze({ render });149}150151export default createDevView;