source: Klonkt/src/assets/js/mod/tape.js@ c0e463b

main
Last change on this file since c0e463b was c0e463b, checked in by Robin <roboburr@…>, 3 weeks ago

Speler-UI in bandmodus: spoelknoppen, eerlijke teller, lijst zonder keuze (shaer-tmyn)

Speelt er een bandje, dan hoort de speler onderin zich als een cassettedeck te
gedragen en niet als een playlist-speler. Drie van de vier stappen uit de bead;
de vierde (seek over de hele tijdlijn) staat er los van, want die raakt
loadTrack en de keten.

  • De tracklijst blijft staan maar is geen keuzelijst: renderQueue hangt in bandmodus geen klik meer op de items, en een CSS-klasse haalt de belofte weg dat het kan. Op een cassette spring je niet naar nummer zeven.
  • Vorige/volgende worden terugspoelen/vooruitspoelen: ander teken, ander woord, en vasthouden spoelt door. Een korte klik spoelt vijf seconden, voor toetsenbord en schermlezer.
  • De spoellus is VERHUISD naar de speler. Hij stond in mod/tape.js, en met spoelknoppen op twee plekken zouden twee kopieen van dezelfde versnelling uit elkaar lopen zodra iemand aan een getal draait. De cassette in de post bedient nu dezelfde lus en tekent alleen nog het jasje.

En de teller is eerlijk geworden. Die kwam uit chain[last].end, dus uit de
lengte van wat toevallig gebufferd was: 2:05 met een nummer geladen, 4:07 met
drie, terwijl de band 6:13 duurt. Erger nog, sprong je naar nummer drie dan
begon de keten daar opnieuw op nul en stond de teller weer vooraan. Nu telt hij
bandOffset(nummer) + positie in dat nummer, uit de trackduren -- die daarvoor
in de wachtrij-JSON moesten. Ontbreekt er een duur, dan valt hij terug: een som
met gaten is een verzonnen getal.

Nagemeten op dev: totaal 6:13 meteen bij de eerste seconde (klopt met de
PT373S op het AP-object), vier knoppen op Terug-/Vooruitspoelen, drie items in
de lijst met pointer-events none en het lopende nummer gemarkeerd, en spoelen
via de spelerknop loopt door de band heen.

audio-player v38, MOD_V 55, style.css v108.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 7.7 KB
Line 
1/**
2 * Het bandje: een cassette die draait, en alleen vooruit en achteruit speelt.
3 *
4 * WAT DEZE MODULE WEL EN NIET DOET. Hij speelt niets af. De site heeft al een
5 * speler (audio-player.js) met een wachtrij, een mini-speler onderin en een
6 * sessie die een paginawissel overleeft. Een tweede speler ernaast bouwen om
7 * een bandgevoel na te doen zou twee dingen tegelijk laten klinken en die hele
8 * sessie weggooien. Dus: de afspeelknop draagt dezelfde data-attributen als een
9 * albumhoes en wordt door die speler opgepakt.
10 *
11 * SPOELEN GAAT IN SECONDEN, over de nummergrenzen heen. Dat kan omdat die
12 * speler een wachtrij in EEN doorlopende MediaSource-tijdlijn giet: "een
13 * trackwissel is een positie, geen omschakeling". Een bandje is dus letterlijk
14 * die tijdlijn. Vandaar seekBy() en niet next()/prev() -- springen per nummer
15 * is een playlistgebaar, en dan is dit een lijst met een cassetteplaatje.
16 *
17 * DE SPOELLUS STAAT IN DE SPELER, niet hier: sinds de speler onderin dezelfde
18 * spoelknoppen heeft zou dezelfde versnelling op twee plekken staan. Deze
19 * module bedient hem en tekent het jasje.
20 *
21 * WAAROM ER GEPOLLD WORDT. audio-player.js zendt geen gebeurtenissen uit -- het
22 * is een gesloten module met een klein oppervlak (setQueue, play, pause, next,
23 * prev, isPlaying, currentTrack). Liever hier vijf keer per seconde kijken dan
24 * daar een gebeurtenissenlaag inbouwen die de rest van de site niet vraagt. Het
25 * polsen stopt zodra er geen bandje op de pagina staat.
26 */
27
28const TIK_MS = 200;
29let tikker = null;
30let gebonden = null; // de container waarop de klikafhandelaar hangt
31
32/** De speler, of niets als hij (nog) niet geladen is. */
33const speler = () => window.pcmsAudioPlayer || null;
34
35/** De url's die op dit bandje staan, in volgorde. */
36function bandUrls(tape) {
37 try {
38 return JSON.parse(tape.dataset.pcmsAlbum || '[]').map((t) => t.url);
39 } catch { return []; }
40}
41
42/**
43 * Speelt DIT bandje op dit moment? Vergelijken op url en niet op index: de
44 * wachtrij kan ondertussen van een ander blok komen, en dan hoort deze cassette
45 * gewoon stil te staan.
46 */
47function huidigeIndex(tape) {
48 const p = speler();
49 if (!p) return -1;
50 const nu = p.currentTrack();
51 if (!nu || !nu.url) return -1;
52 return bandUrls(tape).indexOf(nu.url);
53}
54
55function tekenBand(tape) {
56 const i = huidigeIndex(tape);
57 const p = speler();
58 const draait = i >= 0 && !!p && p.isPlaying();
59
60 tape.classList.toggle('is-playing', draait);
61 tape.classList.toggle('is-loaded', i >= 0);
62 // Spoelt de SPELER (bijvoorbeeld via zijn eigen knoppen onderin), dan hoort
63 // deze cassette dat ook te laten zien.
64 const windt = i >= 0 && !!p && p.isWinding && p.isWinding();
65 if (!windt) tape.classList.remove('is-winding', 'is-winding-back');
66 else tape.classList.add('is-winding');
67
68 const knop = tape.querySelector('[data-tape-play]');
69 if (knop) {
70 knop.classList.toggle('is-pauze', draait);
71 knop.setAttribute('aria-label', draait ? 'Pauzeren' : 'Afspelen');
72 }
73
74 // Welk nummer loopt er. Bij een bandje is dat de enige plek waar je het leest,
75 // want de lijst is met opzet geen knoppenrij.
76 const items = [...tape.querySelectorAll('.tape-track')];
77 items.forEach((li, n) => li.classList.toggle('is-current', n === i));
78 const nu = tape.querySelector('[data-tape-now]');
79 if (nu) {
80 const titel = i >= 0 && items[i] ? items[i].querySelector('.tape-track-title').textContent : '';
81 nu.textContent = i >= 0 ? `${i + 1}. ${titel}` : '';
82 }
83}
84
85function tekenAlles() {
86 for (const tape of document.querySelectorAll('.post-tape')) tekenBand(tape);
87}
88
89/**
90 * SPOELEN IS VASTHOUDEN, en dat is het verschil met een afspeellijst.
91 *
92 * Eerst deed vooruit en achteruit hier next()/prev(): een nummer verder. Dat is
93 * een playlistgebaar. Op een cassette bestaat "volgend nummer" niet -- je houdt
94 * de knop ingedrukt, de band loopt door, en je laat los waar je bent. Robin
95 * wees daarop (21-8) en hij heeft gelijk: met knoppen die per nummer springen
96 * is het een lijst met een cassetteplaatje erboven.
97 *
98 * Het versnelt terwijl je hem vasthoudt, zoals een echt bandje op gang komt.
99 * Een korte TIK spoelt een klein stukje -- dat is er voor toetsenbord en
100 * schermlezer, want vasthouden is met een spatiebalk geen gebaar.
101 */
102const TIK_STAP_S = 5; // een losse klik
103
104function stopWinden(tape) {
105 const p = speler();
106 if (p && p.stopWind) p.stopWind();
107 if (tape) tape.classList.remove('is-winding', 'is-winding-back');
108}
109
110function startWinden(tape, richting) {
111 const p = speler();
112 if (!p || !p.startWind || huidigeIndex(tape) < 0) return;
113 // DE LUS STAAT IN DE SPELER. Hij stond hier, en toen kreeg de speler zelf ook
114 // spoelknoppen -- twee kopieen van dezelfde versnelling die uit elkaar lopen
115 // zodra iemand aan een getal draait. Deze module zet alleen nog het jasje:
116 // welke spoelen draaien en hoe hard.
117 p.startWind(richting);
118 tape.classList.add('is-winding');
119 if (richting < 0) tape.classList.add('is-winding-back');
120}
121
122function opPointerDown(e) {
123 const knop = e.target.closest && e.target.closest('[data-tape-go]');
124 if (!knop) return;
125 const tape = knop.closest('.post-tape');
126 if (!tape) return;
127 startWinden(tape, knop.dataset.tapeGo === 'fwd' ? 1 : -1);
128}
129
130function opPointerUp() {
131 if (winder) stopWinden(winder.tape);
132}
133
134function opKlik(e) {
135 const tape = e.target.closest && e.target.closest('.post-tape');
136 if (!tape) return;
137 const p = speler();
138
139 const spoel = e.target.closest('[data-tape-go]');
140 if (spoel) {
141 // Een tik spoelt een stukje. Vasthouden gaat via de pointer-afhandelaars
142 // hierboven; die hebben dan al gespoeld en deze klik doet er nog een klein
143 // beetje bovenop, wat niet stoort.
144 if (!p || huidigeIndex(tape) < 0) return;
145 p.seekBy(spoel.dataset.tapeGo === 'fwd' ? TIK_STAP_S : -TIK_STAP_S);
146 setTimeout(() => tekenBand(tape), 60);
147 return;
148 }
149
150 const play = e.target.closest('[data-tape-play]');
151 if (play && p) {
152 // Loopt dit bandje al, dan is deze knop een pauzeknop. De globale
153 // afhandelaar van audio-player.js zou hem anders opnieuw vanaf het begin
154 // starten, want die kent alleen "speel deze wachtrij".
155 const i = huidigeIndex(tape);
156 if (i >= 0) {
157 e.preventDefault();
158 e.stopPropagation();
159 if (p.isPlaying()) p.pause(); else p.play();
160 setTimeout(tekenAlles, 60);
161 }
162 // Staat het bandje stil, dan laten we de klik doorlopen: de speler pakt de
163 // data-attributen op en zet de band op.
164 }
165}
166
167export function init() {
168 const tapes = document.querySelectorAll('.post-tape');
169
170 // Altijd eerst opruimen, ook als er niets staat. Zonder dat blijft de tikker
171 // lopen na een paginawissel naar een pagina zonder bandje -- dezelfde fout
172 // die in de leesweergave een keer de hele site heeft vastgezet.
173 if (tikker) { clearInterval(tikker); tikker = null; }
174 stopWinden(null);
175 if (gebonden) {
176 gebonden.removeEventListener('click', opKlik, true);
177 gebonden.removeEventListener('pointerdown', opPointerDown, true);
178 gebonden.removeEventListener('pointerup', opPointerUp, true);
179 gebonden.removeEventListener('pointercancel', opPointerUp, true);
180 gebonden = null;
181 }
182 if (!tapes.length) return;
183
184 // Vangen in de CAPTURE-fase, want de speler luistert zelf op document in de
185 // bubbelfase. Alleen zo kan de pauzeknop zijn klik tegenhouden voordat de
186 // wachtrij opnieuw wordt gezet.
187 gebonden = document;
188 gebonden.addEventListener('click', opKlik, true);
189 gebonden.addEventListener('pointerdown', opPointerDown, true);
190 // Loslaten telt ook als je BUITEN de knop loslaat, anders blijft de band
191 // doorspoelen als je met je vinger wegglijdt.
192 gebonden.addEventListener('pointerup', opPointerUp, true);
193 gebonden.addEventListener('pointercancel', opPointerUp, true);
194
195 tikker = setInterval(tekenAlles, TIK_MS);
196 tekenAlles();
197}
Note: See TracBrowser for help on using the repository browser.