Changeset 19e28ce in Klonkt for src/assets
- Timestamp:
- 09/30/2026 07:38:15 AM (5 hours ago)
- Branches:
- main
- Children:
- ca260d1
- Parents:
- fd8faf5
- Location:
- src/assets
- Files:
-
- 2 deleted
- 2 edited
-
css/style.css (modified) (5 diffs)
-
js/mod/read.js (modified) (8 diffs)
-
vendor/lenis-snap.mjs (deleted)
-
vendor/lenis.mjs (deleted)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
rfd8faf5 r19e28ce 2457 2457 } 2458 2458 2459 /* Snappen doet de SCROLLCONTAINER, en dat is het document -- niet .read-stream, 2460 die heeft overflow: visible en scrollt zelf niet. Proximity en geen mandatory: 2461 in een bericht dat langer is dan het scherm moet je rustig door kunnen lezen. */ 2462 html:has(body.on-home[data-feed-view="reader"]) { 2463 scroll-snap-type: y proximity; 2464 /* GEEN scroll-behavior:smooth hier. Dat maakte elke beweging op deze 2465 scroller traag, ook het snappen zelf -- en een snap moet vlot zijn 2466 (Robin, 20-8: "vlot maar geanimeerd, mag niet te lang duren"). De browser 2467 animeert een snap uit zichzelf al, en korter dan zijn smooth-scroll. 2468 De terug-naar-boven-knop vraagt zijn animatie expliciet aan in 2469 mod/read.js, dus die blijft vloeiend. */ 2470 } 2471 /* En de browser niet óók laten compenseren als Load more er berichten bij zet: 2472 Chrome en Firefox schuiven bij invoegen zelf mee (scroll anchoring), en dat 2473 vecht met het snappen. */ 2459 /* GEWOON SCROLLEN (Robin, 30-9: "Reader view normaal scrollen, Lenis mag 2460 helemaal uit klonkt"). Hier stond scroll-snap-type op het document, en Lenis 2461 deed het snappen erbovenop. Er snapt niets meer: de reader scrolt zoals elke 2462 andere pagina. De .read-anker-punten hieronder staan er nog, maar zonder 2463 snap-container doen ze niets. Terug willen? Zie de vault, "Klonkt Reader 2464 Scroll": native snappen had bekende nadelen, en die staan daar. */ 2465 /* Scroll anchoring uit: als Load more er berichten bij zet, schuiven Chrome en 2466 Firefox anders zelf mee. Stond er voor het snappen, maar ook zonder snappen 2467 hoort wat je leest niet te verspringen. */ 2474 2468 html:has(body.on-home[data-feed-view="reader"]), 2475 2469 .feed-reader .read-stream { overflow-anchor: none; } … … 2561 2555 zichtbare scherm soms de GROTE viewport, en dan komt svh net tekort. 2562 2556 Zichtbaar kost dit niets: berichten hebben geen kader, alleen een scheidings- 2563 lijn erboven, dus het laatste bericht krijgt hooguit witruimte eronder. */ 2564 .feed-reader .read-post:last-child { min-height: 100lvh; } 2557 lijn erboven, dus het laatste bericht krijgt hooguit witruimte eronder. 2558 SINDS 30-9 WEG: zonder snappen hoeft die bovenkant nergens meer heen, en dan is 2559 de volle schermhoogte alleen nog een leeg scherm aan het eind van de stroom. */ 2565 2560 2566 2561 /* ELK BERICHT EEN EIGEN SCHERM, als de site dat wil (Instellingen -> Weergave). … … 2575 2570 Op desktop blijft het een keuze, want daar lees je in een kolom naast veel 2576 2571 witruimte en is een doorlopende stroom vaak prettiger. */ 2577 @media (hover: none) and (pointer: coarse) { 2578 /* Op mobiel draait Lezen in paginamodus (body.is-paged), en die zet hieronder 2579 een vaste hoogte. Deze regel is de terugval voor het moment vóór de module 2580 geladen is, zodat de berichten niet eerst als losse stroom in beeld 2581 springen. */ 2582 .feed-reader .read-post { min-height: 100svh; } 2583 /* Geen native snap op mobiel: dat doet lenis/snap, met een instelbare vangzone 2584 en duur. Twee snapsystemen op dezelfde scroller vechten. */ 2585 html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: none; } 2586 } 2572 /* SINDS 30-9 NIET MEER OP MOBIEL AFGEDWONGEN. Een scherm per bericht hoorde bij 2573 vegen van bericht naar bericht, en dat vegen was het snappen. Zonder snappen 2574 gaf het onder een kort bericht een leeg scherm -- dezelfde leegte waar Robin 2575 op 20-8 op desktop over viel. De instelling hierboven geldt nu overal: wie een 2576 scherm per bericht wil, zet hem aan. */ 2587 2577 2588 2578 /* -------- PAGINAMODUS -------- … … 2590 2580 berichten ga je met knoppen. Daarmee doet het scrollgevoel er niet meer toe: 2591 2581 geen momentum om na te bouwen en geen snap die vangt. 2592 De stroom zelf vangt geen vingers meer -- dat regelt lenis.stop() in 2593 mod/read.js -- en data-lenis-prevent op het paneel houdt de scroll BINNEN een 2594 bericht ongemoeid (die controle staat in Lenis vóór de gestopt-controle). 2582 De stroom zelf scrolt niet: dat deed lenis.stop(), en sinds Lenis weg is 2583 (30-9) de regel hieronder op het document. Het paneel scrolt zelf. 2595 2584 Geldt waar een bericht een eigen scherm heeft: op mobiel altijd, op desktop 2596 2585 als de site dat zo instelt. */ 2586 html:has(body.is-paged) { overflow: hidden; } 2597 2587 body.is-paged .feed-reader .read-post { 2598 2588 height: 100svh; … … 2801 2791 @media (prefers-reduced-motion: reduce) { 2802 2792 html:has(body.on-home[data-feed-view="reader"]) { 2803 scroll-snap-type: none;2804 2793 scroll-behavior: auto; 2805 2794 } -
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)