| 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 |
|
|---|
| 24 | const TIK_MS = 200;
|
|---|
| 25 | let tikker = null;
|
|---|
| 26 | let gebonden = null; // de container waarop de klikafhandelaar hangt
|
|---|
| 27 |
|
|---|
| 28 | /** De speler, of niets als hij (nog) niet geladen is. */
|
|---|
| 29 | const speler = () => window.pcmsAudioPlayer || null;
|
|---|
| 30 |
|
|---|
| 31 | /** De url's die op dit bandje staan, in volgorde. */
|
|---|
| 32 | function 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 | */
|
|---|
| 43 | function 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 |
|
|---|
| 51 | function 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 |
|
|---|
| 76 | function 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 | */
|
|---|
| 93 | const START_SNELHEID = 4; // maal de normale snelheid bij het indrukken
|
|---|
| 94 | const MAX_SNELHEID = 16;
|
|---|
| 95 | const VERSNELLING = 1.35; // per stap
|
|---|
| 96 | const STAP_MS = 120;
|
|---|
| 97 | const TIK_STAP_S = 5; // een losse klik
|
|---|
| 98 |
|
|---|
| 99 | let winder = null;
|
|---|
| 100 |
|
|---|
| 101 | function stopWinden(tape) {
|
|---|
| 102 | if (winder) { clearInterval(winder.timer); winder = null; }
|
|---|
| 103 | if (tape) tape.classList.remove('is-winding', 'is-winding-back');
|
|---|
| 104 | }
|
|---|
| 105 |
|
|---|
| 106 | function 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 |
|
|---|
| 121 | function 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 |
|
|---|
| 129 | function opPointerUp() {
|
|---|
| 130 | if (winder) stopWinden(winder.tape);
|
|---|
| 131 | }
|
|---|
| 132 |
|
|---|
| 133 | function 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 |
|
|---|
| 166 | export 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 | }
|
|---|