| 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 |
|
|---|
| 28 | const TIK_MS = 200;
|
|---|
| 29 | let tikker = null;
|
|---|
| 30 | let gebonden = null; // de container waarop de klikafhandelaar hangt
|
|---|
| 31 |
|
|---|
| 32 | /** De speler, of niets als hij (nog) niet geladen is. */
|
|---|
| 33 | const speler = () => window.pcmsAudioPlayer || null;
|
|---|
| 34 |
|
|---|
| 35 | /** De url's die op dit bandje staan, in volgorde. */
|
|---|
| 36 | function 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 | */
|
|---|
| 47 | function 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 |
|
|---|
| 55 | function 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 |
|
|---|
| 85 | function 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 | */
|
|---|
| 102 | const TIK_STAP_S = 5; // een losse klik
|
|---|
| 103 |
|
|---|
| 104 | function 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 |
|
|---|
| 110 | function 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 |
|
|---|
| 122 | function 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 |
|
|---|
| 130 | function opPointerUp() {
|
|---|
| 131 | if (winder) stopWinden(winder.tape);
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | function 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 |
|
|---|
| 167 | export 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 | }
|
|---|