Changeset e46ebaa in Klonkt for src/assets/js/mod/read.js


Ignore:
Timestamp:
08/20/2026 12:54:30 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
48481cd
Parents:
598f090
Message:

Lezen: snappen dat je laat lezen, en geen lege schermen meer

Barts meldingen van 20 augustus, in een stuk of zes rondes bijgesteld. De
uitkomst is korter dan de weg ernaartoe, en die weg is het waard om vast te
leggen omdat ik er twee keer overheen ben geschoten.

SNAPPEN. De regel is nu in drie zinnen te zeggen:

omhoog scrollen nooit snappen -- je zoekt iets terug en bepaalt zelf

waar je stopt

omlaag, ver weg vrij scrollen
omlaag, laatste
150px van een
bericht vangt, en je landt op de bovenkant van het volgende

Er is geen top-zone: de bovenkant van het bericht waar je IN zit is geen
doelwit meer. Dat was de oorspronkelijke klacht -- je las een paar regels
verder en proximity trok je terug naar de bovenrand, waardoor het leek of
het scrollen vastliep. De grens die wel vangt is de onderkant van je huidige
bericht, en dat is hetzelfde punt als de bovenkant van het volgende.

Onderweg twee doodlopende wegen, allebei door Robin gecorrigeerd:

  • Eerst zette ik de snap UIT voor berichten langer dan het scherm. Op een telefoon is bijna elk bericht net iets langer dan de viewport, dus daar verdween het snappen overal: op 812px hoog kregen berichten van 1009 en zelfs 839 al geen snap meer.
  • Toen probeerde ik de vangzone aan de VOET van het bericht te hangen ("ben je de reacties voorbij"). Bij een kort bericht staat die voet middenin de lege ruimte die min-height maakte, dus de zone begon op een willekeurige plek. Gelukkig ving Robin dat voordat het uitgerold stond.

Het moest dus een getal in pixels zijn, en 150 is dat getal.

GEEN LEGE SCHERMEN MEER. min-height:100svh rekte elk bericht op tot een vol
scherm. Bij een kort bericht gaf dat een halve pagina leegte onder "Replies
and reactions" -- je keek naar niets en moest doorscrollen om te ontdekken
dat er nog iets kwam. Weg dus: berichten hebben hun eigen hoogte (gemeten:
360/1009/839/250 waar het eerst 888/888/888/888 was) en de stroom is gewoon
bericht na bericht. Het snappen hangt aan scroll-snap-align en niet aan die
hoogte, dus dat werkt onveranderd.

VLOEIEND: scroll-behavior:smooth, want zonder dat springt een snap er in een
frame naartoe en voelt het als een hik. Uit bij prefers-reduced-motion.

TERUG NAAR BOVEN, rechtsonder, in twee stappen: eerst naar het begin van DIT
bericht, daarna pas naar de kop van de pagina. Die knop is de enige die naar
een bovenkant mag springen, en hij heeft het snappen daar niet voor nodig --
scrollIntoView stuurt zelf. Nagemeten, juist omdat omhoog-snappen uit staat:
1706 -> 1206, exact de bovenkant.

COVERS nemen de volle kolombreedte met de verhouding intact. De ronde ervoor
haalde het opblazen eruit maar liet kleine afbeeldingen op ware grootte
staan, en dat was als postzegel in een brede kolom te klein.

DE CIRKEL. Lezen blijft daar dood (berichten van anderen), maar een tijdlijn
werkt er juist bij uitstek. Die dwong ik eerst af zodra de site geen
Lezen-site was, en dan kon je in de cirkel geen Grid meer kiezen. Nu is Grid
de landing en staat Tijdlijn ernaast: bij feed_alt_view=timeline altijd, bij
auto op desktop. De cirkel krijgt zijn eigen geheugen terug, met een eigen
sleutel zodat een keuze daar je thuisweergave niet aanraakt.

style.css naar v77 en MOD_V naar 13.

Suite 1165/1165.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/read.js

    r598f090 re46ebaa  
    1717 * cmd/ctrl-klik hoort de browser zelf af te handelen.
    1818 */
     19
     20/**
     21 * Snappen alleen waar het HELPT.
     22 *
     23 * Elk bericht heeft scroll-snap-align:start en min-height:100svh. Voor een kort
     24 * bericht is dat precies goed: het vult het scherm en de volgende klikt netjes
     25 * op zijn plek. Voor een LANG bericht is het een val -- je leest naar beneden en
     26 * de browser trekt je terug naar de bovenrand, waardoor het scrollen lijkt te
     27 * stoppen terwijl je gewoon doorscrollt. Barts melding (20-8).
     28 *
     29 * "Past dit bericht op het scherm?" is een vraag over gemeten hoogte, en die kan
     30 * CSS niet stellen -- vandaar hier. Past het niet, dan gaat de snap eraf en lees
     31 * je ononderbroken door tot het volgende bericht wel weer een snappunt is.
     32 *
     33 * De marge van 4px vangt afrondingsverschillen tussen svh en de echte hoogte;
     34 * zonder die speling wipt een bericht dat toevallig exact past heen en weer.
     35 */
     36/**
     37 * Wanneer is de bovenkant van een bericht een snappunt?
     38 *
     39 * Alleen zolang je er nog NIET voorbij bent. Zit de bovenrand op of onder de
     40 * bovenkant van het scherm, dan kom je er nog aan en mag hij vangen. Is hij
     41 * eenmaal voorbij -- je leest in het bericht -- dan gaat de snap eraf.
     42 *
     43 * Robins formulering (20-8), en die is preciezer dan wat ik er eerst van maakte:
     44 * "in de post zelf mag er nooit gesnapt worden, ook niet bovenin; enkel de
     45 * onderkant mag snappen naar de volgende post, dus daar de bovenkant van".
     46 * Precies dat: de onderkant van bericht N is de bovenkant van N+1, en die is een
     47 * doelwit omdat je hem nadert. De bovenkant van het bericht waar je IN zit is
     48 * dat niet meer, want daar ben je voorbij.
     49 *
     50 * Zonder deze regel trok proximity je terug naar de bovenrand zodra je een paar
     51 * regels verder scrolde, en leek het of het scrollen vastliep.
     52 *
     53 * De 4px is speling voor afronding: tijdens het snappen zelf loopt top naar 0 en
     54 * mag hij niet halverwege afhaken.
     55 */
     56const SPELING = 4;
     57
     58/**
     59 * En snappen doet alleen mee op de weg NAAR BENEDEN (Robin, 20-8).
     60 *
     61 * Omhoog scroll je om iets terug te zoeken, en dan is elke vangst een hindernis:
     62 * je wilt zelf bepalen waar je stopt. Omlaag lees je door, en dan helpt de
     63 * grens juist. Dus bij omhoog gaat de snap er overal af.
     64 *
     65 * De drempel van 2px is er tegen richtingsruis: tijdens een vloeiende snap
     66 * schommelt scrollY een fractie, en zonder speling zou de richting dan heen en
     67 * weer klappen -- precies midden in de beweging die net soepel moest zijn.
     68 */
     69let laatsteY = 0;
     70let omlaag = true;
     71
     72/**
     73 * Hoe DICHT bij de grens hij vangt: de onderste VANGZONE pixels van een bericht.
     74 *
     75 * In pixels, en bewust niet inhoudelijk. Ik probeerde het aan de voet van het
     76 * bericht te hangen ("ben je de reacties voorbij"), maar bij een KORT bericht
     77 * gaat dat mis: min-height rekt zo'n bericht tot een vol scherm, dus de voet
     78 * staat middenin met lege ruimte eronder en de zone begint op een willekeurige
     79 * plek. Robin ving dat (20-8) voordat het uitgerold stond.
     80 *
     81 * Eerst stond dit op 20px en dat was te krap om ooit te vangen. Honderdvijftig
     82 * is ruwweg het laatste stukje van een bericht: kom je daarbinnen tot stilstand,
     83 * dan trekt hij de streep recht naar de bovenkant van het volgende. Stop je
     84 * eerder, dan blijf je gewoon staan.
     85 *
     86 * Eén getal, dus makkelijk bij te stellen als het onder een duim anders voelt.
     87 */
     88const VANGZONE = 150;
     89
     90function ijkEen(art) {
     91  const top = art.getBoundingClientRect().top;
     92  // Omhoog: nooit. Omlaag: alleen een grens die je NADERT en die binnen de zone
     93  // ligt. De ondergrens blijft krap (SPELING): eenmaal voorbij wordt er niet
     94  // teruggetrokken, ook niet over een paar pixels.
     95  const wil = (!omlaag || top < -SPELING || top > VANGZONE) ? 'none' : '';
     96  // Alleen aanraken als het echt verandert: elke stijlwijziging tijdens een
     97  // vloeiende scroll is een kans op een hik, zeker op iOS.
     98  if (art.style.scrollSnapAlign !== wil) art.style.scrollSnapAlign = wil;
     99}
     100
     101function ijkSnappen() {
     102  const y = window.scrollY;
     103  if (Math.abs(y - laatsteY) > 2) omlaag = y > laatsteY;
     104  laatsteY = y;
     105  document.querySelectorAll('.feed-reader .read-post').forEach(ijkEen);
     106}
     107
     108/**
     109 * Terug naar boven, en bewust NIET window.scrollTo(0).
     110 *
     111 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een
     112 * lang stuk zit, en pas daarna naar de kop van de pagina. Twee keer drukken doet
     113 * dus twee verschillende dingen -- dat scheelt op mobiel een halve minuut vegen.
     114 */
     115function naarBoven() {
     116  const zacht = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
     117  const gedrag = zacht ? 'smooth' : 'auto';
     118  const posts = [...document.querySelectorAll('.feed-reader .read-post')];
     119  const huidig = posts.find((a) => {
     120    const r = a.getBoundingClientRect();
     121    return r.top <= 8 && r.bottom > 8;
     122  });
     123  // Sta je al bovenaan dit bericht (of bij het eerste), dan naar de paginakop.
     124  if (huidig && huidig.getBoundingClientRect().top < -8) {
     125    huidig.scrollIntoView({ behavior: gedrag, block: 'start' });
     126    return;
     127  }
     128  window.scrollTo({ top: 0, behavior: gedrag });
     129}
     130
     131/** De knop verschijnt pas als er iets ONDER je ligt om naar terug te keren. */
     132function toonKnop(knop) {
     133  knop.classList.toggle('is-zichtbaar', window.scrollY > window.innerHeight * 0.6);
     134}
    19135
    20136let tapX = 0, tapY = 0;
     
    37153}
    38154
     155let knop = null;
     156let opScroll = null;
     157
    39158export function init() {
    40159  const s = document.getElementById('read-stream');
     
    47166  s.addEventListener('pointerdown', onPointerDown, { passive: true });
    48167  s.addEventListener('click', onTap);
     168
     169  // De knop staat in de HTML (read-top.ejs), zodat hij er ook is zonder deze
     170  // module -- dan doet hij niets, maar hij springt niet in beeld bij het laden.
     171  knop = document.getElementById('read-top');
     172  if (knop) {
     173    knop.onclick = naarBoven;
     174    toonKnop(knop);
     175  }
     176
     177  // Eén luisteraar, en losmaken bij een volgende init(): deze module draait bij
     178  // ELKE paginawissel en anders stapelen ze op.
     179  if (opScroll) {
     180    window.removeEventListener('scroll', opScroll);
     181    window.removeEventListener('resize', opScroll);
     182  }
     183  // Eén keer per frame, niet per scroll-event: scroll vuurt tientallen keren per
     184  // seconde en we raken hier de stijl van elk bericht aan.
     185  let gepland = false;
     186  opScroll = () => {
     187    if (knop) toonKnop(knop);
     188    if (gepland) return;
     189    gepland = true;
     190    requestAnimationFrame(() => { gepland = false; ijkSnappen(); });
     191  };
     192  window.addEventListener('scroll', opScroll, { passive: true });
     193  window.addEventListener('resize', opScroll, { passive: true });
     194  ijkSnappen();   // ook meteen bij binnenkomst, niet pas bij de eerste scroll
    49195}
    50196
Note: See TracChangeset for help on using the changeset viewer.