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

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

Het bandje speelt als bandje: een cassette, een object, spoelen in seconden

Robins correctie (21-8): vooruit en achteruit deden hier next()/prev(), en dat
is een playlistgebaar. Dan is het een lijst met een cassetteplaatje erboven.
Een cassette kent geen "volgend nummer" -- je houdt de knop ingedrukt, de band
loopt door, en je laat los waar je bent.

Dat kan omdat audio-player.js een wachtrij al in EEN doorlopende
MediaSource-tijdlijn giet: "een trackwissel is een positie, geen omschakeling".
Een bandje IS die tijdlijn. Dus:

  • seekBy(seconden) op de speler, over de nummergrenzen heen. Op de blob-motor (iOS, geen MSE) bestaat die tijdlijn niet; daar loopt spoelen tot de rand van het nummer en stapt dan naar de buur. Grover, maar eerlijker dan doen alsof.
  • Bandmodus: de speler toont het BANDJE als titel met het lopende nummer eronder, en de teller loopt over de hele band in plaats van per nummer. Dat is wat "een object, geen losse tracks" betekent op het scherm.
  • Vasthouden versnelt (4x tot 16x), loslaten stopt -- ook als je buiten de knop loslaat. Een korte tik spoelt vijf seconden, want vasthouden is met een spatiebalk geen gebaar.

Een echte cassette, als SVG. Verhouding 100,4 x 63,8 mm, de maat van een
compact cassette, dus viewBox 314x200: behuizing, labelvlak met de titel erop,
venster, twee spoelen met tandjes die meedraaien, de bandpakketten, vier
schroefjes en de openingen voor de kop en de capstans. De vorige versie waren
twee CSS-schijfjes.

Twee dingen die alleen door het echt te draaien boven kwamen:

  • audio-player.js kiest zijn knoppen met een lijst KLASSENAMEN (PLAY_SELECTOR), niet met een regel over data-attributen. De cassetteknop droeg alle juiste data-pcms-* en deed niets. Er staat nu een test op.
  • De sessie sloeg displayTimes().cur op als trackpositie. In bandmodus is dat de bandteller, dus een hersteld bandje sprong naar een plek die in dat nummer niet bestaat. Daarvoor is trackTijd() afgesplitst.

MOD_V 53, style.css v107, audio-player v35.

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

  • Property mode set to 100644
File size: 7.3 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 * 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
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;
111 tape.classList.add('is-winding');
112 if (richting < 0) tape.classList.add('is-winding-back');
113 const timer = setInterval(() => {
114 snelheid = Math.min(MAX_SNELHEID, snelheid * VERSNELLING);
115 p.seekBy(richting * snelheid * (STAP_MS / 1000));
116 tekenBand(tape);
117 }, STAP_MS);
118 winder = { timer, tape };
119}
120
121function opPointerDown(e) {
122 const knop = e.target.closest && e.target.closest('[data-tape-go]');
123 if (!knop) return;
124 const tape = knop.closest('.post-tape');
125 if (!tape) return;
126 startWinden(tape, knop.dataset.tapeGo === 'fwd' ? 1 : -1);
127}
128
129function opPointerUp() {
130 if (winder) stopWinden(winder.tape);
131}
132
133function opKlik(e) {
134 const tape = e.target.closest && e.target.closest('.post-tape');
135 if (!tape) return;
136 const p = speler();
137
138 const spoel = e.target.closest('[data-tape-go]');
139 if (spoel) {
140 // Een tik spoelt een stukje. Vasthouden gaat via de pointer-afhandelaars
141 // hierboven; die hebben dan al gespoeld en deze klik doet er nog een klein
142 // beetje bovenop, wat niet stoort.
143 if (!p || huidigeIndex(tape) < 0) return;
144 p.seekBy(spoel.dataset.tapeGo === 'fwd' ? TIK_STAP_S : -TIK_STAP_S);
145 setTimeout(() => tekenBand(tape), 60);
146 return;
147 }
148
149 const play = e.target.closest('[data-tape-play]');
150 if (play && p) {
151 // Loopt dit bandje al, dan is deze knop een pauzeknop. De globale
152 // afhandelaar van audio-player.js zou hem anders opnieuw vanaf het begin
153 // starten, want die kent alleen "speel deze wachtrij".
154 const i = huidigeIndex(tape);
155 if (i >= 0) {
156 e.preventDefault();
157 e.stopPropagation();
158 if (p.isPlaying()) p.pause(); else p.play();
159 setTimeout(tekenAlles, 60);
160 }
161 // Staat het bandje stil, dan laten we de klik doorlopen: de speler pakt de
162 // data-attributen op en zet de band op.
163 }
164}
165
166export function init() {
167 const tapes = document.querySelectorAll('.post-tape');
168
169 // Altijd eerst opruimen, ook als er niets staat. Zonder dat blijft de tikker
170 // lopen na een paginawissel naar een pagina zonder bandje -- dezelfde fout
171 // die in de leesweergave een keer de hele site heeft vastgezet.
172 if (tikker) { clearInterval(tikker); tikker = null; }
173 stopWinden(null);
174 if (gebonden) {
175 gebonden.removeEventListener('click', opKlik, true);
176 gebonden.removeEventListener('pointerdown', opPointerDown, true);
177 gebonden.removeEventListener('pointerup', opPointerUp, true);
178 gebonden.removeEventListener('pointercancel', opPointerUp, true);
179 gebonden = null;
180 }
181 if (!tapes.length) return;
182
183 // Vangen in de CAPTURE-fase, want de speler luistert zelf op document in de
184 // bubbelfase. Alleen zo kan de pauzeknop zijn klik tegenhouden voordat de
185 // wachtrij opnieuw wordt gezet.
186 gebonden = document;
187 gebonden.addEventListener('click', opKlik, true);
188 gebonden.addEventListener('pointerdown', opPointerDown, true);
189 // Loslaten telt ook als je BUITEN de knop loslaat, anders blijft de band
190 // doorspoelen als je met je vinger wegglijdt.
191 gebonden.addEventListener('pointerup', opPointerUp, true);
192 gebonden.addEventListener('pointercancel', opPointerUp, true);
193
194 tikker = setInterval(tekenAlles, TIK_MS);
195 tekenAlles();
196}
Note: See TracBrowser for help on using the repository browser.