Changeset 4989294 in Klonkt
- Timestamp:
- 08/20/2026 01:41:27 AM (3 weeks ago)
- Branches:
- main
- Children:
- d8f8f07
- Parents:
- 48481cd
- git-author:
- Robin <roboburr@…> (08/20/2026 01:19:43 AM)
- git-committer:
- Robin <roboburr@…> (08/20/2026 01:41:27 AM)
- Location:
- src
- Files:
-
- 3 edited
-
assets/css/style.css (modified) (1 diff)
-
assets/js/mod/read.js (modified) (5 diffs)
-
views/shell.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r48481cd r4989294 2462 2462 html:has(body.on-home[data-feed-view="reader"]) { 2463 2463 scroll-snap-type: y proximity; 2464 /* Vloeiend, niet in een klap. Zonder dit springt een snap er in EEN frame2465 naartoe en voelt het als een hik in plaats van als meegeven -- Barts2466 melding (20-8). Geldt ook voor de terug-naar-boven-knop en voor een2467 an kersprong, en dat is precies wat je wilt.2468 Uitgezet bij prefers-reduced-motion, onderaan dit blok. */2469 scroll-behavior: smooth;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 2470 } 2471 2471 /* En de browser niet óók laten compenseren als Load more er berichten bij zet: -
src/assets/js/mod/read.js
r48481cd r4989294 1 1 /** 2 * De leesweergave: tikken op een bericht opent dat bericht. 2 * De leesweergave: tikken op een bericht opent dat bericht, en een knop om terug 3 * naar boven te gaan. 3 4 * 4 5 * Dit was een heel scherm met een eigen route, dat zijn buren zelf ophaalde, de 5 6 * scrollpositie corrigeerde bij invoegen en de balken wegschoof. Dat is allemaal 6 * weg, en dat is winst: Lezen is nu een DERDEWEERGAVE van de feed7 * weg, en dat is winst: Lezen is nu een WEERGAVE van de feed 7 8 * (body[data-feed-view="reader"]), naast Tijdlijn en Grid. De feed levert de 8 9 * berichten al, "meer laden" vult al aan, en het snappen naar berichtgrenzen 9 * doet CSS. Wat overblijft is dit ene gemak. 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. 10 23 * 11 24 * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk 12 * voor toetsenbord en schermlezer; dit hier is er voor een duim. 13 * 14 * Vier uitzonderingen, want een tik die je niet bedoelde is erger dan geen tik: 15 * iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen werking, 16 * een geselecteerde tekst is geen tik, een verschoven vinger is scrollen, en 17 * cmd/ctrl-klik hoort de browser zelf af te handelen. 18 */ 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 */ 56 const 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 */ 69 let laatsteY = 0; 70 let 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 */ 88 const VANGZONE = 150; 89 90 function 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 101 function 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 } 25 * voor toetsenbord en schermlezer; de tik hieronder is er voor een duim. 26 * 27 * Vier uitzonderingen op die tik, want een tik die je niet bedoelde is erger dan 28 * geen tik: iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen 29 * werking, een geselecteerde tekst is geen tik, een verschoven vinger is 30 * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen. 31 */ 107 32 108 33 /** … … 111 36 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een 112 37 * 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 h alve minuutvegen.38 * dus twee verschillende dingen -- dat scheelt op mobiel een hoop vegen. 114 39 */ 115 40 function naarBoven() { … … 153 78 } 154 79 80 /** 81 * Omhoog scrollen blijft VRIJ (Robin, 20-8). 82 * 83 * Snappen hoort bij doorlezen; ga je terug, dan zoek je iets en bepaal je zelf 84 * waar je stopt. CSS kent geen richtingsgevoelig snappen, dus dat ene stukje 85 * moet hier -- maar dan ook niet meer dan dat: we zetten de CSS-functie aan of 86 * uit op de scroller. Geen vangzones, geen snappunten per bericht, geen 87 * scrollpositie-boekhouding. 88 * 89 * DE RICHTING KOMT UIT DE INVOER, niet uit scroll-events. Gemeten op dev: die 90 * events bevatten ook de bewegingen van de browser zelf -- zijn snap-animatie en 91 * de rubber-band -- en die gaan soms omhoog. Wie daaruit de richting afleidt, 92 * leest de browser en niet de gebruiker. 93 * 94 * EN ER WORDT ALLEEN GESCHAKELD BIJ STILSTAND. Dat is de tweede les, en die 95 * kostte een ronde. Eerst zette dit de schakelaar om bij ELK wiel-event en ELKE 96 * vingerbeweging, dus binnen een veeg klapte hij meerdere keren heen en weer. 97 * De browser raadpleegt scroll-snap-type alleen aan het eind van een gebaar of 98 * van de uitloop, en of je daar net vóór of net ná zit bepaalt dan of er 99 * gesnapt wordt. Gemeten (Robins melding "soms triggert het terwijl we nog aan 100 * het doorscrollen zijn"): 101 * 102 * snappen UIT, beweging naar 1459 loopt 103 * -> halverwege snappen AAN gezet 104 * -> eindigt op 1459, NIET op een berichtgrens 105 * 106 * Bij stilstand omzetten is wel onschuldig: dezelfde proef gaf 0px sprong. 107 * Vandaar: richting bepalen bij het BEGIN van een gebaar, en daarna niets meer 108 * aanraken tot de scroll echt stil is. 109 * 110 * Wat je daarvoor inlevert: binnen een veeg ligt de stand vast. Draai je 111 * halverwege om zonder los te laten, dan geldt de stand van dat gebaar nog. Een 112 * besluit per gebaar is voorspelbaar; het omklappen halverwege was het probleem. 113 */ 114 const RUST_MS = 120; 115 116 let bezig = false; // loopt er een gebaar of een uitloop? 117 let rustTimer = null; 118 119 function zetSnappen(aan) { 120 const el = document.documentElement; 121 const wil = aan ? '' : 'none'; 122 if (el.style.scrollSnapType !== wil) el.style.scrollSnapType = wil; 123 } 124 125 /** Een richting geldt alleen als er NIETS beweegt. Anders negeren we hem. */ 126 function nieuwGebaar(naarBeneden) { 127 if (bezig) return; 128 bezig = true; 129 zetSnappen(naarBeneden); 130 } 131 132 /** 133 * Het gebaar is pas voorbij als de SCROLL stil is, niet als de vinger loslaat: 134 * op iOS loopt de uitloop daarna nog door. `scrollend` zegt dat precies, maar 135 * bestaat niet overal (Chrome 114+, Safari 17+) -- vandaar ook de timer. 136 */ 137 function rustNu() { bezig = false; } 138 function planRust() { 139 clearTimeout(rustTimer); 140 rustTimer = setTimeout(rustNu, RUST_MS); 141 } 142 143 function opWiel(e) { if (Math.abs(e.deltaY) > 1) nieuwGebaar(e.deltaY > 0); } 144 let raakY = 0; 145 function opRaakStart(e) { if (e.touches && e.touches[0]) raakY = e.touches[0].clientY; } 146 function opRaakBeweeg(e) { 147 if (!e.touches || !e.touches[0]) return; 148 const y = e.touches[0].clientY; 149 // Vinger omhoog = inhoud omlaag. Drie pixels speling tegen de trilling van een 150 // duim die stilstaat. 151 if (Math.abs(y - raakY) > 3) { nieuwGebaar(y < raakY); raakY = y; } 152 } 153 function opToets(e) { 154 if (['ArrowDown', 'PageDown', 'End', ' ', 'Spacebar'].indexOf(e.key) >= 0) nieuwGebaar(true); 155 else if (['ArrowUp', 'PageUp', 'Home'].indexOf(e.key) >= 0) nieuwGebaar(false); 156 } 157 155 158 let knop = null; 156 159 let opScroll = null; … … 167 170 s.addEventListener('click', onTap); 168 171 169 // De knop staat in de HTML (read-top.ejs), zodat hij er ook is zonder deze170 // module -- dan doethij niets, maar hij springt niet in beeld bij het laden.172 // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet 173 // hij niets, maar hij springt niet in beeld bij het laden. 171 174 knop = document.getElementById('read-top'); 172 175 if (knop) { … … 175 178 } 176 179 177 // Eén luisteraar, en losmaken bij een volgende init(): deze module draait bij178 // ELKE paginawissel en anders stapelen ze op.179 180 if (opScroll) { 180 181 window.removeEventListener('scroll', opScroll); 182 window.removeEventListener('scrollend', rustNu); 181 183 window.removeEventListener('resize', opScroll); 184 window.removeEventListener('wheel', opWiel); 185 window.removeEventListener('touchstart', opRaakStart); 186 window.removeEventListener('touchmove', opRaakBeweeg); 187 window.removeEventListener('keydown', opToets); 182 188 } 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 }; 189 // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar 190 // levend; pas als het stil blijft mag een nieuwe richting gelden. 191 opScroll = () => { if (knop) toonKnop(knop); planRust(); }; 192 192 window.addEventListener('scroll', opScroll, { passive: true }); 193 window.addEventListener('scrollend', rustNu); 193 194 window.addEventListener('resize', opScroll, { passive: true }); 194 ijkSnappen(); // ook meteen bij binnenkomst, niet pas bij de eerste scroll 195 window.addEventListener('wheel', opWiel, { passive: true }); 196 window.addEventListener('touchstart', opRaakStart, { passive: true }); 197 window.addEventListener('touchmove', opRaakBeweeg, { passive: true }); 198 window.addEventListener('keydown', opToets); 195 199 } 196 200 -
src/views/shell.ejs
r48481cd r4989294 222 222 223 223 <!-- v9 stylesheet (full palette system) --> 224 <link rel="stylesheet" href="/assets/css/style.css?v=7 7">224 <link rel="stylesheet" href="/assets/css/style.css?v=78"> 225 225 <script> 226 226 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in … … 505 505 // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig 506 506 // bumpen kost een bugfix die nooit aankomt. 507 var MOD_V = 1 3;507 var MOD_V = 16; 508 508 509 509 // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)