- Timestamp:
- 08/22/2026 09:54:51 AM (3 weeks ago)
- Branches:
- main
- Children:
- 8bb3b09
- Parents:
- 16fd4fe
- Location:
- src
- Files:
-
- 5 edited
-
assets/css/style.css (modified) (5 diffs)
-
assets/js/audio-player.js (modified) (11 diffs)
-
assets/js/mod/tape.js (modified) (7 diffs)
-
services/AudioEmbedService.js (modified) (1 diff)
-
views/shell.ejs (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r16fd4fe re679bce 5078 5078 5079 5079 /* ── Het bandje (mixtape) ────────────────────────────────────────────── 5080 Zelfde familie als .post-album hierboven: dezelfde rand, dezelfde radius,5081 dezelfde papierkleur. Het verschil zit in wat je ziet draaien, niet in een5082 ander jasje -- een mixtape is een soort uitgave binnen Klonkt, geen ander5083 product.5084 5085 Alle s hieronder gebruikt de bestaande variabelen (--rule, --paper-2, --ink,5086 --accent), dus het volgt licht en donker en het accent van de sitevanzelf. */5080 Een cassette, getekend als SVG op de echte maat: 100,4 x 63,8 mm, dus een 5081 viewBox van 314x200. De vorige versie tekende twee schijfjes met CSS; dit is 5082 een replica -- behuizing, labelvlak, venster, twee spoelen met tandjes, de 5083 schroefjes in de hoeken en de openingen voor de kop en de capstans onderin. 5084 5085 Alle kleuren komen uit de bestaande variabelen, dus licht, donker en het 5086 accent van de site volgen vanzelf. */ 5087 5087 .post-tape { 5088 5088 position: relative; … … 5094 5094 } 5095 5095 5096 /* De cassette zelf. De verhouding is die van een echt bandje (ongeveer 10:6), 5097 zodat hij ook op een telefoon als cassette leest en niet als balk. */ 5098 .tape-shell { 5099 display: grid; 5100 grid-template-rows: auto auto; 5101 gap: .5rem; 5102 padding: .8rem .8rem .4rem; 5103 } 5104 .tape-window { 5105 position: relative; 5106 display: flex; 5107 align-items: center; 5108 justify-content: center; 5109 gap: 12%; 5110 padding: .9rem 1.2rem; 5111 border-radius: 10px; 5112 background: color-mix(in srgb, var(--ink) 8%, transparent); 5113 border: 1px solid var(--rule, rgba(0,0,0,.12)); 5114 } 5115 /* De band tussen de spoelen. Een dun lijntje is genoeg: het gaat om de twee 5116 draaiende schijven, dat is wat een cassette een cassette maakt. */ 5117 .tape-band { 5096 .tape-shell { position: relative; padding: .8rem .8rem .3rem; } 5097 .tape-svg { display: block; width: 100%; height: auto; } 5098 5099 /* De behuizing. Twee lagen zodat er een randje omheen zit, zoals bij een echt 5100 doosje waar de bovenschaal iets uitsteekt. */ 5101 .tape-body { fill: color-mix(in srgb, var(--ink) 82%, var(--paper, #fff)); } 5102 .tape-body-inner { fill: color-mix(in srgb, var(--ink) 70%, var(--paper, #fff)); } 5103 .tape-labelplate { fill: color-mix(in srgb, var(--paper, #fff) 92%, var(--accent)); } 5104 .tape-glass { 5105 fill: color-mix(in srgb, var(--ink) 88%, transparent); 5106 stroke: color-mix(in srgb, var(--paper, #fff) 25%, transparent); 5107 stroke-width: 1.5; 5108 } 5109 .tape-pack { fill: color-mix(in srgb, var(--ink) 55%, var(--paper, #fff)); } 5110 .tape-ribbon { fill: color-mix(in srgb, var(--ink) 45%, var(--paper, #fff)); } 5111 .tape-hub { fill: var(--paper, #fff); } 5112 .tape-tooth { fill: color-mix(in srgb, var(--ink) 60%, var(--paper, #fff)); } 5113 .tape-screw { fill: color-mix(in srgb, var(--ink) 55%, var(--paper, #fff)); } 5114 .tape-slot { fill: color-mix(in srgb, var(--ink) 92%, transparent); } 5115 .tape-slot--capstan { fill: color-mix(in srgb, var(--ink) 96%, transparent); } 5116 5117 /* Draaien. De tandjes zitten in dezelfde groep als de naaf, dus de hele spoel 5118 draait als een geheel -- daar gaat het om, want aan die tandjes zie je pas 5119 DAT hij draait. Het label blijft stil, anders wordt de titel onleesbaar. */ 5120 .post-tape.is-playing .tape-reel { animation: tape-spin 3s linear infinite; } 5121 .post-tape.is-playing .tape-reel--right { animation-duration: 2.2s; } 5122 /* Spoelen: veel sneller, en achteruit ook echt achteruit. */ 5123 .post-tape.is-winding .tape-reel { animation-duration: .45s; } 5124 .post-tape.is-winding-back .tape-reel { animation-direction: reverse; } 5125 @keyframes tape-spin { to { transform: rotate(360deg); } } 5126 @media (prefers-reduced-motion: reduce) { 5127 .post-tape.is-playing .tape-reel, 5128 .post-tape.is-winding .tape-reel { animation: none; } 5129 } 5130 5131 /* De tekst staat OP het labelvlak van de cassette. Percentages volgen de 5132 viewBox, dus het blijft kloppen op elke breedte. */ 5133 .tape-label { 5118 5134 position: absolute; 5119 left: 22%; right: 22%; 5120 height: 3px; 5121 background: color-mix(in srgb, var(--ink) 35%, transparent); 5122 border-radius: 2px; 5123 } 5124 .tape-reel { 5125 position: relative; 5126 width: clamp(48px, 16vw, 76px); 5127 aspect-ratio: 1; 5128 border-radius: 50%; 5129 background: 5130 repeating-conic-gradient(from 0deg, 5131 color-mix(in srgb, var(--ink) 22%, transparent) 0deg 12deg, 5132 transparent 12deg 24deg); 5133 border: 2px solid color-mix(in srgb, var(--ink) 30%, transparent); 5134 display: grid; 5135 place-items: center; 5136 } 5137 .tape-reel-hub { 5138 width: 34%; 5139 aspect-ratio: 1; 5140 border-radius: 50%; 5141 background: var(--paper, #fff); 5142 border: 2px solid color-mix(in srgb, var(--ink) 40%, transparent); 5143 } 5144 /* Draaien doet hij alleen als hij speelt. De animatie staat op de spoel en niet 5145 op de hele cassette: het label hoort stil te blijven, anders wordt de titel 5146 onleesbaar. */ 5147 .post-tape.is-playing .tape-reel { animation: tape-spin 2.4s linear infinite; } 5148 .post-tape.is-playing .tape-reel--right { animation-duration: 3.2s; } 5149 @keyframes tape-spin { to { transform: rotate(360deg); } } 5150 /* Wie liever geen beweging ziet, krijgt geen beweging. De cassette blijft dan 5151 gewoon staan; de statusregel eronder vertelt nog steeds wat er speelt. */ 5152 @media (prefers-reduced-motion: reduce) { 5153 .post-tape.is-playing .tape-reel { animation: none; } 5154 } 5155 5156 .tape-label { 5157 padding: .5rem .6rem; 5158 border-radius: 8px; 5159 background-size: cover; 5160 background-position: center; 5161 position: relative; 5162 } 5163 /* Staat er een hoes achter het label, dan moet de tekst leesbaar blijven. */ 5164 .tape-label[style] { color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.7); } 5135 left: 8.4%; right: 8.4%; 5136 top: calc(.8rem + 11%); 5137 height: 31%; 5138 padding: .1rem .4rem; 5139 color: color-mix(in srgb, var(--ink) 92%, transparent); 5140 overflow: hidden; 5141 } 5165 5142 .tape-kind { 5166 5143 margin: 0; 5167 font-size: .7rem;5168 letter-spacing: . 06em;5144 font-size: clamp(.55rem, 1.6vw, .68rem); 5145 letter-spacing: .14em; 5169 5146 text-transform: uppercase; 5170 opacity: .75; 5171 } 5172 .tape-title { margin: .1rem 0 0; font-size: 1.05rem; line-height: 1.25; } 5173 .tape-artist { margin: .05rem 0 0; font-size: .85rem; opacity: .8; } 5174 .tape-meta { margin: .15rem 0 0; font-size: .75rem; opacity: .65; } 5175 5176 /* De knoppen: terugspoelen, spelen, vooruitspoelen. Meer heeft een bandje niet, 5177 en dat is de bedoeling -- er is geen zoekbalk omdat je op een cassette niet 5178 kunt zoeken. */ 5147 opacity: .7; 5148 } 5149 .tape-title { 5150 margin: 0; 5151 font-size: clamp(.85rem, 2.6vw, 1.1rem); 5152 line-height: 1.15; 5153 white-space: nowrap; 5154 overflow: hidden; 5155 text-overflow: ellipsis; 5156 } 5157 .tape-artist { margin: 0; font-size: clamp(.65rem, 1.9vw, .8rem); opacity: .8; } 5158 .tape-meta { margin: 0; font-size: clamp(.6rem, 1.7vw, .72rem); opacity: .6; } 5159 5160 /* De knoppen: terugspoelen, spelen, vooruitspoelen. Meer heeft een cassette 5161 niet, en dat is de bedoeling -- er is geen zoekbalk omdat je op een bandje 5162 niet kunt zoeken, alleen spoelen. */ 5179 5163 .tape-controls { 5180 5164 display: flex; … … 5182 5166 justify-content: center; 5183 5167 gap: .6rem; 5184 padding: . 3rem 0 .7rem;5168 padding: .5rem 0 .6rem; 5185 5169 } 5186 5170 .tape-btn { … … 5194 5178 cursor: pointer; 5195 5179 padding: 0; 5180 touch-action: manipulation; /* vasthouden mag geen zoom of scroll worden */ 5181 user-select: none; 5196 5182 } 5197 5183 .tape-btn svg { width: 18px; height: 18px; } … … 5205 5191 } 5206 5192 .tape-btn--play svg { width: 22px; height: 22px; } 5207 /* Speelt hij, dan is dezelfde knop de pauzeknop. Twee knoppen zou een bandje 5208 niet hebben. */ 5209 .post-tape.is-playing .tape-btn--play svg path { d: path("M7 5h4v14H7zM13 5h4v14h-4z"); } 5193 .post-tape.is-winding .tape-btn--back, 5194 .post-tape.is-winding .tape-btn--fwd { border-color: var(--accent); color: var(--accent); } 5210 5195 5211 5196 .tape-now { -
src/assets/js/audio-player.js
r16fd4fe re679bce 158 158 let isPlaying = false; 159 159 let albumName = ''; 160 // BANDMODUS (Robins eis, 21-8): een mixtape is EEN object, geen wachtrij met 161 // nummers. De MSE-keten hieronder maakt van een wachtrij toch al een 162 // doorlopende tijdlijn -- "een trackwissel is een positie, geen omschakeling" 163 // -- dus een bandje is precies die tijdlijn, alleen anders getoond en anders 164 // bediend: je ziet de titel van het bandje, de teller loopt over het geheel, 165 // en spoelen gaat in seconden in plaats van per nummer. 166 let tapeMode = false; 160 167 // Playback pipeline. We fetch each track's bytes ourselves (X-Audio-Player 161 168 // gate; no plain media URL is ever exposed to the page) and feed them to the … … 457 464 // Current position/duration in TRACK coordinates (the MSE timeline is the 458 465 // whole chain; the UI always shows the single playing track). 459 function displayTimes() { 466 // De positie BINNEN het huidige nummer. Apart van displayTimes(), want die 467 // geeft in bandmodus de teller over de hele band -- en het opslaan van de 468 // sessie heeft juist de trackpositie nodig: bij het herstellen begint de 469 // keten opnieuw en start dit nummer weer op nul. 470 function trackTijd() { 460 471 if (useMse() && chain.length) { 461 472 const seg = currentSegment(); … … 463 474 } 464 475 return { cur: audio.currentTime || 0, dur: audio.duration }; 476 } 477 478 function displayTimes() { 479 // Een bandje heeft een teller, geen nummerpositie: hij telt door over de 480 // kant heen. Dat is letterlijk de ketentijdlijn, dus hier juist NIET 481 // terugrekenen naar trackcoordinaten. 482 if (tapeMode && useMse() && chain.length) { 483 return { cur: audio.currentTime || 0, dur: chain[chain.length - 1].end }; 484 } 485 return trackTijd(); 465 486 } 466 487 … … 505 526 // session metadata. Called from loadTrack AND from the gapless boundary-cross. 506 527 function updateTrackChrome(t) { 507 titleEl.textContent = t.title || 'Untitled'; 508 artistEl.textContent = t.artist || ''; 509 sheetTitle.textContent = t.title || 'Untitled'; 510 sheetArtist.textContent = t.artist || ''; 528 // In bandmodus staat het BANDJE op de speler. Wat er op dit moment klinkt 529 // staat eronder, zoals een cassette een titel op het label heeft en de 530 // nummers op het doosje. 531 const hoofd = tapeMode ? (albumName || 'Mixtape') : (t.title || 'Untitled'); 532 const onder = tapeMode ? (t.title || '') : (t.artist || ''); 533 titleEl.textContent = hoofd; 534 artistEl.textContent = onder; 535 sheetTitle.textContent = hoofd; 536 sheetArtist.textContent = onder; 511 537 sheetAlbum.textContent = albumName || ''; 512 538 setCoverImage(cover, t.cover); … … 579 605 queue = Array.isArray(tracks) ? tracks.slice() : []; 580 606 albumName = (opts && opts.albumName) || ''; 607 tapeMode = !!(opts && opts.asTape); 581 608 if (!queue.length) return; 582 609 loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true); … … 629 656 queue = []; 630 657 albumName = ''; 658 tapeMode = false; 631 659 loadSeq++; // cancel any in-flight load 632 660 dropPreload(); … … 1010 1038 // would leave dead buttons (flag baked into the snapshot, listener gone). One 1011 1039 // delegated listener works regardless of how many times the DOM is (re)swapped. 1040 // WELKE KNOPPEN DEZE SPELER BEDIENT. Let op: dit is een LIJST MET NAMEN, geen 1041 // regel over data-attributen. Een nieuwe knop die keurig data-pcms-track-url 1042 // en data-pcms-album-id draagt doet dus niets zolang hij hier niet bij staat. 1043 // Precies daar liep de cassetteknop op vast (21-8): de opmaak klopte, de 1044 // gegevens klopten, en er gebeurde niets. 1012 1045 const PLAY_SELECTOR = 1013 '.post-audio-track .pat-play, .post-album-tracks .pat-row, .post-album-cover-btn, .post-album-playall ';1046 '.post-audio-track .pat-play, .post-album-tracks .pat-row, .post-album-cover-btn, .post-album-playall, .tape-btn--play'; 1014 1047 document.body.addEventListener('click', (e) => { 1015 1048 const btn = e.target.closest(PLAY_SELECTOR); … … 1038 1071 // (cover-btn and playall both want to start from the beginning). 1039 1072 const startIdx = trackUrl ? tracks.findIndex(t => t.url === trackUrl) : 0; 1040 setQueue(tracks, startIdx >= 0 ? startIdx : 0, { albumName: album.dataset.pcmsAlbumTitle || '' }); 1073 // Een mixtape gaat als EEN object de speler in: de titel van het bandje 1074 // op de speler, de teller over de hele band, en spoelen in seconden. 1075 // De soort staat al op het blok (data-pcms-album-kind), dus hier is het 1076 // een doorgeefje en geen tweede plek die iets afleidt. 1077 setQueue(tracks, startIdx >= 0 ? startIdx : 0, { 1078 albumName: album.dataset.pcmsAlbumTitle || '', 1079 asTape: album.dataset.pcmsAlbumKind === 'mixtape', 1080 }); 1041 1081 } catch(err) { console.error('[pcms-audio] bad album JSON', err, album.dataset.pcmsAlbum); } 1042 1082 } else if (trackData) { … … 1092 1132 // 9. Public API 1093 1133 // ============================================================ 1134 /** 1135 * SPOELEN, in seconden over de hele band. 1136 * 1137 * Op de MSE-motor is dit precies wat een cassette doet: `audio.currentTime` 1138 * IS de tijdlijn van de hele keten, dus over een nummergrens heen spoelen is 1139 * gewoon doortellen. Geen trackwissel, geen nieuwe afspeelsessie. 1140 * 1141 * Op de blob-motor (iOS Safari, geen MSE) bestaat die doorlopende tijdlijn 1142 * niet: daar is elk nummer een eigen bron. Spoelen loopt daar dus tot de rand 1143 * van het huidige nummer en stapt dan naar de buur. Grover, maar het is 1144 * eerlijker dan doen alsof de band doorloopt terwijl hij dat niet doet. 1145 */ 1146 function seekBy(seconden) { 1147 const d = Number(seconden) || 0; 1148 if (!d || !audio) return; 1149 if (useMse() && chain.length) { 1150 const eind = chain[chain.length - 1].end; 1151 const doel = Math.max(0, Math.min((audio.currentTime || 0) + d, Math.max(0, eind - 0.25))); 1152 try { audio.currentTime = doel; } catch (e) { /* buffer nog niet zover */ } 1153 updatePositionState(); 1154 return; 1155 } 1156 // Blob-motor: binnen het nummer blijven, en anders naar de buur. 1157 const duur = audio.duration || 0; 1158 const nu = audio.currentTime || 0; 1159 if (duur && nu + d >= duur) { next(); return; } 1160 if (nu + d < 0) { prev(); return; } 1161 try { audio.currentTime = Math.max(0, nu + d); } catch (e) {} 1162 } 1163 1164 /** Waar staat de teller, over het hele bandje. */ 1165 function tapeTijden() { return displayTimes(); } 1166 1094 1167 window.pcmsAudioPlayer = { 1095 setQueue, play, pause, next, prev, close, openSheet, closeSheet, 1168 setQueue, play, pause, next, prev, close, openSheet, closeSheet, seekBy, tapeTijden, 1169 isTape: () => tapeMode, 1096 1170 isPlaying: () => isPlaying, 1097 1171 currentTrack: () => queue[currentIndex] || null, … … 1113 1187 if (!queue.length) { sessionStorage.removeItem(PLAYER_STATE_KEY); return; } 1114 1188 sessionStorage.setItem(PLAYER_STATE_KEY, JSON.stringify({ 1115 queue, currentIndex, albumName, 1189 queue, currentIndex, albumName, tapeMode, 1116 1190 // In-TRACK position (the MSE timeline spans the whole chain; a restore 1117 1191 // starts a fresh chain where this track begins at 0). 1118 time: displayTimes().cur || 0,1192 time: trackTijd().cur || 0, 1119 1193 playing: !!audio.src && !audio.paused, 1120 1194 })); … … 1140 1214 queue = s.queue; 1141 1215 albumName = s.albumName || ''; 1216 tapeMode = !!s.tapeMode; 1142 1217 pendingSeek = s.time || 0; 1143 1218 const idx = Math.max(0, Math.min(s.currentIndex || 0, queue.length - 1)); -
src/assets/js/mod/tape.js
r16fd4fe re679bce 7 7 * een bandgevoel na te doen zou twee dingen tegelijk laten klinken en die hele 8 8 * sessie weggooien. Dus: de afspeelknop draagt dezelfde data-attributen als een 9 * albumhoes en wordt door die speler opgepakt , en spoelen is next()/prev().9 * albumhoes en wordt door die speler opgepakt. 10 10 * 11 * DAAROM SPOELT HET PER NUMMER en niet per seconde. Die speler denkt in een 12 * wachtrij. Dat is een eerlijke benadering van een cassette: je komt vooruit en 13 * achteruit, en je springt niet naar nummer zeven. 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. 14 16 * 15 17 * WAAROM ER GEPOLLD WORDT. audio-player.js zendt geen gebeurtenissen uit -- het … … 76 78 } 77 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 78 133 function opKlik(e) { 79 134 const tape = e.target.closest && e.target.closest('.post-tape'); … … 83 138 const spoel = e.target.closest('[data-tape-go]'); 84 139 if (spoel) { 85 if (!p) return; 86 // Spoelen kan alleen als DIT bandje loopt. Anders zou de knop de wachtrij 87 // van een ander blok verzetten, en dat is niet wat een spoelknop op deze 88 // cassette hoort te doen. 89 if (huidigeIndex(tape) < 0) return; 90 if (spoel.dataset.tapeGo === 'fwd') p.next(); else p.prev(); 91 setTimeout(tekenAlles, 60); 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); 92 146 return; 93 147 } … … 96 150 if (play && p) { 97 151 // Loopt dit bandje al, dan is deze knop een pauzeknop. De globale 98 // afhandelaar van audio-player.js zou hem anders opnieuw vanaf nummer een152 // afhandelaar van audio-player.js zou hem anders opnieuw vanaf het begin 99 153 // starten, want die kent alleen "speel deze wachtrij". 100 154 const i = huidigeIndex(tape); … … 106 160 } 107 161 // Staat het bandje stil, dan laten we de klik doorlopen: de speler pakt de 108 // data-attributen op en zet de wachtrijop.162 // data-attributen op en zet de band op. 109 163 } 110 164 } … … 117 171 // die in de leesweergave een keer de hele site heeft vastgezet. 118 172 if (tikker) { clearInterval(tikker); tikker = null; } 119 if (gebonden) { gebonden.removeEventListener('click', opKlik, true); gebonden = 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 } 120 181 if (!tapes.length) return; 121 182 … … 125 186 gebonden = document; 126 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); 127 193 128 194 tikker = setInterval(tekenAlles, TIK_MS); -
src/services/AudioEmbedService.js
r16fd4fe re679bce 726 726 </div> 727 727 ` : ''} <div class="tape-shell"> 728 <div class="tape-window" aria-hidden="true"> 729 <span class="tape-reel tape-reel--left"><span class="tape-reel-hub"></span></span> 730 <span class="tape-band"></span> 731 <span class="tape-reel tape-reel--right"><span class="tape-reel-hub"></span></span> 732 </div> 733 <div class="tape-label"${coverH ? ` style="background-image:url('${coverH}')"` : ''}> 734 <p class="tape-kind">📼 Mixtape</p> 728 <svg class="tape-svg" viewBox="0 0 314 200" role="img" aria-label="Cassette" preserveAspectRatio="xMidYMid meet"> 729 <defs> 730 <clipPath id="tapewin-${domId}"><rect x="72" y="96" width="170" height="62" rx="6"/></clipPath> 731 </defs> 732 <!-- de behuizing --> 733 <rect class="tape-body" x="3" y="3" width="308" height="194" rx="9"/> 734 <rect class="tape-body-inner" x="12" y="12" width="290" height="176" rx="6"/> 735 <!-- het labelvlak: hier komt de HTML-tekst overheen te staan --> 736 <rect class="tape-labelplate" x="24" y="22" width="266" height="62" rx="3"/> 737 <!-- het venster waardoor je de band ziet lopen --> 738 <rect class="tape-glass" x="72" y="96" width="170" height="62" rx="6"/> 739 <g clip-path="url(#tapewin-${domId})"> 740 <!-- de bandpakketten om de spoelen; links loopt vol terwijl rechts leegloopt --> 741 <circle class="tape-pack tape-pack--left" cx="112" cy="127" r="30"/> 742 <circle class="tape-pack tape-pack--right" cx="202" cy="127" r="22"/> 743 <rect class="tape-ribbon" x="112" y="150" width="90" height="3"/> 744 </g> 745 <!-- de spoelen zelf: deze twee draaien --> 746 <g class="tape-reel tape-reel--left" style="transform-origin:112px 127px"> 747 <circle class="tape-hub" cx="112" cy="127" r="15"/> 748 ${[0, 60, 120, 180, 240, 300].map((a) => `<rect class="tape-tooth" x="109.5" y="112" width="5" height="9" rx="1" transform="rotate(${a} 112 127)"/>`).join('')} 749 </g> 750 <g class="tape-reel tape-reel--right" style="transform-origin:202px 127px"> 751 <circle class="tape-hub" cx="202" cy="127" r="15"/> 752 ${[0, 60, 120, 180, 240, 300].map((a) => `<rect class="tape-tooth" x="199.5" y="112" width="5" height="9" rx="1" transform="rotate(${a} 202 127)"/>`).join('')} 753 </g> 754 <!-- de schroefjes in de hoeken, en de openingen voor de kop onderin --> 755 <circle class="tape-screw" cx="22" cy="22" r="3.5"/> 756 <circle class="tape-screw" cx="292" cy="22" r="3.5"/> 757 <circle class="tape-screw" cx="22" cy="178" r="3.5"/> 758 <circle class="tape-screw" cx="292" cy="178" r="3.5"/> 759 <rect class="tape-slot" x="128" y="168" width="26" height="14" rx="2"/> 760 <rect class="tape-slot" x="160" y="168" width="26" height="14" rx="2"/> 761 <rect class="tape-slot tape-slot--capstan" x="98" y="170" width="10" height="12" rx="2"/> 762 <rect class="tape-slot tape-slot--capstan" x="206" y="170" width="10" height="12" rx="2"/> 763 </svg> 764 <div class="tape-label"> 765 <p class="tape-kind">Mixtape</p> 735 766 <h3 class="tape-title">${titleH}</h3> 736 767 ${artistH ? `<p class="tape-artist">${artistH}</p>` : ''} -
src/views/shell.ejs
r16fd4fe re679bce 222 222 223 223 <!-- v9 stylesheet (full palette system) --> 224 <link rel="stylesheet" href="/assets/css/style.css?v=10 6">224 <link rel="stylesheet" href="/assets/css/style.css?v=107"> 225 225 <script> 226 226 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in … … 406 406 ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat 407 407 Cloudflare (max-age=1y) niet de oude versie blijft serveren. --> 408 <script src="/assets/js/audio-player.js?v=3 2"></script>408 <script src="/assets/js/audio-player.js?v=35"></script> 409 409 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte 410 410 player-API's + gedeelde mutual-exclusion registry met de site-speler. --> … … 505 505 // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig 506 506 // bumpen kost een bugfix die nooit aankomt. 507 var MOD_V = 5 2;507 var MOD_V = 53; 508 508 509 509 // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)