source: Klonkt/src/assets/js/mod/tape.js@ 4fbe2c1

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

Een bandje loopt niet rond: het stopt aan het eind, en aan het begin

Robins eis (22-8). Drie omlopen zaten in de weg, en de stilste was de keten:
na het laatste nummer hing die nummer een er weer achter, en omdat de wachtrij
EEN doorlopende tijdlijn is merk je dat niet eens als een trackwissel -- de
band gaat gewoon door alsof er meer op stond.

  • De keten haakt in bandmodus niets meer aan voorbij het laatste nummer.
  • next() pauzeert daar in plaats van om te lopen. ended roept next() aan, dus dit is wat de band echt laat stoppen.
  • prev() loopt aan het begin niet naar achteren om: terugspoelen voorbij de kop levert de kop op, niet het laatste nummer.
  • Vooruit prefetchen stopt ook aan het eind.

De drie modulo's blijven staan voor een gewone wachtrij; die hoort wel te
blijven rondlopen. Daar staat een test op, anders sneuvelt dat gedrag stil.

TERUGSPOELEN IS NU ECHT SPOELEN, ook over de nummergrenzen. Op de MSE-motor
ging dat al vanzelf (de band is een tijdlijn). Op de blob-motor landde het in
de KOP van het vorige nummer, en dan kom je nooit ergens in het midden uit; nu
zet hij pendingSeek zodat je aan het EIND van dat nummer uitkomt.

EN EEN FOUT DIE IK ER ZELF MEE INTRODUCEERDE, gevonden door het te draaien en
niet door te lezen: bij een MediaSource vuurt ended pas als de stream dicht
is, en die ging alleen dicht bij een wachtrij van een. Sinds de keten aan het
eind niets meer aanhaakt bleef de band dus hangen op de laatste seconde --
teller stil, isPlaying() waar, spoelen draaiend. Gemeten: 125,6 van 125,7.
De stream sluit nu ook aan het eind van een bandje.

Spoelen stopt vanzelf bij de kop en de staart: beweegt de teller twee rondes
niet, dan is er geen band meer. Anders bleven de spoelen draaien terwijl er
niets gebeurde.

Nagemeten op dev: band loopt af -> laatste nummer, gestopt, teller op 125,7 van
125,7, spoelen stil, knop terug op Afspelen. next() op het laatste nummer
blijft staan; prev() op het eerste spoelt naar de kop.

audio-player v37, MOD_V 54.

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

  • Property mode set to 100644
File size: 7.9 KB
RevLine 
[16fd4fe]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
[e679bce]9 * albumhoes en wordt door die speler opgepakt.
[16fd4fe]10 *
[e679bce]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.
[16fd4fe]16 *
17 * WAAROM ER GEPOLLD WORDT. audio-player.js zendt geen gebeurtenissen uit -- het
18 * is een gesloten module met een klein oppervlak (setQueue, play, pause, next,
19 * prev, isPlaying, currentTrack). Liever hier vijf keer per seconde kijken dan
20 * daar een gebeurtenissenlaag inbouwen die de rest van de site niet vraagt. Het
21 * polsen stopt zodra er geen bandje op de pagina staat.
22 */
23
24const TIK_MS = 200;
25let tikker = null;
26let gebonden = null; // de container waarop de klikafhandelaar hangt
27
28/** De speler, of niets als hij (nog) niet geladen is. */
29const speler = () => window.pcmsAudioPlayer || null;
30
31/** De url's die op dit bandje staan, in volgorde. */
32function bandUrls(tape) {
33 try {
34 return JSON.parse(tape.dataset.pcmsAlbum || '[]').map((t) => t.url);
35 } catch { return []; }
36}
37
38/**
39 * Speelt DIT bandje op dit moment? Vergelijken op url en niet op index: de
40 * wachtrij kan ondertussen van een ander blok komen, en dan hoort deze cassette
41 * gewoon stil te staan.
42 */
43function huidigeIndex(tape) {
44 const p = speler();
45 if (!p) return -1;
46 const nu = p.currentTrack();
47 if (!nu || !nu.url) return -1;
48 return bandUrls(tape).indexOf(nu.url);
49}
50
51function tekenBand(tape) {
52 const i = huidigeIndex(tape);
53 const p = speler();
54 const draait = i >= 0 && !!p && p.isPlaying();
55
56 tape.classList.toggle('is-playing', draait);
57 tape.classList.toggle('is-loaded', i >= 0);
58
59 const knop = tape.querySelector('[data-tape-play]');
60 if (knop) {
61 knop.classList.toggle('is-pauze', draait);
62 knop.setAttribute('aria-label', draait ? 'Pauzeren' : 'Afspelen');
63 }
64
65 // Welk nummer loopt er. Bij een bandje is dat de enige plek waar je het leest,
66 // want de lijst is met opzet geen knoppenrij.
67 const items = [...tape.querySelectorAll('.tape-track')];
68 items.forEach((li, n) => li.classList.toggle('is-current', n === i));
69 const nu = tape.querySelector('[data-tape-now]');
70 if (nu) {
71 const titel = i >= 0 && items[i] ? items[i].querySelector('.tape-track-title').textContent : '';
72 nu.textContent = i >= 0 ? `${i + 1}. ${titel}` : '';
73 }
74}
75
76function tekenAlles() {
77 for (const tape of document.querySelectorAll('.post-tape')) tekenBand(tape);
78}
79
[e679bce]80/**
81 * SPOELEN IS VASTHOUDEN, en dat is het verschil met een afspeellijst.
82 *
83 * Eerst deed vooruit en achteruit hier next()/prev(): een nummer verder. Dat is
84 * een playlistgebaar. Op een cassette bestaat "volgend nummer" niet -- je houdt
85 * de knop ingedrukt, de band loopt door, en je laat los waar je bent. Robin
86 * wees daarop (21-8) en hij heeft gelijk: met knoppen die per nummer springen
87 * is het een lijst met een cassetteplaatje erboven.
88 *
89 * Het versnelt terwijl je hem vasthoudt, zoals een echt bandje op gang komt.
90 * Een korte TIK spoelt een klein stukje -- dat is er voor toetsenbord en
91 * schermlezer, want vasthouden is met een spatiebalk geen gebaar.
92 */
93const START_SNELHEID = 4; // maal de normale snelheid bij het indrukken
94const MAX_SNELHEID = 16;
95const VERSNELLING = 1.35; // per stap
96const STAP_MS = 120;
97const TIK_STAP_S = 5; // een losse klik
98
99let winder = null;
100
101function stopWinden(tape) {
102 if (winder) { clearInterval(winder.timer); winder = null; }
103 if (tape) tape.classList.remove('is-winding', 'is-winding-back');
104}
105
106function startWinden(tape, richting) {
107 const p = speler();
108 if (!p || huidigeIndex(tape) < 0) return;
109 stopWinden(tape);
110 let snelheid = START_SNELHEID;
[4fbe2c1]111 let vorigePositie = null;
112 let stil = 0;
[e679bce]113 tape.classList.add('is-winding');
114 if (richting < 0) tape.classList.add('is-winding-back');
115 const timer = setInterval(() => {
116 snelheid = Math.min(MAX_SNELHEID, snelheid * VERSNELLING);
117 p.seekBy(richting * snelheid * (STAP_MS / 1000));
118 tekenBand(tape);
[4fbe2c1]119 // AAN HET EIND VAN DE BAND STOPPEN MET SPOELEN. De speler klemt de positie
120 // af, dus daar gebeurt niets meer -- maar de spoelen bleven wel doordraaien
121 // en de knop bleef oplichten, alsof er nog band was. Beweegt de teller twee
122 // rondes niet, dan zijn we bij de kop of de staart.
123 const nu = p.tapeTijden ? p.tapeTijden().cur : null;
124 if (nu !== null && vorigePositie !== null && Math.abs(nu - vorigePositie) < 0.05) {
125 if (++stil >= 2) { stopWinden(tape); return; }
126 } else stil = 0;
127 vorigePositie = nu;
[e679bce]128 }, STAP_MS);
129 winder = { timer, tape };
130}
131
132function opPointerDown(e) {
133 const knop = e.target.closest && e.target.closest('[data-tape-go]');
134 if (!knop) return;
135 const tape = knop.closest('.post-tape');
136 if (!tape) return;
137 startWinden(tape, knop.dataset.tapeGo === 'fwd' ? 1 : -1);
138}
139
140function opPointerUp() {
141 if (winder) stopWinden(winder.tape);
142}
143
[16fd4fe]144function opKlik(e) {
145 const tape = e.target.closest && e.target.closest('.post-tape');
146 if (!tape) return;
147 const p = speler();
148
149 const spoel = e.target.closest('[data-tape-go]');
150 if (spoel) {
[e679bce]151 // Een tik spoelt een stukje. Vasthouden gaat via de pointer-afhandelaars
152 // hierboven; die hebben dan al gespoeld en deze klik doet er nog een klein
153 // beetje bovenop, wat niet stoort.
154 if (!p || huidigeIndex(tape) < 0) return;
155 p.seekBy(spoel.dataset.tapeGo === 'fwd' ? TIK_STAP_S : -TIK_STAP_S);
156 setTimeout(() => tekenBand(tape), 60);
[16fd4fe]157 return;
158 }
159
160 const play = e.target.closest('[data-tape-play]');
161 if (play && p) {
162 // Loopt dit bandje al, dan is deze knop een pauzeknop. De globale
[e679bce]163 // afhandelaar van audio-player.js zou hem anders opnieuw vanaf het begin
[16fd4fe]164 // starten, want die kent alleen "speel deze wachtrij".
165 const i = huidigeIndex(tape);
166 if (i >= 0) {
167 e.preventDefault();
168 e.stopPropagation();
169 if (p.isPlaying()) p.pause(); else p.play();
170 setTimeout(tekenAlles, 60);
171 }
172 // Staat het bandje stil, dan laten we de klik doorlopen: de speler pakt de
[e679bce]173 // data-attributen op en zet de band op.
[16fd4fe]174 }
175}
176
177export function init() {
178 const tapes = document.querySelectorAll('.post-tape');
179
180 // Altijd eerst opruimen, ook als er niets staat. Zonder dat blijft de tikker
181 // lopen na een paginawissel naar een pagina zonder bandje -- dezelfde fout
182 // die in de leesweergave een keer de hele site heeft vastgezet.
183 if (tikker) { clearInterval(tikker); tikker = null; }
[e679bce]184 stopWinden(null);
185 if (gebonden) {
186 gebonden.removeEventListener('click', opKlik, true);
187 gebonden.removeEventListener('pointerdown', opPointerDown, true);
188 gebonden.removeEventListener('pointerup', opPointerUp, true);
189 gebonden.removeEventListener('pointercancel', opPointerUp, true);
190 gebonden = null;
191 }
[16fd4fe]192 if (!tapes.length) return;
193
194 // Vangen in de CAPTURE-fase, want de speler luistert zelf op document in de
195 // bubbelfase. Alleen zo kan de pauzeknop zijn klik tegenhouden voordat de
196 // wachtrij opnieuw wordt gezet.
197 gebonden = document;
198 gebonden.addEventListener('click', opKlik, true);
[e679bce]199 gebonden.addEventListener('pointerdown', opPointerDown, true);
200 // Loslaten telt ook als je BUITEN de knop loslaat, anders blijft de band
201 // doorspoelen als je met je vinger wegglijdt.
202 gebonden.addEventListener('pointerup', opPointerUp, true);
203 gebonden.addEventListener('pointercancel', opPointerUp, true);
[16fd4fe]204
205 tikker = setInterval(tekenAlles, TIK_MS);
206 tekenAlles();
207}
Note: See TracBrowser for help on using the repository browser.