Changeset 19e28ce in Klonkt for src/assets


Ignore:
Timestamp:
09/30/2026 07:38:15 AM (5 hours ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
ca260d1
Parents:
fd8faf5
Message:

Lezen scrolt gewoon, en Lenis is uit Klonkt

Robin, 30-9: "Reader view normaal scrollen, Lenis mag helemaal uit klonkt."

Lenis stond alleen in de leesweergave: op desktop voor zacht scrollen en
snappen naar de bovenkant van een bericht, op mobiel nam hij de vinger over en
bootste mod/read.js het snappen na. Dat is er allemaal uit, en de browser
scrolt nu zoals op elke andere pagina.

ER SNAPT OOK NIETS MEER, en dat is een keuze. Er lag nog een native route
klaar (scroll-snap-type, alleen bij naar beneden scrollen aangezet), maar die
had precies de problemen waarom Lenis er ooit kwam: bij het laden sprong hij
naar het eerste bericht zodat de header wegviel, en bij een harde veeg vloog hij
over een grens. "Normaal scrollen" is zonder snappen; de vault-notitie "Klonkt
Reader Scroll" zegt hoe het native terug kan als dat toch gewild is.

Wat met het snappen meeging:

  • de richtingsdetectie in read.js (wiel, vinger, toetsen, rust-timer): die bestond alleen om het snappen aan en uit te zetten;
  • de volle schermhoogte op het laatste bericht: die maakte zijn bovenkant bereikbaar als snappunt, en is zonder snappen een leeg scherm aan het eind;
  • de afgedwongen schermhoogte per bericht op mobiel: die hoorde bij vegen van bericht naar bericht, en gaf onder een kort bericht dezelfde leegte waar Robin op 20-8 op desktop over viel. De site-instelling "elk bericht een eigen scherm" geldt nu overal voor wie dat wil.

De bewaarde paginamodus (staat uit) leunde op lenis.stop() om de stroom vast te
zetten; dat doet nu een regel CSS op het document, en de balken gaan met native
scrollIntoView. Niet uitgeprobeerd sinds die omzetting, want de modus staat uit;
dat staat er ook bij.

Weg: src/assets/vendor/lenis.mjs en lenis-snap.mjs (57 KB samen, en twee
netwerkrondes voordat er iets werkte), en data-lenis-prevent op het zoekvak.
Scroll anchoring blijft uit: ook zonder snappen hoort wat je leest niet te
verspringen als Load more er berichten bij zet. init() zet een inline
scroll-snap-type van de vorige versie terug, voor een tabblad dat na een
htmx-wissel met de oude module was begonnen.

MOD_V 67 en style.css v111: anders draait een browser die er al was nog een jaar
de oude read.js met Lenis.

Volledige suite 1292 groen; er leunde geen toets op Lenis of het snappen.

Co-Authored-By: Claude Opus 5.5 <noreply@…>

Location:
src/assets
Files:
2 deleted
2 edited

Legend:

Unmodified
Added
Removed
  • src/assets/css/style.css

    rfd8faf5 r19e28ce  
    24572457}
    24582458
    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. */
    24742468html:has(body.on-home[data-feed-view="reader"]),
    24752469.feed-reader .read-stream { overflow-anchor: none; }
    … …  
    25612555   zichtbare scherm soms de GROTE viewport, en dan komt svh net tekort.
    25622556   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. */
    25652560
    25662561/* ELK BERICHT EEN EIGEN SCHERM, als de site dat wil (Instellingen -> Weergave).
    … …  
    25752570   Op desktop blijft het een keuze, want daar lees je in een kolom naast veel
    25762571   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. */
    25872577
    25882578/* -------- PAGINAMODUS --------
    … …  
    25902580   berichten ga je met knoppen. Daarmee doet het scrollgevoel er niet meer toe:
    25912581   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.
    25952584   Geldt waar een bericht een eigen scherm heeft: op mobiel altijd, op desktop
    25962585   als de site dat zo instelt. */
     2586html:has(body.is-paged) { overflow: hidden; }
    25972587body.is-paged .feed-reader .read-post {
    25982588    height: 100svh;
    … …  
    28012791@media (prefers-reduced-motion: reduce) {
    28022792    html:has(body.on-home[data-feed-view="reader"]) {
    2803         scroll-snap-type: none;
    28042793        scroll-behavior: auto;
    28052794    }
  • src/assets/js/mod/read.js

    rfd8faf5 r19e28ce  
    77 * weg, en dat is winst: Lezen is nu een WEERGAVE van de feed
    88 * (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".
    2317 *
    2418 * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk
    … …  
    3125 */
    3226
    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';
     27const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)');
    3628
    3729// ── 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 dat
    42  * is de winst: in dit model DOET het scrollgevoel er niet toe. Er is geen
    43  * 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 en
    47  *    vinger af (hij preventDefault't in onVirtualScroll), dus je kunt niet meer
    48  *    tussen berichten door scrollen.
    49  *  - data-lenis-prevent op elk paneel houdt de scroll BINNEN een bericht
    50  *    ongemoeid. Dat werkt ook terwijl Lenis gestopt is, want die controle staat
    51  *    in zijn bron vóór de gestopt-controle. Nagekeken in de dist, niet gehoopt.
    52  *  - Bewegen tussen panelen gaat met lenis.scrollTo(..., { force: true }), want
    53  *    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 desktop
    56  * als de site dat instelt (reader_full_page). Buiten die modus verandert er
    57  * niets -- dan is Lenis gewoon aan en scrol je vrij.
    58  */
    5930/**
    6031 * PAGINAMODUS STAAT UIT -- maar de code blijft staan (Robin, 20-8: "weghalen
    6132 * maar bewaren, voor als ik het later weer wil").
    6233 *
    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.
    6842 *
    6943 * Aanzetten: deze constante op true. Dan komen de balken terug (CSS hangt aan
    70  * body.is-paged), stopt Lenis de stroomscroll en scrollen de panelen zelf.
     44 * body.is-paged) en scrollen de panelen zelf.
    7145 */
    7246const PAGINAMODUS = false;
    … …  
    9367  const doel = P[Math.max(0, Math.min(P.length - 1, i))];
    9468  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' });
    9770  setTimeout(zetBalken, 80);
    9871}
    … …  
    10174 * Omhoog vanaf het EERSTE bericht brengt je naar de header.
    10275 *
    103  * In paginamodus staat Lenis stil, dus de pagina scrolt niet meer met je vinger
    104  * -- en dan is alles boven het eerste bericht onbereikbaar. Robin liep daar
    105  * tegenaan (20-8): "ik kan niet meer terug scrollen naar de header". De
    106  * omhoog-knop is daar de enige weg naartoe, 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.
    10780 */
    10881function gaOmhoog() {
    10982  const i = huidigIndex();
    11083  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' });
    11385}
    11486
    … …  
    162134
    163135function 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());
    171137  zetBalken();
    172138}
    173139
    174140/**
    175  * Terug naar boven, en bewust NIET window.scrollTo(0).
     141 * Terug naar boven, en bewust NIET meteen window.scrollTo(0).
    176142 *
    177143 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een
    … …  
    220186}
    221187
    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 zelf
    226  * waar je stopt. CSS kent geen richtingsgevoelig snappen, dus dat ene stukje
    227  * moet hier -- maar dan ook niet meer dan dat: we zetten de CSS-functie aan of
    228  * uit op de scroller. Geen vangzones, geen snappunten per bericht, geen
    229  * scrollpositie-boekhouding.
    230  *
    231  * DE RICHTING KOMT UIT DE INVOER, niet uit scroll-events. Gemeten op dev: die
    232  * events bevatten ook de bewegingen van de browser zelf -- zijn snap-animatie en
    233  * 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 die
    237  * kostte een ronde. Eerst zette dit de schakelaar om bij ELK wiel-event en ELKE
    238  * 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 of
    240  * van de uitloop, en of je daar net vóór of net ná zit bepaalt dan of er
    241  * gesnapt wordt. Gemeten (Robins melding "soms triggert het terwijl we nog aan
    242  * het doorscrollen zijn"):
    243  *
    244  *     snappen UIT, beweging naar 1459 loopt
    245  *     -> halverwege snappen AAN gezet
    246  *     -> eindigt op 1459, NIET op een berichtgrens
    247  *
    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 meer
    250  * aanraken tot de scroll echt stil is.
    251  *
    252  * Wat je daarvoor inlevert: binnen een veeg ligt de stand vast. Draai je
    253  * halverwege om zonder los te laten, dan geldt de stand van dat gebaar nog. Een
    254  * 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 is
    266  * het systeem-scrollen al soepel -- daar blijft de native CSS-snap staan die
    267  * hierboven beschreven is. Robins keuze (20-8): "enkel voor desktop, dat is
    268  * prima, logisch dat het niet op mobiel kan".
    269  *
    270  * WAAROM LENIS HIER STAAT, en dat is niet het vloeiende scrollen: de DUUR van
    271  * 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 zijn
    273  * allemaal van ons. Dat was de aanleiding.
    274  *
    275  * Het vloeiende scrollen (smoothWheel) kwam er daarna bij, en dat is de kant die
    276  * OPPASSEN vraagt. Een Mac-trackpad heeft zijn EIGEN momentum, en Lenis'
    277  * demping komt daar bovenop -- dubbel gedempt voelt drijverig. Vandaar lerp 0.2
    278  * 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 de
    282  * 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 aan
    285  * (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 -- de
    289 // header niet zichtbaar bij laden, snappen dat soms te ver gaat, en horizontaal
    290 // kunnen scrollen. De eerste twee komen van de NATIVE snap: die vangt al bij het
    291 // laden, en zijn uitloop vliegt bij een flick over een grens heen. Lenis zet de
    292 // 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 overneemt
    295 // van iOS -- momentum, rubber-band en het wegschuiven van de adresbalk worden
    296 // dan een benadering in JavaScript. De makers waarschuwen daar zelf voor op
    297 // 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 was
    307   // op de systeemscroll; dat klopte niet meer met de regel hieronder, die
    308   // 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 het
    320      * bericht zelf. Dat regelde ik eerst met een data-lenis-prevent-attribuut
    321      * dat de module op elk paneel zette -- maar dan hangt het scrollen af van of
    322      * dat attribuut op tijd en op elk (ook later bijgeladen) paneel staat, en
    323      * Robin kon binnen een lang bericht niet scrollen.
    324      *
    325      * Deze functie stelt dezelfde vraag zonder die afhankelijkheid: is dit een
    326      * bericht-paneel? Dan bemoeit Lenis zich er niet mee en scrolt de browser
    327      * het zelf -- wat op een telefoon precies is wat je wilt, want dat is de
    328      * systeemscroll.
    329      */
    330     prevent: (node) => !!(node && node.classList
    331       && node.classList.contains('read-post')
    332       && document.body.classList.contains('is-paged')),
    333     // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in
    334     // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en
    335     // heeft die demping nodig; een Mac-trackpad heeft zijn EIGEN momentum en
    336     // krijgt er dan een tweede overheen -- dat is precies het drijverige gevoel
    337     // waar Robin voor waarschuwde. Hoger betekent korter naijlen, dus dit is de
    338     // middenweg: de schokjes weg, de nasleep kort.
    339     // Staat het toch te zweven, dan is lerp omhoog (richting 1) of terug naar
    340     // 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 het
    346     // LOSLATEN eenmalig een doel uit -- afstand = snelheid^touchInertiaExponent
    347     // -- 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 slikt
    353     // 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 exponent
    357     // gaat omhoog (een stevige flick draagt daarmee ruim vier tot zes keer zo
    358     // ver) en de lerp gaat juist boven de standaard: hij legt die afstand vlot
    359     // 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% -- want
    367     // sinds het snappen alleen nog vooruit werkt, voelde die brede zone als
    368     // trekken tijdens het lezen. 12% is ~110 pixels op een venster van 910: hij
    369     // vangt pas vlak vóór de lijn.
    370     // Houd dit gelijk aan vangZone() hieronder: deze drempel bepaalt of
    371     // lenis/snap überhaupt aanklopt, die andere of wij het doorlaten. Zet je
    372     // 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 500
    375     // en dat voelt als te laat. Op touch nog korter dan op desktop, want daar
    376     // eindigt een veeg in een lange, trage staart -- en juist dan wil je dat de
    377     // 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) maakt
    380     // die staart langer, en stelt de snap dus uit. Deze twee getallen houden
    381     // 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 korte
    384     // stilte in de scrollbeweging. Met 25ms viel de snap precies in dat gat: hij
    385     // greep op het moment van loslaten en knipte de veeg af -- wat aanvoelt als
    386     // "stroef bij het loslaten", en het werd er dan ook erger van. 200ms laat de
    387     // uitloop eerst zijn werk doen.
    388     // Met een iOS-achtige uitloop duurt de staart lang; te kort wachten laat de
    389     // 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, en
    392     // 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 de
    395     // 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 dan
    397     // 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. Dat
    401   // deed eerder niet mee omdat zijn bovenkant onbereikbaar was; sinds het
    402   // 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 niet
    407   // optioneel: lenis/snap kiest het DICHTSTBIJZIJNDE punt en zijn drempel geldt
    408   // naar twee kanten. Scroll je een lang bericht in en stop je 300 pixels onder
    409   // de bovenkant, dan is die bovenkant het dichtstbij -- en met een zone van 400
    410   // wordt je teruggetrokken. Dat is precies de klacht "ik kan niet scrollen
    411   // binnen een lang bericht", en met de oude zone van 86 pixels viel het alleen
    412   // niet op.
    413   //
    414   // Dus: een doel ACHTER je slaan we over. Ligt er een punt voor je binnen de
    415   // 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 dichtstbijzijnde
    419   // kandidaat beoordeelt: zonder deze omleiding houdt de zone vooruit op bij de
    420   // 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 de
    423   // onderkant van elke post, snappen naar de lijn tussen de posts" -- en geen
    424   // 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 lijn
    429   // ertussen, en dat is het essentiële.
    430   //
    431   // DE ZONE IS EEN GETAL, geen '55%'. Die string kwam ongewijzigd uit de opties
    432   // en werd hier met een getal vergeleken -- altijd onwaar, dus deze terugval
    433   // heeft nooit gewerkt en alleen het doel dat lenis zelf koos kwam erdoor.
    434   const vangZone = () => 0.12 * window.innerHeight;   // gelijk aan distanceThreshold hierboven
    435   // Lenis' eigen `direction` is het teken van de snelheid, en die is bij het
    436   // afvuren van de (gedebouncede) snap alweer nul. Daarom onthouden we de
    437   // 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 vangen
    445     const punten = snap.computeSnaps();   // zelfde volgorde als goTo intern gebruikt
    446     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 uitscrollen
    452     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 de
    456   // vendor-bron, geen vermoeden. Drie feiten op een rij:
    457   //
    458   //   1. lenis geeft 'virtual-scroll' door met de RUWE vingerdelta, VOOR hij
    459   //      de uitloop uitrekent (teken * |snelheid|^touchInertiaExponent).
    460   //   2. lenis-snap negeert elke touchmove en beoordeelt dus EEN keer per
    461   //      gebaar, op touchend, met `scroll + rawDelta` -- de plek waar je
    462   //      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 de
    467   // smoothWheel-uitloop is kort, dus voorspelling en landing liggen bijeen.
    468   // Vandaar: op touch de module-trigger eraf en zelf beoordelen tegen
    469   // lenis.targetScroll -- die wordt synchroon bij touchend gezet en IS het
    470   // exacte landingspunt van de uitloop.
    471   if (OP_TOUCH.matches) {
    472     // Hoe lang het richten naar de lijn duurt, in seconden. De ene knop voor
    473     // "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 zijn
    478                        // de bovenkant van het HUIDIGE bericht, nooit grijpen
    479     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 de
    486       // touchend verwerkt (de emit staat in de bron boven de verwerking), dus
    487       // targetScroll is hier nog het oude doel. Een tik later is de uitloop
    488       // gestart en staat targetScroll op de echte landing -- en dan sturen we
    489       // hem meteen bij, in plaats van de uitloop 150ms te laten lopen en er
    490       // 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 raakdelta
    494         const punten = snap.computeSnaps();
    495         // De EERSTE grens voorbij het loslaatpunt, niet de dichtstbijzijnde bij
    496         // de landing. Robins regel (24-8): wordt het target voorbijgescrolld,
    497         // dan landen we alsnog OP het target. De zone werkt dus alleen aan de
    498         // korte kant -- kom je er niet eens bij in de buurt, dan lees je gewoon
    499         // binnen een lang bericht en blijft alles vrij. Eroverheen, hoe ver
    500         // ook, betekent grijpen. Een harde veeg komt daarmee altijd precies
    501         // 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 uitscrollen
    507         // HET RICHTEN IS DE SNAP (Robins regel, 24-8): de lopende uitloop
    508         // 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-vorm
    511         // (0.09) is boterzacht maar heeft een exponentiele staart van ruim een
    512         // halve seconde -- Robin wilde het korter en beslister (24-8). Een
    513         // duur eindigt ECHT, op een instelbaar moment, zoals iOS-paging: vlot
    514         // 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 (dus
    540  * 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 het
    545   // laden UIT -- dat is de standaardstand van deze variabele -- zodat de browser
    546   // niet meteen naar het eerste bericht springt en de header wegvalt. Pas je
    547   // 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, maar
    563  * 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 een
    578   // 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 
    586188let knop = null;
    587189let opScroll = null;
    588190
    589191export 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").
    602197  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 = '';
    605202
    606203  const s = document.getElementById('read-stream');
    … …  
    625222  if (opScroll) {
    626223    window.removeEventListener('scroll', opScroll);
    627     window.removeEventListener('scrollend', rustNu);
    628224    window.removeEventListener('resize', opScroll);
    629     window.removeEventListener('wheel', opWiel);
    630     window.removeEventListener('touchstart', opRaakStart);
    631     window.removeEventListener('touchmove', opRaakBeweeg);
    632     window.removeEventListener('keydown', opToets);
    633225  }
    634   // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar
    635   // levend; pas als het stil blijft mag een nieuwe richting gelden.
    636226  opScroll = () => {
    637227    if (knop) toonKnop(knop);
    638     planRust();
    639228    if (document.body.classList.contains('is-paged')) zetBalken();
    640229  };
    641230  window.addEventListener('scroll', opScroll, { passive: true });
    642   window.addEventListener('scrollend', rustNu);
    643231  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);
    648232
    649233  const balkBoven = document.getElementById('read-prev');
    … …  
    652236  if (balkOnder) balkOnder.onclick = () => gaNaar(huidigIndex() + 1);
    653237
    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();
    661239}
    662240
Note: See TracChangeset for help on using the changeset viewer.