- commit
- edcb9d800afc84117ca06be08e1a1fad0a5f9e74
- parent
- 60c0fd220df549103b69139f3c8b42efe2309477
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 2 | +- |
| M | src/ | 64 | ++++++++++++++++++++++++++++++ |
| A | tests/ | 10 | +++++ |
@@ -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 ? '◖' : '○';@@ -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,@@ -0,0 +1,10 @@
1+import test from "node:test";2+import assert from "node:assert/strict";3+import { copyConfirmationProgress } from "../src/stage.js";4+5+test("copy confirmation rises through a bounded 900ms window", () => {6+ assert.equal(copyConfirmationProgress(1000, 1000), 0);7+ assert.equal(copyConfirmationProgress(1450, 1000), 0.5);8+ assert.equal(copyConfirmationProgress(1900, 1000), 1);9+ assert.equal(copyConfirmationProgress(2200, 1000), 1);10+});