Let the queue page pin the frame animation

Waiting for the shuffle bag to land on the animation you are working on makes
testing one of them tedious. The queue page now carries an Animation row next
to Background: Auto keeps the per-track cycle, any other button pins the player
to that animation until the mode changes again.

The animation names live in config.js alongside their labels, so the server can
validate them and the queue page can label its buttons; motion.js keys its
registry by the same names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
This commit is contained in:
simon
2026-08-09 22:12:52 +02:00
co-authored by Claude Opus 5
parent ce1a6d6a47
commit e031ebda1a
9 changed files with 159 additions and 47 deletions
+36 -14
View File
@@ -11,6 +11,7 @@ import {
BOUNCE_SPEED,
CORNER_INTERVAL_SECONDS,
CORNER_MARGIN,
AUTO_ANIMATION,
} from '/config.js';
const ASPECT = 9 / 16;
@@ -261,33 +262,54 @@ export function startCorners(context) {
// --- registry -----------------------------------------------------------
export const ANIMATIONS = [
{ name: 'bounce', start: startBounce },
{ name: 'corners', start: startCorners },
];
// Keyed by the names in config.js, which is what travels over the socket.
export const ANIMATIONS = {
bounce: startBounce,
corners: startCorners,
};
const NAMES = Object.keys(ANIMATIONS);
/**
* Run one animation at a time, swapping to a freshly picked one on next().
* Returns the handle the player drives from its track changes.
* Run one animation at a time. In auto mode next() swaps to a freshly picked
* one on every track change; pinned to a name (from the queue page, for
* testing) it stays put until the mode changes again.
*/
export function startAnimationCycle(context) {
const picker = createPicker(ANIMATIONS.length);
export function startAnimationCycle(context, mode = AUTO_ANIMATION) {
const picker = createPicker(NAMES.length);
let stop = null;
let current = null;
function next() {
function run(name) {
stop?.();
current = ANIMATIONS[picker.next()];
stop = current.start(context);
return current.name;
current = name;
stop = ANIMATIONS[name](context);
return current;
}
next();
function next() {
// Pinned: the track changed, but the operator asked for this one.
if (mode !== AUTO_ANIMATION) return current;
return run(NAMES[picker.next()]);
}
function setMode(value) {
mode = ANIMATIONS[value] ? value : AUTO_ANIMATION;
// Always restarts, so switching back to auto takes effect immediately
// rather than waiting out the current track.
return mode === AUTO_ANIMATION ? run(NAMES[picker.next()]) : run(mode);
}
setMode(mode);
return {
next,
setMode,
get mode() {
return mode;
},
get name() {
return current?.name ?? null;
return current;
},
};
}
+5 -3
View File
@@ -1,12 +1,12 @@
import { connect } from '/shared/socket.js';
import { OVERLAP_SECONDS, HARD_CUT_SECONDS, BACKGROUNDS } from '/config.js';
import { OVERLAP_SECONDS, HARD_CUT_SECONDS, BACKGROUNDS, AUTO_ANIMATION } from '/config.js';
import { createPlayer, loadApi, STATE } from './yt.js';
import { startAnimationCycle } from './motion.js';
import { requestCapture } from './capture.js';
const TICK_MS = 250;
let state = { items: [], currentId: null, background: 'pink' };
let state = { items: [], currentId: null, background: 'pink', animation: AUTO_ANIMATION };
let decks = [];
let active = 0;
let transition = null; // { until, duration, from, to } while crossfading
@@ -74,7 +74,7 @@ async function start() {
animationContext.capture = capture;
});
animations = startAnimationCycle(animationContext);
animations = startAnimationCycle(animationContext, state.animation);
decks = await Promise.all([
makeDeck('deck-a', 0),
@@ -325,8 +325,10 @@ initProbe();
function onMessage(msg) {
if (msg.type !== 'state') return;
const previousAnimation = state.animation;
state = msg.state;
applyBackground(state.background);
if (state.animation !== previousAnimation) animations?.setMode(state.animation);
updateGate();
pumpProbe();
}