Changeset 19e28ce in Klonkt for src/assets/js/mod/read.js
- Timestamp:
- 09/30/2026 07:38:15 AM (3 hours ago)
- Branches:
- main
- Children:
- ca260d1
- Parents:
- fd8faf5
- File:
-
- 1 edited
-
src/assets/js/mod/read.js (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/read.js
rfd8faf5 r19e28ce 7 7 * weg, en dat is winst: Lezen is nu een WEERGAVE van de feed 8 8 * (body[data-feed-view="reader"]), naast Tijdlijn en Grid. De feed levert de 9 * berichten al, "meer laden" vult al aan, en het snappen naar berichtgrenzen 10 * doet CSS. 11 * 12 * HET SNAPPEN STAAT HIER MET OPZET NIET IN. Ik heb dat een ronde lang wel 13 * geprobeerd -- richting bijhouden, een vangzone uitrekenen, per scroll-event 14 * het snappunt verzetten -- en dat is de verkeerde laag. Robins bezwaar (20-8): 15 * "scroll-snap op het element is iets anders dan touch events, 16 * requestAnimationFrame etc. dan gaan we te veel van de view doen." Klopt, en 17 * het vocht ook met de browser: de scroll-events bevatten OOK de bewegingen van 18 * zijn eigen snap-animatie, dus de richting die je eruit afleidt is niet die van 19 * de gebruiker. 20 * 21 * Wat er nodig was, was een regel minder in de CSS en niet honderd erbij hier: 22 * zie style.css bij .feed-reader .read-post. 9 * berichten al en "meer laden" vult al aan. 10 * 11 * GEWOON SCROLLEN (Robin, 30-9: "Reader view normaal scrollen, Lenis mag helemaal 12 * uit klonkt"). Hier stond Lenis: op desktop voor zacht scrollen en snappen naar 13 * de bovenkant van een bericht, op mobiel nam hij de vinger over en bootsten we 14 * het snappen na. Dat is er allemaal uit. De browser scrolt, zoals op elke andere 15 * pagina, en er snapt niets meer. Waarom snappen niet native terugkwam, en hoe 16 * dat alsnog zou kunnen, staat in de vault onder "Klonkt Reader Scroll". 23 17 * 24 18 * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk … … 31 25 */ 32 26 33 // VENDOR_V komt uit lib.js: een nummer voor de hele vendormap (shaer-724), 34 // zodat 'ophogen als de bibliotheek wijzigt' op een plek staat. 35 import { VENDOR_V } from './lib.js'; 27 const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)'); 36 28 37 29 // ── Paginamodus ───────────────────────────────────────────────────────────── 38 /**39 * Elk bericht een paneel dat zelf scrollt; tussen berichten ga je met knoppen.40 *41 * Robins voorstel (20-8) nadat het namaken van iOS-momentum niet lukte, en dat42 * is de winst: in dit model DOET het scrollgevoel er niet toe. Er is geen43 * momentum om na te bouwen en geen snap die vangt.44 *45 * Hoe het werkt:46 * - lenis.stop() zet het scrollen van de STROOM stil. Lenis vangt dan wiel en47 * vinger af (hij preventDefault't in onVirtualScroll), dus je kunt niet meer48 * tussen berichten door scrollen.49 * - data-lenis-prevent op elk paneel houdt de scroll BINNEN een bericht50 * ongemoeid. Dat werkt ook terwijl Lenis gestopt is, want die controle staat51 * in zijn bron vóór de gestopt-controle. Nagekeken in de dist, niet gehoopt.52 * - Bewegen tussen panelen gaat met lenis.scrollTo(..., { force: true }), want53 * force is precies de uitzondering die een gestopte Lenis toch laat scrollen.54 *55 * Geldt waar een bericht een eigen scherm heeft: op mobiel altijd, op desktop56 * als de site dat instelt (reader_full_page). Buiten die modus verandert er57 * niets -- dan is Lenis gewoon aan en scrol je vrij.58 */59 30 /** 60 31 * PAGINAMODUS STAAT UIT -- maar de code blijft staan (Robin, 20-8: "weghalen 61 32 * maar bewaren, voor als ik het later weer wil"). 62 33 * 63 * Wat het is: elk bericht een paneel van een scherm dat zelf scrolt, met twee 64 * balken om ertussen te navigeren en de stroomscroll vastgezet. Gebouwd omdat 65 * het namaken van iOS-momentum niet lukte; in dit model doet het scrollgevoel er 66 * namelijk niet toe. Mobiel is nu terug op systeemscroll met snap, en dat is wat 67 * er ook stond voordat we dit probeerden. 34 * Wat het is: elk bericht een paneel van een scherm dat zelf scrollt, met twee 35 * balken om ertussen te navigeren. Gebouwd omdat het namaken van iOS-momentum 36 * niet lukte; in dit model doet het scrollgevoel er namelijk niet toe. 37 * 38 * Sinds Lenis weg is (30-9) zet de CSS de stroom vast in plaats van lenis.stop() 39 * (html:has(body.is-paged) in style.css), en gaan de balken met native 40 * scrollIntoView. Niet uitgeprobeerd sinds die omzetting, want de modus staat 41 * uit: wie hem aanzet, kijkt dat eerst na. 68 42 * 69 43 * Aanzetten: deze constante op true. Dan komen de balken terug (CSS hangt aan 70 * body.is-paged) , stopt Lenis de stroomscrollen scrollen de panelen zelf.44 * body.is-paged) en scrollen de panelen zelf. 71 45 */ 72 46 const PAGINAMODUS = false; … … 93 67 const doel = P[Math.max(0, Math.min(P.length - 1, i))]; 94 68 if (!doel) return; 95 if (lenis) lenis.scrollTo(doel, { force: true }); 96 else doel.scrollIntoView({ behavior: 'smooth', block: 'start' }); 69 doel.scrollIntoView({ behavior: 'smooth', block: 'start' }); 97 70 setTimeout(zetBalken, 80); 98 71 } … … 101 74 * Omhoog vanaf het EERSTE bericht brengt je naar de header. 102 75 * 103 * In paginamodus s taat Lenis stil, dus de pagina scrolt niet meer met je vinger104 * -- en dan is alles boven het eerste bericht onbereikbaar. Robin liep daar105 * tegenaan (20-8): "ik kan niet meer terug scrollen naar de header". De106 * omhoog-knop is daar de enige wegnaartoe, dus die krijgt er een trede bij.76 * In paginamodus scrolt de stroom niet met je vinger, dus dan is alles boven het 77 * eerste bericht onbereikbaar. Robin liep daar tegenaan (20-8): "ik kan niet 78 * meer terug scrollen naar de header". De omhoog-knop is daar de enige weg 79 * naartoe, dus die krijgt er een trede bij. 107 80 */ 108 81 function gaOmhoog() { 109 82 const i = huidigIndex(); 110 83 if (i > 0) { gaNaar(i - 1); return; } 111 if (lenis) lenis.scrollTo(0, { force: true }); 112 else window.scrollTo({ top: 0, behavior: 'smooth' }); 84 window.scrollTo({ top: 0, behavior: 'smooth' }); 113 85 } 114 86 … … 162 134 163 135 function pasPaginaModusToe() { 164 const aan = paginaModus(); 165 document.body.classList.toggle('is-paged', aan); 166 panelen().forEach((a) => { 167 if (aan) a.setAttribute('data-lenis-prevent', ''); 168 else a.removeAttribute('data-lenis-prevent'); 169 }); 170 if (lenis) { if (aan) lenis.stop(); else lenis.start(); } 136 document.body.classList.toggle('is-paged', paginaModus()); 171 137 zetBalken(); 172 138 } 173 139 174 140 /** 175 * Terug naar boven, en bewust NIET window.scrollTo(0).141 * Terug naar boven, en bewust NIET meteen window.scrollTo(0). 176 142 * 177 143 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een … … 220 186 } 221 187 222 /**223 * Omhoog scrollen blijft VRIJ (Robin, 20-8).224 *225 * Snappen hoort bij doorlezen; ga je terug, dan zoek je iets en bepaal je zelf226 * waar je stopt. CSS kent geen richtingsgevoelig snappen, dus dat ene stukje227 * moet hier -- maar dan ook niet meer dan dat: we zetten de CSS-functie aan of228 * uit op de scroller. Geen vangzones, geen snappunten per bericht, geen229 * scrollpositie-boekhouding.230 *231 * DE RICHTING KOMT UIT DE INVOER, niet uit scroll-events. Gemeten op dev: die232 * events bevatten ook de bewegingen van de browser zelf -- zijn snap-animatie en233 * de rubber-band -- en die gaan soms omhoog. Wie daaruit de richting afleidt,234 * leest de browser en niet de gebruiker.235 *236 * EN ER WORDT ALLEEN GESCHAKELD BIJ STILSTAND. Dat is de tweede les, en die237 * kostte een ronde. Eerst zette dit de schakelaar om bij ELK wiel-event en ELKE238 * vingerbeweging, dus binnen een veeg klapte hij meerdere keren heen en weer.239 * De browser raadpleegt scroll-snap-type alleen aan het eind van een gebaar of240 * van de uitloop, en of je daar net vóór of net ná zit bepaalt dan of er241 * gesnapt wordt. Gemeten (Robins melding "soms triggert het terwijl we nog aan242 * het doorscrollen zijn"):243 *244 * snappen UIT, beweging naar 1459 loopt245 * -> halverwege snappen AAN gezet246 * -> eindigt op 1459, NIET op een berichtgrens247 *248 * Bij stilstand omzetten is wel onschuldig: dezelfde proef gaf 0px sprong.249 * Vandaar: richting bepalen bij het BEGIN van een gebaar, en daarna niets meer250 * aanraken tot de scroll echt stil is.251 *252 * Wat je daarvoor inlevert: binnen een veeg ligt de stand vast. Draai je253 * halverwege om zonder los te laten, dan geldt de stand van dat gebaar nog. Een254 * besluit per gebaar is voorspelbaar; het omklappen halverwege was het probleem.255 */256 const RUST_MS = 120;257 258 let bezig = false; // loopt er een gebaar of een uitloop?259 let ooitGescrold = false; // snappen begint UIT, zie zetSnappen()260 let rustTimer = null;261 262 /**263 * LENIS, en alleen op desktop.264 *265 * Op touch doet Lenis van zichzelf niets (syncTouch staat standaard uit) en is266 * het systeem-scrollen al soepel -- daar blijft de native CSS-snap staan die267 * hierboven beschreven is. Robins keuze (20-8): "enkel voor desktop, dat is268 * prima, logisch dat het niet op mobiel kan".269 *270 * WAAROM LENIS HIER STAAT, en dat is niet het vloeiende scrollen: de DUUR van271 * een snap is met native scroll-snap niet in te stellen -- die zit in de browser.272 * Lenis' snap-pakket wel: duration, easing, distanceThreshold en debounce zijn273 * allemaal van ons. Dat was de aanleiding.274 *275 * Het vloeiende scrollen (smoothWheel) kwam er daarna bij, en dat is de kant die276 * OPPASSEN vraagt. Een Mac-trackpad heeft zijn EIGEN momentum, en Lenis'277 * demping komt daar bovenop -- dubbel gedempt voelt drijverig. Vandaar lerp 0.2278 * in plaats van de standaard 0.1. Robin vond dat op 20-8 nog steeds te zweverig,279 * dus dat getal is nog niet uit; zie de opmerking bij de instellingen.280 *281 * De stand met `smoothWheel: false` werkte ook, en dan doet Lenis alleen de282 * snap. Dat is de terugvalpositie als het vloeiende scrollen niet bevalt.283 *284 * lenis/snap haakt alleen in op lenis.on('scroll') en roept lenis.scrollTo aan285 * (nagekeken in de dist), dus die opzet werkt.286 */287 const OP_DESKTOP = window.matchMedia('(hover: hover) and (pointer: fine)');288 // PROEF (20-8): Lenis ook op touch. Robin meldde op mobiel drie dingen -- de289 // header niet zichtbaar bij laden, snappen dat soms te ver gaat, en horizontaal290 // kunnen scrollen. De eerste twee komen van de NATIVE snap: die vangt al bij het291 // laden, en zijn uitloop vliegt bij een flick over een grens heen. Lenis zet de292 // native snap uit en doet het zelf, dus die twee kunnen ermee verdwijnen.293 //294 // De prijs is echt: syncTouch betekent dat Lenis het VINGERSCROLLEN overneemt295 // van iOS -- momentum, rubber-band en het wegschuiven van de adresbalk worden296 // dan een benadering in JavaScript. De makers waarschuwen daar zelf voor op297 // iOS < 16. Dit staat er dus als proef, niet als besluit: voelt het niet goed,298 // dan is OP_TOUCH weer false zetten de hele terugweg.299 const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)');300 // VENDOR_V komt uit lib.js: een nummer voor de hele vendormap (shaer-724).301 302 let lenis = null;303 let snap = null;304 305 async function startLenis() {306 // DESKTOP EN TOUCH ALLEBEI. Er heeft hier een tijd gestaan dat mobiel terug was307 // op de systeemscroll; dat klopte niet meer met de regel hieronder, die308 // syncTouch juist AANzet op touch. Reden dat Lenis ook op de telefoon meedraait:309 // zijn snap heeft zijn scroll-gebeurtenissen nodig om te kunnen timen.310 if (lenis || !(OP_DESKTOP.matches || OP_TOUCH.matches)) return;311 const [L, S] = await Promise.all([312 import(`/assets/vendor/lenis.mjs?v=${VENDOR_V}`),313 import(`/assets/vendor/lenis-snap.mjs?v=${VENDOR_V}`),314 ]);315 lenis = new L.default({316 /**317 * Wat Lenis MET RUST LAAT.318 *319 * In paginamodus staat Lenis stil en scrolt alleen het paneel van het320 * bericht zelf. Dat regelde ik eerst met een data-lenis-prevent-attribuut321 * dat de module op elk paneel zette -- maar dan hangt het scrollen af van of322 * dat attribuut op tijd en op elk (ook later bijgeladen) paneel staat, en323 * Robin kon binnen een lang bericht niet scrollen.324 *325 * Deze functie stelt dezelfde vraag zonder die afhankelijkheid: is dit een326 * bericht-paneel? Dan bemoeit Lenis zich er niet mee en scrolt de browser327 * het zelf -- wat op een telefoon precies is wat je wilt, want dat is de328 * systeemscroll.329 */330 prevent: (node) => !!(node && node.classList331 && node.classList.contains('read-post')332 && document.body.classList.contains('is-paged')),333 // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in334 // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en335 // heeft die demping nodig; een Mac-trackpad heeft zijn EIGEN momentum en336 // krijgt er dan een tweede overheen -- dat is precies het drijverige gevoel337 // waar Robin voor waarschuwde. Hoger betekent korter naijlen, dus dit is de338 // middenweg: de schokjes weg, de nasleep kort.339 // Staat het toch te zweven, dan is lerp omhoog (richting 1) of terug naar340 // smoothWheel:false de knop -- die stand werkte ook, met alleen de snap.341 smoothWheel: true,342 lerp: 0.2,343 // Alleen op touch overneemt hij het vingerscrollen; op desktop hoeft dat niet.344 syncTouch: OP_TOUCH.matches,345 // Twee touch-knoppen, en ze doen echt iets anders. Lenis rekent bij het346 // LOSLATEN eenmalig een doel uit -- afstand = snelheid^touchInertiaExponent347 // -- en kruipt daar dan naartoe met syncTouchLerp.348 //349 // touchInertiaExponent hoe VER de flick draagt (standaard 1.7)350 // syncTouchLerp hoe hard hij REMT (standaard 0.075)351 //352 // Let op: `touchInertiaMultiplier` bestaat NIET in 1.3.26. Lenis slikt353 // onbekende opties zonder fout, dus zo'n regel lijkt te werken en doet niets.354 // Controleer een optienaam in src/assets/vendor/lenis.mjs voor je hem zet.355 //356 // Robin wilde langer en sneller uitrollen zonder remgevoel, dus de exponent357 // gaat omhoog (een stevige flick draagt daarmee ruim vier tot zes keer zo358 // ver) en de lerp gaat juist boven de standaard: hij legt die afstand vlot359 // af in plaats van er stroperig naartoe te kruipen.360 ...(OP_TOUCH.matches ? { syncTouchLerp: 0.09, touchInertiaExponent: 2.2 } : {}),361 autoRaf: true,362 });363 snap = new S.default(lenis, {364 type: 'proximity',365 // DE VANGZONE, en dit is HET getal om aan te draaien. Ooit 12%, toen 55%366 // (20-8: eerder vangen, boven de reacties), en 20-8 terug naar 12% -- want367 // sinds het snappen alleen nog vooruit werkt, voelde die brede zone als368 // trekken tijdens het lezen. 12% is ~110 pixels op een venster van 910: hij369 // vangt pas vlak vóór de lijn.370 // Houd dit gelijk aan vangZone() hieronder: deze drempel bepaalt of371 // lenis/snap überhaupt aanklopt, die andere of wij het doorlaten. Zet je372 // deze lager dan die, dan komt onze regel nooit aan bod.373 distanceThreshold: '12%',374 // Hoe lang na de laatste scrollbeweging hij mag vangen. De standaard is 500375 // en dat voelt als te laat. Op touch nog korter dan op desktop, want daar376 // eindigt een veeg in een lange, trage staart -- en juist dan wil je dat de377 // snap er snel bij is in plaats van te wachten tot de laatste pixel stil ligt.378 //379 // LET OP DE SPANNING: een vloeiendere uitloop (lagere syncTouchLerp) maakt380 // die staart langer, en stelt de snap dus uit. Deze twee getallen houden381 // elkaar in evenwicht; draai je aan de een, kijk dan ook naar de ander.382 // OP TOUCH JUIST LANG, en dat is het omgekeerde van wat ik vorige ronde deed.383 // Tussen je vinger loslaten en het op gang komen van de uitloop zit een korte384 // stilte in de scrollbeweging. Met 25ms viel de snap precies in dat gat: hij385 // greep op het moment van loslaten en knipte de veeg af -- wat aanvoelt als386 // "stroef bij het loslaten", en het werd er dan ook erger van. 200ms laat de387 // uitloop eerst zijn werk doen.388 // Met een iOS-achtige uitloop duurt de staart lang; te kort wachten laat de389 // snap midden in de vlucht ingrijpen. 150ms laat hem uitrollen en vangt dan.390 debounce: OP_TOUCH.matches ? 150 : 60,391 // Op touch korter: een telefoon vraagt om directer antwoord dan een muis, en392 // de snap komt daar aan het eind van een lange uitloop -- dan mag hij kort.393 duration: OP_TOUCH.matches ? 0.18 : 0.4,394 // Vlot weg, dan steeds langzamer aankomen (Robin, 20-8). easeOutQuart: op de395 // helft van de tijd is 94% van de weg af, en de rest dempt zacht uit.396 // Bewust NIET Lenis' standaard easeOutExpo -- die schiet weg en kruipt dan397 // zo lang na dat het lijkt of hij niet afmaakt.398 easing: (t) => 1 - Math.pow(1 - t, 4),399 });400 // Het snappunt is de bovenkant van elk bericht -- ook van het laatste. Dat401 // deed eerder niet mee omdat zijn bovenkant onbereikbaar was; sinds het402 // laatste bericht in CSS een volle schermhoogte krijgt, kan hij wel.403 const berichten = [...document.querySelectorAll('.feed-reader .read-post')];404 berichten.forEach((a) => snap.addElement(a, { align: 'start' }));405 406 // ALLEEN VOORUIT VANGEN. Dit hoort bij de ruime zone hierboven en is niet407 // optioneel: lenis/snap kiest het DICHTSTBIJZIJNDE punt en zijn drempel geldt408 // naar twee kanten. Scroll je een lang bericht in en stop je 300 pixels onder409 // de bovenkant, dan is die bovenkant het dichtstbij -- en met een zone van 400410 // wordt je teruggetrokken. Dat is precies de klacht "ik kan niet scrollen411 // binnen een lang bericht", en met de oude zone van 86 pixels viel het alleen412 // niet op.413 //414 // Dus: een doel ACHTER je slaan we over. Ligt er een punt voor je binnen de415 // zone, dan gaan we daarheen; anders gebeurt er niets en scrol je vrij door.416 // Zo verruimt de zone alleen de kant waar hij bedoeld is.417 //418 // Waarom niet gewoon de drempel? Omdat lenis/snap alleen de dichtstbijzijnde419 // kandidaat beoordeelt: zonder deze omleiding houdt de zone vooruit op bij de420 // helft van de afstand tussen twee berichten, hoe groot je de drempel ook zet.421 //422 // DE REGEL, in Robins woorden (20-8): "enkel bij downscrollen, aan de423 // onderkant van elke post, snappen naar de lijn tussen de posts" -- en geen424 // snap op een bericht dat al voorbij is gescrold. Drie voorwaarden dus:425 // 1. de laatste echte beweging ging omlaag,426 // 2. het doel ligt VOOR je (een punt achter je slaan we over),427 // 3. het ligt binnen de vangzone -- die begint rond de voet van het bericht.428 // Het doel zelf blijft de bovenkant van het volgende bericht: dat IS de lijn429 // ertussen, en dat is het essentiële.430 //431 // DE ZONE IS EEN GETAL, geen '55%'. Die string kwam ongewijzigd uit de opties432 // en werd hier met een getal vergeleken -- altijd onwaar, dus deze terugval433 // heeft nooit gewerkt en alleen het doel dat lenis zelf koos kwam erdoor.434 const vangZone = () => 0.12 * window.innerHeight; // gelijk aan distanceThreshold hierboven435 // Lenis' eigen `direction` is het teken van de snelheid, en die is bij het436 // afvuren van de (gedebouncede) snap alweer nul. Daarom onthouden we de437 // laatste richting die er echt was.438 let laatsteRichtingOmlaag = true;439 lenis.on('scroll', () => {440 if (Math.abs(lenis.velocity) > 0.05) laatsteRichtingOmlaag = lenis.velocity > 0;441 });442 const echtGaNaar = snap.goTo.bind(snap);443 snap.goTo = (index) => {444 if (!laatsteRichtingOmlaag) return; // omhoog: nooit vangen445 const punten = snap.computeSnaps(); // zelfde volgorde als goTo intern gebruikt446 const nu = lenis.scroll;447 let vooruit = -1;448 punten.forEach((punt, i) => {449 if (punt.value > nu + 2 && (vooruit < 0 || punt.value < punten[vooruit].value)) vooruit = i;450 });451 if (vooruit < 0) return; // niets meer voor je: vrij uitscrollen452 if (punten[vooruit].value - nu <= vangZone()) echtGaNaar(vooruit);453 };454 455 // OP TOUCH DOET DE MODULE-EIGEN TRIGGER HET NIET, en dat is meetbaar in de456 // vendor-bron, geen vermoeden. Drie feiten op een rij:457 //458 // 1. lenis geeft 'virtual-scroll' door met de RUWE vingerdelta, VOOR hij459 // de uitloop uitrekent (teken * |snelheid|^touchInertiaExponent).460 // 2. lenis-snap negeert elke touchmove en beoordeelt dus EEN keer per461 // gebaar, op touchend, met `scroll + rawDelta` -- de plek waar je462 // vinger LOSLIET.463 // 3. de uitloop draagt daarna nog honderden pixels verder.464 //465 // De snapbeslissing valt dus op een positie die de scroll meteen verlaat.466 // Op desktop klopt dezelfde som wel: wieldelta's zijn klein en de467 // smoothWheel-uitloop is kort, dus voorspelling en landing liggen bijeen.468 // Vandaar: op touch de module-trigger eraf en zelf beoordelen tegen469 // lenis.targetScroll -- die wordt synchroon bij touchend gezet en IS het470 // exacte landingspunt van de uitloop.471 if (OP_TOUCH.matches) {472 // Hoe lang het richten naar de lijn duurt, in seconden. De ene knop voor473 // "snapperiger": 0.18 is direct, 0.3 is landen, 0.5 is zweven.474 const SNAP_DUUR = 0.22;475 lenis.off('virtual-scroll', snap.onSnapDebounced);476 let raakTimer = null;477 let vertrek = 0; // scrollpositie bij loslaten: punten daarachter zijn478 // de bovenkant van het HUIDIGE bericht, nooit grijpen479 lenis.on('virtual-scroll', (e) => {480 const soort = e.event && e.event.type;481 if (soort === 'touchstart' || soort === 'touchmove') { clearTimeout(raakTimer); return; }482 if (soort !== 'touchend') return;483 vertrek = lenis.scroll;484 clearTimeout(raakTimer);485 // TIMEOUT 0, geen 150ms. Dit event komt binnen VOOR lenis zelf de486 // touchend verwerkt (de emit staat in de bron boven de verwerking), dus487 // targetScroll is hier nog het oude doel. Een tik later is de uitloop488 // gestart en staat targetScroll op de echte landing -- en dan sturen we489 // hem meteen bij, in plaats van de uitloop 150ms te laten lopen en er490 // dan een aparte snap-animatie overheen te zetten.491 raakTimer = setTimeout(() => {492 if (!laatsteRichtingOmlaag || snap.isStopped) return;493 const doel = lenis.targetScroll; // de echte landing, niet de raakdelta494 const punten = snap.computeSnaps();495 // De EERSTE grens voorbij het loslaatpunt, niet de dichtstbijzijnde bij496 // de landing. Robins regel (24-8): wordt het target voorbijgescrolld,497 // dan landen we alsnog OP het target. De zone werkt dus alleen aan de498 // korte kant -- kom je er niet eens bij in de buurt, dan lees je gewoon499 // binnen een lang bericht en blijft alles vrij. Eroverheen, hoe ver500 // ook, betekent grijpen. Een harde veeg komt daarmee altijd precies501 // een bericht verder, nooit twee: dat is het bladeren.502 let eerste = -1;503 punten.forEach((punt, i) => {504 if (punt.value > vertrek + 2 && (eerste < 0 || punt.value < punten[eerste].value)) eerste = i;505 });506 if (eerste < 0) return; // niets meer voor je: vrij uitscrollen507 // HET RICHTEN IS DE SNAP (Robins regel, 24-8): de lopende uitloop508 // krijgt het nieuwe doel, er komt geen tweede animatie overheen.509 //510 // MET EEN VASTE DUUR, niet met de lerp van de uitloop. Die lerp-vorm511 // (0.09) is boterzacht maar heeft een exponentiele staart van ruim een512 // halve seconde -- Robin wilde het korter en beslister (24-8). Een513 // duur eindigt ECHT, op een instelbaar moment, zoals iOS-paging: vlot514 // weg, stevig landen. Zelfde easing-familie als de desktop-snap.515 // SNAP_DUUR is de enige knop: korter is snapperiger, langer is zachter.516 if (doel >= punten[eerste].value - vangZone()) {517 lenis.scrollTo(punten[eerste].value, {518 duration: SNAP_DUUR,519 easing: (t) => 1 - Math.pow(1 - t, 4),520 force: true,521 userData: { initiator: 'snap' },522 });523 }524 }, 0);525 });526 }527 528 // Native snappen uit: twee mechanismen op dezelfde scroller vechten.529 document.documentElement.style.scrollSnapType = 'none';530 }531 532 function stopLenis() {533 if (snap) { snap.destroy(); snap = null; }534 if (lenis) { lenis.destroy(); lenis = null; }535 document.documentElement.style.scrollSnapType = '';536 }537 538 /**539 * Omhoog niet snappen. Met Lenis is dat snap.stop()/start(); zonder Lenis (dus540 * op touch) zetten we de CSS-eigenschap om, precies zoals hiervoor.541 */542 function zetSnappen(aan) {543 if (snap) { if (aan) snap.start(); else snap.stop(); return; }544 // Zonder Lenis (mobiel) zetten we de CSS-eigenschap om. Hij staat bij het545 // laden UIT -- dat is de standaardstand van deze variabele -- zodat de browser546 // niet meteen naar het eerste bericht springt en de header wegvalt. Pas je547 // eerste gebaar naar beneden zet hem aan.548 const el = document.documentElement;549 const wil = aan ? '' : 'none';550 if (el.style.scrollSnapType !== wil) el.style.scrollSnapType = wil;551 }552 553 /** Een richting geldt alleen als er NIETS beweegt. Anders negeren we hem. */554 function nieuwGebaar(naarBeneden) {555 if (bezig) return;556 bezig = true;557 zetSnappen(naarBeneden);558 }559 560 /**561 * Het gebaar is pas voorbij als de SCROLL stil is, niet als de vinger loslaat:562 * op iOS loopt de uitloop daarna nog door. `scrollend` zegt dat precies, maar563 * bestaat niet overal (Chrome 114+, Safari 17+) -- vandaar ook de timer.564 */565 function rustNu() { bezig = false; }566 function planRust() {567 clearTimeout(rustTimer);568 rustTimer = setTimeout(rustNu, RUST_MS);569 }570 571 function opWiel(e) { if (Math.abs(e.deltaY) > 1) nieuwGebaar(e.deltaY > 0); }572 let raakY = 0;573 function opRaakStart(e) { if (e.touches && e.touches[0]) raakY = e.touches[0].clientY; }574 function opRaakBeweeg(e) {575 if (!e.touches || !e.touches[0]) return;576 const y = e.touches[0].clientY;577 // Vinger omhoog = inhoud omlaag. Drie pixels speling tegen de trilling van een578 // duim die stilstaat.579 if (Math.abs(y - raakY) > 3) { nieuwGebaar(y < raakY); raakY = y; }580 }581 function opToets(e) {582 if (['ArrowDown', 'PageDown', 'End', ' ', 'Spacebar'].indexOf(e.key) >= 0) nieuwGebaar(true);583 else if (['ArrowUp', 'PageUp', 'Home'].indexOf(e.key) >= 0) nieuwGebaar(false);584 }585 586 188 let knop = null; 587 189 let opScroll = null; 588 190 589 191 export function init() { 590 // EERST OPRUIMEN, en pas daarna terugvallen als er geen leesstroom is. 591 // 592 // Dit stond andersom, en dat brak het scrollen op de HELE site: navigeerde je 593 // van Lezen naar het beheer, dan viel init() bij de ontbrekende leesstroom 594 // meteen terug -- en bleef Lenis leven EN gestopt (lenis.stop() vangt wiel en 595 // vinger af). Daarna scrolde niets meer, ook niet op pagina's die met Lezen 596 // niets te maken hebben. Robins melding (20-8): "scrollen werkt nu nergens ook 597 // niet in admin panels". 598 // 599 // Opruimen hoort dus bij het VERLATEN van de weergave, niet bij het opzetten 600 // ervan. init() draait bij elke paginawissel, dus dit is de plek. 601 stopLenis(); 192 // EERST OPRUIMEN, en pas daarna terugvallen als er geen leesstroom is. init() 193 // draait bij elke paginawissel, en wat de leesweergave aanzette hoort weg te 194 // zijn zodra je hem verlaat -- ook op pagina's die met Lezen niets te maken 195 // hebben (Robins melding van 20-8, toen dat misging: "scrollen werkt nu nergens 196 // ook niet in admin panels"). 602 197 document.body.classList.remove('is-paged'); 603 document.querySelectorAll('[data-lenis-prevent].read-post') 604 .forEach((a) => a.removeAttribute('data-lenis-prevent')); 198 // Een tabblad dat nog met de vorige versie van deze module is geopend, heeft 199 // het native snappen inline uitgezet. Na een htmx-wissel naar deze versie hoort 200 // daar niets van over te blijven. 201 document.documentElement.style.scrollSnapType = ''; 605 202 606 203 const s = document.getElementById('read-stream'); … … 625 222 if (opScroll) { 626 223 window.removeEventListener('scroll', opScroll); 627 window.removeEventListener('scrollend', rustNu);628 224 window.removeEventListener('resize', opScroll); 629 window.removeEventListener('wheel', opWiel);630 window.removeEventListener('touchstart', opRaakStart);631 window.removeEventListener('touchmove', opRaakBeweeg);632 window.removeEventListener('keydown', opToets);633 225 } 634 // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar635 // levend; pas als het stil blijft mag een nieuwe richting gelden.636 226 opScroll = () => { 637 227 if (knop) toonKnop(knop); 638 planRust();639 228 if (document.body.classList.contains('is-paged')) zetBalken(); 640 229 }; 641 230 window.addEventListener('scroll', opScroll, { passive: true }); 642 window.addEventListener('scrollend', rustNu);643 231 window.addEventListener('resize', opScroll, { passive: true }); 644 window.addEventListener('wheel', opWiel, { passive: true });645 window.addEventListener('touchstart', opRaakStart, { passive: true });646 window.addEventListener('touchmove', opRaakBeweeg, { passive: true });647 window.addEventListener('keydown', opToets);648 232 649 233 const balkBoven = document.getElementById('read-prev'); … … 652 236 if (balkOnder) balkOnder.onclick = () => gaNaar(huidigIndex() + 1); 653 237 654 // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw 655 // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en 656 // wegnavigeren hoort hem op te ruimen. 657 if (document.body.dataset.feedView === 'reader') startLenis().then(pasPaginaModusToe); 658 else pasPaginaModusToe(); 659 660 238 pasPaginaModusToe(); 661 239 } 662 240
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)