This commit is contained in:
parent
ac33e2fec6
commit
35a4dc2dbf
9 changed files with 106 additions and 23 deletions
|
|
@ -82,6 +82,7 @@ html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 1.4em;
|
min-height: 1.4em;
|
||||||
max-width: 32ch;
|
max-width: 32ch;
|
||||||
|
white-space: pre-line;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
font-size: clamp(0.95rem, 4vw, 1.1rem);
|
font-size: clamp(0.95rem, 4vw, 1.1rem);
|
||||||
|
|
|
||||||
14
js/audio.js
14
js/audio.js
|
|
@ -45,7 +45,14 @@ export function unlockAudio() {
|
||||||
const d = noiseBuf.getChannelData(0);
|
const d = noiseBuf.getChannelData(0);
|
||||||
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
||||||
}
|
}
|
||||||
if (ctx.state !== 'running') ctx.resume().catch(() => {});
|
if (ctx.state !== 'running') {
|
||||||
|
ctx.resume().catch(() => {});
|
||||||
|
// Older iOS Safari only unlocks once a sound starts inside the gesture.
|
||||||
|
const src = ctx.createBufferSource();
|
||||||
|
src.buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
|
||||||
|
src.connect(ctx.destination);
|
||||||
|
src.start();
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* audio unavailable */
|
/* audio unavailable */
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +63,10 @@ export function installAudioUnlock() {
|
||||||
for (const t of ['pointerdown', 'pointerup', 'touchend', 'click', 'keydown']) window.addEventListener(t, unlockAudio, opts);
|
for (const t of ['pointerdown', 'pointerup', 'touchend', 'click', 'keydown']) window.addEventListener(t, unlockAudio, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const live = () => ctx && ctx.state === 'running' && !muted;
|
/** False until the browser has let us start audio (needs a tap, not just a touch down). */
|
||||||
|
export const audioUnlocked = () => !!ctx && ctx.state === 'running';
|
||||||
|
|
||||||
|
const live = () => audioUnlocked() && !muted;
|
||||||
|
|
||||||
function tone({ type = 'sine', freq, freqEnd, at = 0, dur, gain = 0.2, attack = 0.005, filter }) {
|
function tone({ type = 'sine', freq, freqEnd, at = 0, dur, gain = 0.2, attack = 0.005, filter }) {
|
||||||
const t0 = ctx.currentTime + at;
|
const t0 = ctx.currentTime + at;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export const translations = {
|
||||||
lastStanding: 'Last one standing!',
|
lastStanding: 'Last one standing!',
|
||||||
tapRestart: 'Tap to play again',
|
tapRestart: 'Tap to play again',
|
||||||
desktopHint: 'On a computer? Add ?debug=1 to the address to test with a mouse.',
|
desktopHint: 'On a computer? Add ?debug=1 to the address to test with a mouse.',
|
||||||
|
tapForSound: '🔈 Tap the screen once to turn on sound',
|
||||||
team: 'Team {name}',
|
team: 'Team {name}',
|
||||||
|
|
||||||
modeLabel: 'Mode',
|
modeLabel: 'Mode',
|
||||||
|
|
@ -90,6 +91,7 @@ export const translations = {
|
||||||
lastStanding: 'Laatste overlevende!',
|
lastStanding: 'Laatste overlevende!',
|
||||||
tapRestart: 'Tik om opnieuw te spelen',
|
tapRestart: 'Tik om opnieuw te spelen',
|
||||||
desktopHint: 'Op een computer? Zet ?debug=1 achter het adres om met de muis te testen.',
|
desktopHint: 'Op een computer? Zet ?debug=1 achter het adres om met de muis te testen.',
|
||||||
|
tapForSound: '🔈 Tik één keer op het scherm voor geluid',
|
||||||
team: 'Team {name}',
|
team: 'Team {name}',
|
||||||
|
|
||||||
modeLabel: 'Modus',
|
modeLabel: 'Modus',
|
||||||
|
|
|
||||||
21
js/input.js
21
js/input.js
|
|
@ -1,5 +1,7 @@
|
||||||
// Pointer input. Fingers are tracked by pointerId; pointercancel counts as
|
// Pointer input. Fingers are tracked by pointerId; pointercancel counts as
|
||||||
// a lift (Android cancels touches it thinks are palms or system gestures).
|
// a lift (Android cancels touches it thinks are palms or system gestures).
|
||||||
|
// Browsers may recycle pointerIds, while the game keeps lifted fingers on
|
||||||
|
// screen after a round, so every touch gets its own never-reused finger id.
|
||||||
//
|
//
|
||||||
// Debug mode (?debug=1): mouse clicks place persistent fake fingers.
|
// Debug mode (?debug=1): mouse clicks place persistent fake fingers.
|
||||||
// Click a fake finger again to remove it; drag it to move it.
|
// Click a fake finger again to remove it; drag it to move it.
|
||||||
|
|
@ -8,7 +10,8 @@
|
||||||
const DRAG_THRESHOLD = 6;
|
const DRAG_THRESHOLD = 6;
|
||||||
|
|
||||||
export function createInput(canvas, { debug, onDown, onMove, onUp }) {
|
export function createInput(canvas, { debug, onDown, onMove, onUp }) {
|
||||||
const active = new Set(); // real pointer ids currently down
|
const active = new Map(); // pointerId -> finger id, for touches currently down
|
||||||
|
let touchSeq = 0;
|
||||||
const fakes = new Map(); // fake id -> {x, y}
|
const fakes = new Map(); // fake id -> {x, y}
|
||||||
let fakeSeq = 0;
|
let fakeSeq = 0;
|
||||||
let mouse = null; // {pointerId, fakeId, startX, startY, moved, created}
|
let mouse = null; // {pointerId, fakeId, startX, startY, moved, created}
|
||||||
|
|
@ -17,8 +20,10 @@ export function createInput(canvas, { debug, onDown, onMove, onUp }) {
|
||||||
const hitRadius = () => Math.max(40, Math.min(64, Math.min(innerWidth, innerHeight) * 0.115));
|
const hitRadius = () => Math.max(40, Math.min(64, Math.min(innerWidth, innerHeight) * 0.115));
|
||||||
|
|
||||||
function release(pointerId) {
|
function release(pointerId) {
|
||||||
if (!active.delete(pointerId)) return;
|
const id = active.get(pointerId);
|
||||||
onUp(String(pointerId), now());
|
if (id == null) return;
|
||||||
|
active.delete(pointerId);
|
||||||
|
onUp(id, now());
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener('pointerdown', (e) => {
|
canvas.addEventListener('pointerdown', (e) => {
|
||||||
|
|
@ -32,14 +37,16 @@ export function createInput(canvas, { debug, onDown, onMove, onUp }) {
|
||||||
} catch {
|
} catch {
|
||||||
/* not fatal */
|
/* not fatal */
|
||||||
}
|
}
|
||||||
active.add(e.pointerId);
|
release(e.pointerId); // a recycled id whose lift we never saw
|
||||||
onDown(String(e.pointerId), e.clientX, e.clientY, now());
|
const id = `t${++touchSeq}`;
|
||||||
|
active.set(e.pointerId, id);
|
||||||
|
onDown(id, e.clientX, e.clientY, now());
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointermove', (e) => {
|
canvas.addEventListener('pointermove', (e) => {
|
||||||
if (mouse && e.pointerId === mouse.pointerId) return debugMove(e);
|
if (mouse && e.pointerId === mouse.pointerId) return debugMove(e);
|
||||||
if (!active.has(e.pointerId)) return;
|
const id = active.get(e.pointerId);
|
||||||
onMove(String(e.pointerId), e.clientX, e.clientY);
|
if (id != null) onMove(id, e.clientX, e.clientY);
|
||||||
});
|
});
|
||||||
|
|
||||||
const up = (e) => {
|
const up = (e) => {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export class Renderer {
|
||||||
data.style === 'teams'
|
data.style === 'teams'
|
||||||
? game.players.map((id) => game.rings.get(id)).filter(Boolean).map((ring) => ({ ring, color: this.theme.team[game.plan.teams.get(ring.id)] }))
|
? game.players.map((id) => game.rings.get(id)).filter(Boolean).map((ring) => ({ ring, color: this.theme.team[game.plan.teams.get(ring.id)] }))
|
||||||
: data.winners.map((id) => game.rings.get(id)).filter(Boolean).map((ring) => ({ ring, color: this.colorOf(ring) }));
|
: data.winners.map((id) => game.rings.get(id)).filter(Boolean).map((ring) => ({ ring, color: this.colorOf(ring) }));
|
||||||
this.reveal = createReveal(data.style, { targets, w: this.w, h: this.h, R, start: now, palette: this.theme.colors });
|
this.reveal = createReveal(data.style, { targets, view: this, start: now, palette: this.theme.colors });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'phase':
|
case 'phase':
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,20 @@ function farthestCorner(x, y, w, h) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} style flood | confetti | spotlight | shockwave | calm | teams
|
* @param {string} style flood | confetti | spotlight | shockwave | calm | teams
|
||||||
* @param {{targets:{ring:{x:number,y:number}, color:string}[], w:number, h:number, R:number, start:number, palette:string[]}} o
|
* @param {{targets:{ring:{x:number,y:number}, color:string}[], view:{w:number,h:number,R:number}, start:number, palette:string[]}} o
|
||||||
|
* `view` is read every frame so the effect follows resizes (e.g. entering fullscreen).
|
||||||
*/
|
*/
|
||||||
export function createReveal(style, o) {
|
export function createReveal(style, o) {
|
||||||
const make = { flood, confetti, spotlight, shockwave, calm, teams }[style] || calm;
|
const make = { flood, confetti, spotlight, shockwave, calm, teams }[style] || calm;
|
||||||
return { winnerWhite: false, drawBack() {}, drawFront() {}, shake: null, ...make(o) };
|
return { winnerWhite: false, drawBack() {}, drawFront() {}, shake: null, ...make(o) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function flood({ targets, w, h, start }) {
|
function flood({ targets, view, start }) {
|
||||||
const single = targets.length === 1;
|
const single = targets.length === 1;
|
||||||
return {
|
return {
|
||||||
winnerWhite: true,
|
winnerWhite: true,
|
||||||
drawBack(g, now) {
|
drawBack(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const k = easeInOutCubic(clamp01((now - start) / 850));
|
const k = easeInOutCubic(clamp01((now - start) / 850));
|
||||||
g.globalAlpha = single ? 1 : 0.8;
|
g.globalAlpha = single ? 1 : 0.8;
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
|
|
@ -51,7 +53,7 @@ function flood({ targets, w, h, start }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function confetti({ targets, w, h, R, start, palette }) {
|
function confetti({ targets, view, start, palette }) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const per = Math.round(170 / targets.length);
|
const per = Math.round(170 / targets.length);
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
|
|
@ -82,6 +84,7 @@ function confetti({ targets, w, h, R, start, palette }) {
|
||||||
const grav = 1100;
|
const grav = 1100;
|
||||||
return {
|
return {
|
||||||
drawBack(g, now) {
|
drawBack(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const k = clamp01((now - start) / 700);
|
const k = clamp01((now - start) / 700);
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
const grad = g.createRadialGradient(ring.x, ring.y, R, ring.x, ring.y, Math.max(w, h));
|
const grad = g.createRadialGradient(ring.x, ring.y, R, ring.x, ring.y, Math.max(w, h));
|
||||||
|
|
@ -92,6 +95,7 @@ function confetti({ targets, w, h, R, start, palette }) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drawFront(g, now) {
|
drawFront(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const T = (now - start) / 1000;
|
const T = (now - start) / 1000;
|
||||||
for (const p of parts) {
|
for (const p of parts) {
|
||||||
const t = T - p.delay;
|
const t = T - p.delay;
|
||||||
|
|
@ -118,16 +122,15 @@ function confetti({ targets, w, h, R, start, palette }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function spotlight({ targets, w, h, R, start }) {
|
function spotlight({ targets, view, start }) {
|
||||||
// Darkness is drawn at half resolution: it's all soft gradients anyway.
|
// Darkness is drawn at half resolution: it's all soft gradients anyway.
|
||||||
const scale = 0.5;
|
const scale = 0.5;
|
||||||
const off = document.createElement('canvas');
|
const off = document.createElement('canvas');
|
||||||
off.width = Math.ceil(w * scale);
|
|
||||||
off.height = Math.ceil(h * scale);
|
|
||||||
const o = off.getContext('2d');
|
const o = off.getContext('2d');
|
||||||
const spots = targets.map(() => ({ sx: Math.random() * w, sy: Math.random() * h * 0.6, ph: Math.random() * 6 }));
|
const spots = targets.map(() => ({ sx: Math.random() * view.w, sy: Math.random() * view.h * 0.6, ph: Math.random() * 6 }));
|
||||||
return {
|
return {
|
||||||
drawBack(g, now) {
|
drawBack(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const k = clamp01((now - start - 900) / 500);
|
const k = clamp01((now - start - 900) / 500);
|
||||||
if (!k) return;
|
if (!k) return;
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
|
|
@ -139,7 +142,14 @@ function spotlight({ targets, w, h, R, start }) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drawFront(g, now) {
|
drawFront(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const t = (now - start) / 1000;
|
const t = (now - start) / 1000;
|
||||||
|
const ow = Math.ceil(w * scale);
|
||||||
|
const oh = Math.ceil(h * scale);
|
||||||
|
if (off.width !== ow || off.height !== oh) {
|
||||||
|
off.width = ow;
|
||||||
|
off.height = oh;
|
||||||
|
}
|
||||||
o.setTransform(scale, 0, 0, scale, 0, 0);
|
o.setTransform(scale, 0, 0, scale, 0, 0);
|
||||||
o.globalCompositeOperation = 'source-over';
|
o.globalCompositeOperation = 'source-over';
|
||||||
o.clearRect(0, 0, w, h);
|
o.clearRect(0, 0, w, h);
|
||||||
|
|
@ -164,7 +174,7 @@ function spotlight({ targets, w, h, R, start }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function shockwave({ targets, w, h, R, start }) {
|
function shockwave({ targets, view, start }) {
|
||||||
const delays = [0, 0.14, 0.3];
|
const delays = [0, 0.14, 0.3];
|
||||||
return {
|
return {
|
||||||
shake(now) {
|
shake(now) {
|
||||||
|
|
@ -174,6 +184,7 @@ function shockwave({ targets, w, h, R, start }) {
|
||||||
return [Math.sin(now * 0.09) * amp, Math.cos(now * 0.113) * amp];
|
return [Math.sin(now * 0.09) * amp, Math.cos(now * 0.113) * amp];
|
||||||
},
|
},
|
||||||
drawBack(g, now) {
|
drawBack(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const t = (now - start) / 1000;
|
const t = (now - start) / 1000;
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
g.fillStyle = hexToRgba(color, 0.25 * clamp01((t - 0.2) / 0.6));
|
g.fillStyle = hexToRgba(color, 0.25 * clamp01((t - 0.2) / 0.6));
|
||||||
|
|
@ -197,6 +208,7 @@ function shockwave({ targets, w, h, R, start }) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drawFront(g, now) {
|
drawFront(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const t = (now - start) / 1000;
|
const t = (now - start) / 1000;
|
||||||
if (t < 0.15) {
|
if (t < 0.15) {
|
||||||
g.fillStyle = `rgba(255,255,255,${0.55 * (1 - t / 0.15)})`;
|
g.fillStyle = `rgba(255,255,255,${0.55 * (1 - t / 0.15)})`;
|
||||||
|
|
@ -207,9 +219,10 @@ function shockwave({ targets, w, h, R, start }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** prefers-reduced-motion: a slow, gentle colour fade. */
|
/** prefers-reduced-motion: a slow, gentle colour fade. */
|
||||||
function calm({ targets, w, h, R, start }) {
|
function calm({ targets, view, start }) {
|
||||||
return {
|
return {
|
||||||
drawBack(g, now) {
|
drawBack(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const k = easeInOutCubic(clamp01((now - start) / 1200));
|
const k = easeInOutCubic(clamp01((now - start) / 1200));
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
const grad = g.createRadialGradient(ring.x, ring.y, R, ring.x, ring.y, Math.max(w, h) * 0.9);
|
const grad = g.createRadialGradient(ring.x, ring.y, R, ring.x, ring.y, Math.max(w, h) * 0.9);
|
||||||
|
|
@ -222,9 +235,10 @@ function calm({ targets, w, h, R, start }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function teams({ targets, R, start }) {
|
function teams({ targets, view, start }) {
|
||||||
return {
|
return {
|
||||||
drawBack(g, now) {
|
drawBack(g, now) {
|
||||||
|
const { w, h, R } = view;
|
||||||
const k = easeOutCubic(clamp01((now - start) / 700));
|
const k = easeOutCubic(clamp01((now - start) / 700));
|
||||||
for (const { ring, color } of targets) {
|
for (const { ring, color } of targets) {
|
||||||
const r = R * (1.5 + 2.5 * k);
|
const r = R * (1.5 + 2.5 * k);
|
||||||
|
|
|
||||||
10
js/ui.js
10
js/ui.js
|
|
@ -4,7 +4,7 @@ import { t, setLang, getLang, detectLang, LANGS } from './i18n.js';
|
||||||
import { MODES } from './modes.js';
|
import { MODES } from './modes.js';
|
||||||
import { TIMING } from './game.js';
|
import { TIMING } from './game.js';
|
||||||
import { pick } from './random.js';
|
import { pick } from './random.js';
|
||||||
import { canVibrate } from './audio.js';
|
import { canVibrate, audioUnlocked } from './audio.js';
|
||||||
import { fullscreenSupported, isFullscreen, toggleFullscreen, onFullscreenChange } from './device.js';
|
import { fullscreenSupported, isFullscreen, toggleFullscreen, onFullscreenChange } from './device.js';
|
||||||
|
|
||||||
const $ = (sel) => document.querySelector(sel);
|
const $ = (sel) => document.querySelector(sel);
|
||||||
|
|
@ -207,7 +207,13 @@ export function createUI({ settings, debug, showDesktopHint }) {
|
||||||
const count = game.activeRings().length;
|
const count = game.activeRings().length;
|
||||||
switch (game.phase) {
|
switch (game.phase) {
|
||||||
case 'idle':
|
case 'idle':
|
||||||
if (count === 0) return [t('hint'), showDesktopHint ? t('desktopHint') : modeSummary(s), 'hint'];
|
if (count === 0) {
|
||||||
|
if (showDesktopHint) return [t('hint'), t('desktopHint'), 'hint'];
|
||||||
|
// Browsers only allow sound after a tap (a finger *lift*), so ask for one.
|
||||||
|
const soundHint = !s.muted && !audioUnlocked() ? `
|
||||||
|
${t('tapForSound')}` : '';
|
||||||
|
return [t('hint'), modeSummary(s) + soundHint, 'hint'];
|
||||||
|
}
|
||||||
return [t('needMore'), modeSummary(s), 'hint'];
|
return [t('needMore'), modeSummary(s), 'hint'];
|
||||||
case 'gathering':
|
case 'gathering':
|
||||||
return [t('holdStill'), '', 'soft'];
|
return [t('holdStill'), '', 'soft'];
|
||||||
|
|
|
||||||
2
sw.js
2
sw.js
|
|
@ -2,7 +2,7 @@
|
||||||
// cache fallback so updates show up immediately when online and the app
|
// cache fallback so updates show up immediately when online and the app
|
||||||
// still works fully offline. Bump VERSION when the file list changes.
|
// still works fully offline. Bump VERSION when the file list changes.
|
||||||
|
|
||||||
const VERSION = 'picker-v1';
|
const VERSION = 'picker-v2';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'./',
|
'./',
|
||||||
'./index.html',
|
'./index.html',
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
import { Game, TIMING } from '../js/game.js';
|
import { Game, TIMING } from '../js/game.js';
|
||||||
import { planRound, makeHighlightSchedule } from '../js/modes.js';
|
import { planRound, makeHighlightSchedule } from '../js/modes.js';
|
||||||
import { randomInt } from '../js/random.js';
|
import { randomInt } from '../js/random.js';
|
||||||
|
import { createInput } from '../js/input.js';
|
||||||
|
import { createReveal } from '../js/reveals.js';
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
function test(name, fn) {
|
function test(name, fn) {
|
||||||
|
|
@ -270,6 +272,47 @@ test('unknown pointer ids and double lifts are harmless', () => {
|
||||||
assert(h.game.rings.size === 0, 'pruned');
|
assert(h.game.rings.size === 0, 'pruned');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('recycled pointerIds still restart after a round (input -> game)', () => {
|
||||||
|
const h = setup();
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
document.body.append(canvas);
|
||||||
|
createInput(canvas, {
|
||||||
|
debug: false,
|
||||||
|
onDown: (id, x, y) => h.game.fingerDown(id, x, y, h.now),
|
||||||
|
onMove: (id, x, y) => h.game.fingerMove(id, x, y),
|
||||||
|
onUp: (id) => h.game.fingerUp(id, h.now),
|
||||||
|
});
|
||||||
|
const fire = (type, pointerId, x = 50, y = 50) =>
|
||||||
|
canvas.dispatchEvent(new PointerEvent(type, { pointerId, pointerType: 'touch', clientX: x, clientY: y, bubbles: true, cancelable: true }));
|
||||||
|
fire('pointerdown', 1, 50, 50);
|
||||||
|
fire('pointerdown', 2, 250, 250);
|
||||||
|
h.until('result');
|
||||||
|
fire('pointerup', 1);
|
||||||
|
fire('pointerup', 2);
|
||||||
|
h.run(TIMING.restartGraceMs + 100);
|
||||||
|
assert(h.game.canRestart(h.now), 'can restart');
|
||||||
|
fire('pointerdown', 1); // the browser reuses pointerId 1 for the tap
|
||||||
|
assert(h.game.phase === 'idle' && h.count('restart') === 1, 'tap with a recycled pointerId restarts, phase ' + h.game.phase);
|
||||||
|
fire('pointerup', 1);
|
||||||
|
h.run(500);
|
||||||
|
assert(h.game.rings.size === 0, 'tap ring removed');
|
||||||
|
canvas.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reveal follows a resize (entering fullscreen) without leaving an unpainted strip', () => {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 100;
|
||||||
|
c.height = 200;
|
||||||
|
const g = c.getContext('2d');
|
||||||
|
const view = { w: 100, h: 100, R: 10 };
|
||||||
|
const ring = { x: 50, y: 50 };
|
||||||
|
const fx = createReveal('flood', { targets: [{ ring, color: '#ff0000' }], view, start: 0, palette: ['#ff0000'] });
|
||||||
|
view.h = 200; // screen grew after the reveal started
|
||||||
|
fx.drawBack(g, 5000);
|
||||||
|
const px = g.getImageData(50, 190, 1, 1).data;
|
||||||
|
assert(px[0] > 100 && px[3] === 255, 'bottom painted, got ' + Array.from(px));
|
||||||
|
});
|
||||||
|
|
||||||
// Service worker: parses, and every precached file exists.
|
// Service worker: parses, and every precached file exists.
|
||||||
try {
|
try {
|
||||||
const src = await (await fetch('../sw.js')).text();
|
const src = await (await fetch('../sw.js')).text();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue