clone

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

Animate copied share links as rising word tiles

Dami committed edcb9d8
commit
edcb9d800afc84117ca06be08e1a1fad0a5f9e74
parent
60c0fd220df549103b69139f3c8b42efe2309477
author
Dami <dami.etoile@gmail.com>
3 files changed, +75 -1
Msrc/main.js2+-
Msrc/stage.js64++++++++++++++++++++++++++++++
Atests/stage-copy.test.js10+++++
M src/main.js +1 -1
@@ -63,7 +63,7 @@ const giftController = createGiftScene({
6363 },6464 onAction: result => { if (result.action === 'keep' || result.action === 'replace' || result.action === 'copy') { const installed=packService.installPack(result.pack,{origin:giftOrigin,...(result.install||{})}); if(installed.ok){ showModStatus(`Pack installed / ${installed.pack.name}`); const first = currentLevels().findIndex(level => level.packId === installed.pack.id); if (first >= 0) loadLevel(first); refreshIndex(currentLevels()[first]?.id); workshopController.refresh(); stage.show('index'); } else {workshopController.showErrors(installed.errors);stage.show('workshop');} } }6565 });66-const workshopController = createWorkshopScene({ service: packService, onBack: () => stage.closeWorkshop(), onImport: () => document.querySelector('#mod-input').click(), onPlay: packId => { const first=currentLevels().find(level=>level.packId===packId); refreshIndex(first?.id); stage.show('index'); }, onRemove: () => { refreshIndex(); if (!catalog.getLevel(catalog.activeLevelId)) loadLevel(0); }, onShare: (_id,url) => { navigator.clipboard?.writeText(url).then(() => showModStatus('LINK IS COPIED')).catch(() => showModStatus('Share URL ready / copy it manually',url)); }, onExport: (id,blob) => { const href=URL.createObjectURL(blob); const a=document.createElement('a');a.href=href;a.download=`${id}.cjs`;a.click();setTimeout(()=>URL.revokeObjectURL(href),0); }, copyToClipboard: text => navigator.clipboard?.writeText(text) });66+const workshopController = createWorkshopScene({ service: packService, onBack: () => stage.closeWorkshop(), onImport: () => document.querySelector('#mod-input').click(), onPlay: packId => { const first=currentLevels().find(level=>level.packId===packId); refreshIndex(first?.id); stage.show('index'); }, onRemove: () => { refreshIndex(); if (!catalog.getLevel(catalog.activeLevelId)) loadLevel(0); }, onShare: (_id,url) => { navigator.clipboard?.writeText(url).then(() => { stage.announce('LINK IS COPIED'); stage.showCopyConfirmation?.(); }).catch(() => showModStatus('Share URL ready / copy it manually',url)); }, onExport: (id,blob) => { const href=URL.createObjectURL(blob); const a=document.createElement('a');a.href=href;a.download=`${id}.cjs`;a.click();setTimeout(()=>URL.revokeObjectURL(href),0); }, copyToClipboard: text => navigator.clipboard?.writeText(text) });6767 stage.setControllers({ title: titleController, gift: giftController, workshop: workshopController });6868 let soundOn = !audio.isMuted();6969 soundButton.textContent = soundOn ? '◖' : '○';
M src/stage.js +64 -0
@@ -31,6 +31,12 @@ const SCENES = {
3131 },3232 };3333 34+export function copyConfirmationProgress(now, startedAt, duration = 900) {35+ if (!Number.isFinite(now) || !Number.isFinite(startedAt) || now < startedAt)36+ return 0;37+ return Math.max(0, Math.min(1, (now - startedAt) / duration));38+}39+3440 /** Small scene shell. Gameplay remains owned by main.js; this owns scene intent and overlays. */3541 export function createStage(options = {}) {3642 const canvas = document.createElement("canvas");
@@ -104,6 +110,7 @@ export function createStage(options = {}) {
104110 let winningNodeId = null;105111 let indexPopStart = 0;106112 let indexPopNodeId = null;113+ let copyFloatStart = 0;107114 const reducedMotion =108115 globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ??109116 false;
@@ -119,6 +126,60 @@ export function createStage(options = {}) {
119126 function announce(message) {120127 live.textContent = message;121128 }129+ function drawCopyConfirmation(ctx, width, height, now = performance.now()) {130+ if (!copyFloatStart) return;131+ const progress = copyConfirmationProgress(now, copyFloatStart);132+ const labels = ["LINK", "IS", "COPIED"];133+ const size = Math.min(16, Math.max(12, width * 0.035));134+ ctx.save();135+ ctx.globalAlpha = 1 - progress;136+ ctx.font = `800 ${size}px Recursive, monospace`;137+ const widths = labels.map((label) =>138+ Math.max(size * 1.6, ctx.measureText(label).width + size),139+ );140+ const gap = Math.max(5, size * 0.4);141+ const total =142+ widths.reduce((sum, value) => sum + value, 0) + gap * (labels.length - 1);143+ let x = Math.max(16, (width - total) / 2);144+ const y = Math.min(height - 82, height * 0.78) - progress * 52;145+ labels.forEach((label, index) => {146+ const tileWidth = widths[index];147+ ctx.save();148+ ctx.globalAlpha = 1 - progress;149+ ctx.fillStyle = "#000000";150+ ctx.fillRect(x + 3, y + 3, tileWidth, size * 1.5);151+ ctx.fillStyle = "#F2EEE6";152+ ctx.fillRect(x, y, tileWidth, size * 1.5);153+ ctx.fillStyle = "#0F0E0D";154+ ctx.textAlign = "center";155+ ctx.textBaseline = "middle";156+ ctx.font = `800 ${size}px Recursive, monospace`;157+ ctx.fillText(label, x + tileWidth / 2, y + size * 0.78);158+ ctx.restore();159+ x += widths[index] + gap;160+ });161+ ctx.restore();162+ if (progress >= 1) copyFloatStart = 0;163+ }164+ function showCopyConfirmation() {165+ copyFloatStart = performance.now();166+ if (reducedMotion) {167+ copyFloatStart = 0;168+ return api;169+ }170+ render();171+ requestAnimationFrame(showCopyConfirmationFrame);172+ return api;173+ }174+ function showCopyConfirmationFrame() {175+ if (!copyFloatStart) return;176+ if (performance.now() - copyFloatStart >= 900 || (scene !== "gift" && scene !== "workshop")) {177+ copyFloatStart = 0;178+ return;179+ }180+ render();181+ if (copyFloatStart) requestAnimationFrame(showCopyConfirmationFrame);182+ }122183 function drawTransition(now, generation) {123184 if (124185 !transitionStart ||
@@ -189,11 +250,13 @@ export function createStage(options = {}) {
189250 if (scene === "gift" && gift) {190251 gift.draw(ctx, performance.now(), { width: w, height: h });191252 canvas.setAttribute("aria-label", gift.accessibilityText?.() || "Miau Is True Gift");253+ drawCopyConfirmation(ctx, w, h);192254 return;193255 }194256 if (scene === "workshop" && workshop) {195257 workshop.draw(ctx, { width: w, height: h });196258 canvas.setAttribute("aria-label", workshop.accessibilityText?.() || "Miau Is True Workshop");259+ drawCopyConfirmation(ctx, w, h);197260 return;198261 }199262 if (scene === "proof" && boardState) {
@@ -646,6 +709,7 @@ export function createStage(options = {}) {
646709 setControllers,647710 setActions,648711 setPauseStatus,712+ showCopyConfirmation,649713 closeWorkshop,650714 closeGift,651715 announce,
A tests/stage-copy.test.js +10 -0