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
+8 -2
View File
@@ -39,8 +39,14 @@
<p id="empty">Nothing queued yet.</p>
<footer>
<span class="label">Background</span>
<div id="backgrounds"></div>
<div class="switcher">
<span class="label">Background</span>
<div id="backgrounds"></div>
</div>
<div class="switcher">
<span class="label">Animation</span>
<div id="animations"></div>
</div>
</footer>
<script type="module" src="/queue/queue.js"></script>
+11 -3
View File
@@ -241,13 +241,20 @@ footer {
border-top: 1px solid #331630;
}
#backgrounds {
footer .switcher + .switcher {
margin-top: 18px;
}
#backgrounds,
#animations {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
#backgrounds button {
#backgrounds button,
#animations button {
font: inherit;
padding: 8px 18px;
border-radius: 999px;
@@ -257,7 +264,8 @@ footer {
cursor: pointer;
}
#backgrounds button.active {
#backgrounds button.active,
#animations button.active {
background: var(--accent);
border-color: var(--accent);
color: var(--ink);
+43 -16
View File
@@ -1,7 +1,7 @@
import { connect, formatDuration } from '/shared/socket.js';
import { BACKGROUNDS } from '/config.js';
import { BACKGROUNDS, ANIMATIONS, AUTO_ANIMATION } from '/config.js';
let state = { items: [], currentId: null, background: 'pink' };
let state = { items: [], currentId: null, background: 'pink', animation: AUTO_ANIMATION };
// Last playhead report, kept with its arrival time so the countdown can be
// extrapolated smoothly between the 1 Hz updates.
let progress = { id: null, currentTime: 0, at: 0 };
@@ -11,6 +11,7 @@ const empty = document.getElementById('empty');
const errorBox = document.getElementById('error');
const statusBox = document.getElementById('status');
const backgroundBox = document.getElementById('backgrounds');
const animationBox = document.getElementById('animations');
const socket = connect({ onMessage, onStatus });
@@ -183,25 +184,50 @@ function clearDropMarkers() {
}
}
// --- background switcher ------------------------------------------------
// --- background / animation switchers -----------------------------------
const backgroundButtons = new Map();
/**
* A row of buttons that sets one field of the shared state. Returns a render
* function that lights up whichever key the server currently reports.
*/
function buildSwitcher(box, options, type, currentKey) {
const buttons = new Map();
for (const [key, { label }] of Object.entries(BACKGROUNDS)) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.addEventListener('click', () => socket.send({ type: 'setBackground', key }));
backgroundBox.appendChild(button);
backgroundButtons.set(key, button);
}
function renderBackground() {
for (const [key, button] of backgroundButtons) {
button.classList.toggle('active', key === state.background);
for (const [key, label] of options) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.addEventListener('click', () => socket.send({ type, key }));
box.appendChild(button);
buttons.set(key, button);
}
return function render() {
for (const [key, button] of buttons) {
button.classList.toggle('active', key === currentKey());
}
};
}
const renderBackground = buildSwitcher(
backgroundBox,
Object.entries(BACKGROUNDS).map(([key, { label }]) => [key, label]),
'setBackground',
() => state.background,
);
// Auto leads: it is the normal party setting, the named ones are for testing
// a single animation without waiting for the cycle to land on it.
const renderAnimation = buildSwitcher(
animationBox,
[
[AUTO_ANIMATION, 'Auto'],
...Object.entries(ANIMATIONS).map(([key, { label }]) => [key, label]),
],
'setAnimation',
() => state.animation,
);
// --- socket -------------------------------------------------------------
function onMessage(msg) {
@@ -209,6 +235,7 @@ function onMessage(msg) {
state = msg.state;
render();
renderBackground();
renderAnimation();
} else if (msg.type === 'progress') {
progress = { id: msg.id, currentTime: msg.currentTime, at: Date.now() };
renderTotals();