Changeset a505f31 in Klonkt


Ignore:
Timestamp:
08/20/2026 02:27:56 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
88838bc
Parents:
d8f8f07
Message:

Lezen: Lenis voor het snappen op desktop, plus twee kleine dingen

WAAROM LENIS, en dat is niet het vloeiende scrollen: de DUUR van een snap is
met native scroll-snap niet in te stellen -- die zit in de browser. Robin
vroeg om een snellere snap, en dat was declaratief onmogelijk. Lenis' snap-
pakket geeft ons duration, easing, distanceThreshold en debounce.

distanceThreshold 12% de vangzone (native: browsereigen, ~200-300px)
duration 0.4s in totaal onder de halve seconde
easing easeOutQuart -- op de helft van de tijd is 94% van de

weg af, daarna zacht uitdempen. Bewust niet
Lenis' standaard easeOutExpo: die schiet weg
en kruipt zo lang na dat het lijkt of hij niet
afmaakt.

debounce 60ms niet de standaard 500; dat voelt als te laat

ALLEEN OP DESKTOP (hover: hover en pointer: fine). Op touch doet Lenis van
zichzelf niets -- syncTouch staat uit -- en is het systeem-scrollen al
soepel. Daar blijft de native CSS-snap staan die er nu is, inclusief het
anker van nul hoog en de richtingsschakelaar. Robins keuze: "enkel voor
desktop, dat is prima, logisch dat het niet op mobiel kan".

Omhoog niet snappen gaat met Lenis via snap.stop()/start(), een stuk
schoner dan de CSS-eigenschap omzetten. Beide wegen delen dezelfde
richtingsdetectie uit wiel, vinger en toets.

NOG NIET UIT: smoothWheel. Dat kwam er na het snappen bij, en het is de kant
die oppassen vraagt -- een Mac-trackpad heeft zijn EIGEN momentum en Lenis'
demping komt daar bovenop. Vandaar lerp 0.2 in plaats van de standaard 0.1.
Robin vond dat op 20-8 nog steeds te zweverig, dus dat getal moet nog omhoog
of smoothWheel moet terug naar false. Die tweede stand werkte ook; dan doet
Lenis alleen de snap. Dat staat zo in het commentaar bij de instellingen,
zodat de volgende sessie niet hoeft te raden waar de knop zit.

Gevendored in src/assets/js/vendor (Lenis 1.3.26, MIT, 43KB samen), want de
CSP staat geen CDN toe. Dynamisch geimporteerd, dus mobiel haalt het niet
op. VENDOR_V ophogen als die bestanden wijzigen. De CSP hoefde niet aan:
strict-dynamic staat een import vanuit een vertrouwde module toe.

TWEE KLEINE DINGEN uit dezelfde ronde:

  • De terug-naar-boven-knop wijkt voor de mini-speler. Die kwam er anders bovenop te liggen (schermafbeelding). body.has-audio-player bestond al en wordt door audio-player.js gezet, dus dit volgt vanzelf.
  • Vastgepind is nu zichtbaar in de leestitel, met hetzelfde pin-icoon als in de tijdlijn. In de leesstroom staat een gepind bericht bovenaan zonder dat je ziet waarom. Nagekeken met een echte pin op dev.

style.css naar v82, MOD_V naar 19.

Suite 1169/1169.

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

Location:
src
Files:
3 added
5 edited

Legend:

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

    rd8f8f07 ra505f31  
    25872587.feed-reader .read-head { padding-bottom: .5rem; }
    25882588.feed-reader .read-title { font-size: clamp(1.6rem, 4vw, 2.4rem); line-height: 1.15; margin: 0 0 .3rem; }
     2589/* Het pin-icoon voor de titel: meeschalend met de kop, maar duidelijk kleiner --
     2590   het is een markering en geen deel van de titel. */
     2591.feed-reader .read-pin {
     2592    display: inline-block;
     2593    vertical-align: baseline;
     2594    width: .62em; height: .62em;
     2595    margin-right: .35em;
     2596    color: var(--ink-muted);
     2597}
     2598.feed-reader .read-pin svg { width: 100%; height: 100%; display: block; }
    25892599/* De titel is een link, maar mag er niet als een link uitzien: dit is een
    25902600   leesscherm, geen index. */
     
    26352645@media (min-width: 768px) {
    26362646    .read-top { bottom: 1.5rem; right: 1.5rem; }
     2647}
     2648/* Staat de mini-speler onderin, dan wijkt de knop daarvoor -- anders komt hij er
     2649   bovenop te liggen (Robins schermafbeelding, 20-8). body.has-audio-player wordt
     2650   door audio-player.js gezet zodra de speler zichtbaar is, en weer weggehaald
     2651   als hij verdwijnt, dus dit volgt vanzelf.
     2652   Op mobiel staat de speler BOVEN de tabbalk gestapeld, dus daar komt zijn
     2653   hoogte bovenop de ruimte die er al gereserveerd was. */
     2654body.has-audio-player .read-top { bottom: calc(env(safe-area-inset-bottom, 0) + 4.75rem + 4.25rem); }
     2655@media (min-width: 768px) {
     2656    body.has-audio-player .read-top { bottom: calc(1.5rem + 4.25rem); }
    26372657}
    26382658@media (prefers-reduced-motion: reduce) {
  • src/assets/js/mod/read.js

    rd8f8f07 ra505f31  
    117117let rustTimer = null;
    118118
     119/**
     120 * LENIS, en alleen op desktop.
     121 *
     122 * Op touch doet Lenis van zichzelf niets (syncTouch staat standaard uit) en is
     123 * het systeem-scrollen al soepel -- daar blijft de native CSS-snap staan die
     124 * hierboven beschreven is. Robins keuze (20-8): "enkel voor desktop, dat is
     125 * prima, logisch dat het niet op mobiel kan".
     126 *
     127 * WAAROM LENIS HIER STAAT, en dat is niet het vloeiende scrollen: de DUUR van
     128 * een snap is met native scroll-snap niet in te stellen -- die zit in de browser.
     129 * Lenis' snap-pakket wel: duration, easing, distanceThreshold en debounce zijn
     130 * allemaal van ons. Dat was de aanleiding.
     131 *
     132 * Het vloeiende scrollen (smoothWheel) kwam er daarna bij, en dat is de kant die
     133 * OPPASSEN vraagt. Een Mac-trackpad heeft zijn EIGEN momentum, en Lenis'
     134 * demping komt daar bovenop -- dubbel gedempt voelt drijverig. Vandaar lerp 0.2
     135 * in plaats van de standaard 0.1. Robin vond dat op 20-8 nog steeds te zweverig,
     136 * dus dat getal is nog niet uit; zie de opmerking bij de instellingen.
     137 *
     138 * De stand met `smoothWheel: false` werkte ook, en dan doet Lenis alleen de
     139 * snap. Dat is de terugvalpositie als het vloeiende scrollen niet bevalt.
     140 *
     141 * lenis/snap haakt alleen in op lenis.on('scroll') en roept lenis.scrollTo aan
     142 * (nagekeken in de dist), dus die opzet werkt.
     143 */
     144const OP_DESKTOP = window.matchMedia('(hover: hover) and (pointer: fine)');
     145const VENDOR_V = 1;   // ophogen als de bestanden in /assets/js/vendor wijzigen
     146
     147let lenis = null;
     148let snap = null;
     149
     150async function startLenis() {
     151  if (lenis || !OP_DESKTOP.matches) return;
     152  const [L, S] = await Promise.all([
     153    import(`/assets/js/vendor/lenis.mjs?v=${VENDOR_V}`),
     154    import(`/assets/js/vendor/lenis-snap.mjs?v=${VENDOR_V}`),
     155  ]);
     156  lenis = new L.default({
     157    // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in
     158    // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en
     159    // heeft die demping nodig; een Mac-trackpad heeft zijn EIGEN momentum en
     160    // krijgt er dan een tweede overheen -- dat is precies het drijverige gevoel
     161    // waar Robin voor waarschuwde. Hoger betekent korter naijlen, dus dit is de
     162    // middenweg: de schokjes weg, de nasleep kort.
     163    // Staat het toch te zweven, dan is lerp omhoog (richting 1) of terug naar
     164    // smoothWheel:false de knop -- die stand werkte ook, met alleen de snap.
     165    smoothWheel: true,
     166    lerp: 0.2,
     167    syncTouch: false,   // op touch blijft alles van het systeem
     168    autoRaf: true,
     169  });
     170  snap = new S.default(lenis, {
     171    type: 'proximity',
     172    distanceThreshold: '12%',   // de vangzone, hier WEL instelbaar
     173    debounce: 60,               // niet de standaard 500: dat voelt als te laat
     174    duration: 0.4,              // in totaal onder de halve seconde
     175    // Vlot weg, dan steeds langzamer aankomen (Robin, 20-8). easeOutQuart: op de
     176    // helft van de tijd is 94% van de weg af, en de rest dempt zacht uit.
     177    // Bewust NIET Lenis' standaard easeOutExpo -- die schiet weg en kruipt dan
     178    // zo lang na dat het lijkt of hij niet afmaakt.
     179    easing: (t) => 1 - Math.pow(1 - t, 4),
     180  });
     181  // Het snappunt is de bovenkant van elk bericht. Het laatste doet niet mee:
     182  // zijn bovenkant is niet te bereiken, er zit te weinig pagina onder.
     183  const berichten = [...document.querySelectorAll('.feed-reader .read-post')];
     184  berichten.slice(0, -1).forEach((a) => snap.addElement(a, { align: 'start' }));
     185  // Native snappen uit: twee mechanismen op dezelfde scroller vechten.
     186  document.documentElement.style.scrollSnapType = 'none';
     187}
     188
     189function stopLenis() {
     190  if (snap) { snap.destroy(); snap = null; }
     191  if (lenis) { lenis.destroy(); lenis = null; }
     192  document.documentElement.style.scrollSnapType = '';
     193}
     194
     195/**
     196 * Omhoog niet snappen. Met Lenis is dat snap.stop()/start(); zonder Lenis (dus
     197 * op touch) zetten we de CSS-eigenschap om, precies zoals hiervoor.
     198 */
    119199function zetSnappen(aan) {
     200  if (snap) { if (aan) snap.start(); else snap.stop(); return; }
    120201  const el = document.documentElement;
    121202  const wil = aan ? '' : 'none';
     
    197278  window.addEventListener('touchmove', opRaakBeweeg, { passive: true });
    198279  window.addEventListener('keydown', opToets);
     280
     281  // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw
     282  // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en
     283  // wegnavigeren hoort hem op te ruimen.
     284  stopLenis();
     285  if (document.body.dataset.feedView === 'reader') startLenis();
    199286}
    200287
  • src/services/i18n.js

    rd8f8f07 ra505f31  
    3333    'switch.solo': 'Solo',
    3434    'switch.circle': 'Cirkels',
    35     'switch.grid': 'Grid', 'switch.reader': 'Lezen', 'switch.timeline': 'Tijdlijn', 'read.to_top': 'Terug naar boven',
     35    'switch.grid': 'Grid', 'switch.reader': 'Lezen', 'switch.timeline': 'Tijdlijn', 'read.to_top': 'Terug naar boven', 'read.pinned': 'Vastgepind',
    3636    'asite.feed_alt': 'Tweede weergave', 'asite.feed_alt_reader': 'Lezen', 'asite.feed_alt_timeline': 'Tijdlijn', 'asite.feed_alt_auto': 'Lezen op mobiel, Tijdlijn op desktop',
    3737    'switch.reader_solo_only': 'Lezen kan alleen in Solo — in de cirkel staan berichten van anderen',
     
    10491049    'switch.solo': 'Solo',
    10501050    'switch.circle': 'Circles',
    1051     'switch.grid': 'Grid', 'switch.reader': 'Reader', 'switch.timeline': 'Timeline', 'read.to_top': 'Back to top',
     1051    'switch.grid': 'Grid', 'switch.reader': 'Reader', 'switch.timeline': 'Timeline', 'read.to_top': 'Back to top', 'read.pinned': 'Pinned',
    10521052    'asite.feed_alt': 'Second view', 'asite.feed_alt_reader': 'Reader', 'asite.feed_alt_timeline': 'Timeline', 'asite.feed_alt_auto': 'Reader on mobile, Timeline on desktop',
    10531053    'switch.reader_solo_only': 'Reader is Solo only — a circle shows other people\u2019s posts',
     
    20562056    'switch.solo': 'Solo',
    20572057    'switch.circle': 'Zirkel',
    2058     'switch.grid': 'Raster', 'switch.reader': 'Lesen', 'switch.timeline': 'Zeitleiste', 'read.to_top': 'Nach oben',
     2058    'switch.grid': 'Raster', 'switch.reader': 'Lesen', 'switch.timeline': 'Zeitleiste', 'read.to_top': 'Nach oben', 'read.pinned': 'Angeheftet',
    20592059    'asite.feed_alt': 'Zweite Ansicht', 'asite.feed_alt_reader': 'Lesen', 'asite.feed_alt_timeline': 'Zeitleiste', 'asite.feed_alt_auto': 'Lesen mobil, Zeitleiste am Desktop',
    20602060    'switch.reader_solo_only': 'Lesen gibt es nur in Solo — im Zirkel stehen Beitr\u00e4ge anderer',
  • src/views/partials/read-article.ejs

    rd8f8f07 ra505f31  
    6868
    6969  <header class="read-head">
    70     <h1 class="read-title"><a href="<%= _b %>/<%= post.slug %>"><%= post.title || '' %></a></h1>
     70    <%# Vastgepind hoort zichtbaar te zijn in de titel: in de leesstroom staat een
     71        gepind bericht bovenaan zonder dat je ziet WAAROM het daar staat. Zelfde
     72        icoon als in de tijdlijn (post-card.ejs), zodat het hetzelfde ding is. %>
     73    <h1 class="read-title">
     74      <% if (post.pinned > 0) { %><span class="read-pin" title="<%= t('read.pinned') %>" aria-label="<%= t('read.pinned') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg></span><% } %><a href="<%= _b %>/<%= post.slug %>"><%= post.title || '' %></a>
     75    </h1>
    7176    <% if (post.published_at) { %><p class="read-when"><%= formatDateTime(post.published_at) %></p><% } %>
    7277  </header>
  • src/views/shell.ejs

    rd8f8f07 ra505f31  
    222222
    223223<!-- v9 stylesheet (full palette system) -->
    224 <link rel="stylesheet" href="/assets/css/style.css?v=81">
     224<link rel="stylesheet" href="/assets/css/style.css?v=82">
    225225<script>
    226226/* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in
     
    505505  // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig
    506506  // bumpen kost een bugfix die nooit aankomt.
    507   var MOD_V = 16;
     507  var MOD_V = 19;
    508508
    509509  // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
Note: See TracChangeset for help on using the changeset viewer.