From 5399a95ca5819c7226b316a80d12a3a2977aedb3 Mon Sep 17 00:00:00 2001 From: Juan Cobo Betancourt Date: Sun, 1 Nov 2026 13:08:39 -0800 Subject: [PATCH 1/2] Keep the homepage video within one scroll stop --- layouts/_partials/homepage-demo.html | 6 +----- layouts/_partials/landing-body.html | 6 ++++++ static/assets/homepage-demo/homepage-demo.css | 19 +++++++++---------- static/assets/homepage-demo/homepage-demo.js | 8 ++++++-- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/layouts/_partials/homepage-demo.html b/layouts/_partials/homepage-demo.html index 63b3932..28b060c 100644 --- a/layouts/_partials/homepage-demo.html +++ b/layouts/_partials/homepage-demo.html @@ -1,10 +1,6 @@
-
-

What is Telar?¿Qué es Telar?

-

Telar (Spanish for "loom") weaves together images, video, audio, narrative text, and layered contextual information into interactive visual narrative exhibitions. It follows minimal computing principles: plain text authoring, static generation, and sustainable hosting.

-

Telar entreteje imágenes, video, audio, texto narrativo e información contextual en exhibiciones narrativas visuales e interactivas. Sigue los principios de computación mínima: autoría en texto plano, generación estática y alojamiento sostenible.

-
+

What is Telar?¿Qué es Telar?

diff --git a/layouts/_partials/landing-body.html b/layouts/_partials/landing-body.html index 7a781cc..6669f5f 100644 --- a/layouts/_partials/landing-body.html +++ b/layouts/_partials/landing-body.html @@ -44,6 +44,12 @@

Telar

+
+

What is Telar?¿Qué es Telar?

+

Telar (Spanish for "loom") weaves together images, video, audio, narrative text, and layered contextual information into interactive visual narrative exhibitions. It follows minimal computing principles: plain text authoring, static generation, and sustainable hosting.

+

Telar entreteje imágenes, video, audio, texto narrativo e información contextual en exhibiciones narrativas visuales e interactivas. Sigue los principios de computación mínima: autoría en texto plano, generación estática y alojamiento sostenible.

+
+

Key Features

Características principales

diff --git a/static/assets/homepage-demo/homepage-demo.css b/static/assets/homepage-demo/homepage-demo.css index 30c73be..95272f3 100644 --- a/static/assets/homepage-demo/homepage-demo.css +++ b/static/assets/homepage-demo/homepage-demo.css @@ -17,11 +17,12 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var( .hero__utility a{color:var(--cream);font-size:15px;text-decoration:underline;text-underline-offset:4px;opacity:.9} .hero__utility a:hover{opacity:1} .hero__scroll{bottom:20px} -.telar-demo-section{background:var(--demo-background);min-height:auto;padding:64px 24px} -.telar-demo-layout{display:grid;grid-template-columns:1fr;gap:32px;max-width:1600px;width:100%;margin:auto} -.telar-about-copy{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(0,1.7fr);gap:clamp(24px,4vw,64px);align-items:start;background:transparent;box-shadow:none;border:0;padding:0} -.telar-about-copy h2{margin-bottom:0} -.telar-about-copy p{font-weight:400;line-height:1.65;margin-bottom:0} +.telar-demo-section{background:var(--demo-background);min-height:100svh;padding:24px} +/* Fit the complete film inside one viewport, including on short desktop windows. */ +.telar-demo-layout{width:100%;max-width:min(1600px,calc((100svh - 48px) * 16 / 9));margin:auto} +.telar-about-copy{background:transparent;box-shadow:none;border:0;padding:0;margin-bottom:56px} +.telar-about-copy h2{margin-bottom:20px} +.telar-about-copy p{font-weight:400;line-height:1.65;margin-bottom:0;font-size:20px} .telar-demo-media{margin:0;min-width:0} /* The Vimeo exports have 109px of blank padding above and below the 1920x1080 film. Crop that padding without cropping any film content. @@ -49,18 +50,16 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var( .landing-footer a{color:var(--cream);text-underline-offset:3px} .landing-footer a:hover{color:var(--gold)} @media(max-width:900px){ - .telar-demo-layout{grid-template-columns:1fr;gap:24px;max-width:900px} - .telar-demo-section{padding:64px 24px} - .telar-about-copy{grid-template-columns:1fr;gap:12px;max-width:900px} + .telar-demo-section{min-height:auto} + .telar-about-copy{margin-bottom:40px} .telar-demo-media{width:100%} } @media(max-width:600px){ .hero{padding:24px 20px 68px} .hero__lang-toggle button{min-height:44px} .hero__utility{gap:4px 18px} - .telar-demo-section{padding:48px 16px} + .telar-demo-section{padding:24px 16px} .telar-about-copy{padding:0 4px} - .telar-demo-layout{gap:16px} .cta-section{padding:56px 20px} .features__grid{gap:16px} } diff --git a/static/assets/homepage-demo/homepage-demo.js b/static/assets/homepage-demo/homepage-demo.js index 3c28a91..3861bd5 100644 --- a/static/assets/homepage-demo/homepage-demo.js +++ b/static/assets/homepage-demo/homepage-demo.js @@ -27,9 +27,13 @@ raf=requestAnimationFrame(tick); function manualInput(){pendingTarget=null;onArrival=null;travel.reset(scrollY)} function preference(){manualInput();lenis.options.smoothWheel=!motion.matches;lenis.resize();if(motion.matches)suspend();else syncPlayback()} + // Read layout positions so fade-in transforms cannot create an extra arrow stop. + function layoutTop(element){ + let top=0;for(;element;element=element.offsetParent)top+=element.offsetTop;return top; + } function scrollTo(target,arrive=null){ if(typeof target==='string')target=document.querySelector(target); - if(target instanceof Element)target=target.getBoundingClientRect().top+scrollY; + if(target instanceof Element)target=layoutTop(target); if(!Number.isFinite(target))return; target=Math.min(limit(),Math.max(0,target));onArrival=arrive; if(!smooth()){lenis.scrollTo(target,{immediate:true});travel.reset(target);pendingTarget=null;onArrival=null;arrive?.();return} @@ -47,7 +51,7 @@ if(!direction||event.altKey||event.ctrlKey||event.metaKey||event.shiftKey)return; if(event.target.isContentEditable||event.target.closest('input,textarea,select,iframe,video,[role="slider"],[role="listbox"],[role="tablist"]'))return; event.preventDefault();if(event.repeat)return; - const stops=[...document.querySelectorAll('main > section,body > section,body > footer')].filter(e=>!e.hidden).map(e=>e.getBoundingClientRect().top+scrollY); + const stops=[...document.querySelectorAll('main > section,body > section,body > footer')].filter(e=>!e.hidden).map(layoutTop); const ordered=[...new Set(stops.map(n=>Math.min(limit(),Math.max(0,n))))].sort((a,b)=>a-b); const current=pendingTarget??scrollY,target=direction>0?ordered.find(n=>n>current+8):ordered.slice().reverse().find(n=>n Date: Sun, 1 Nov 2026 13:21:13 -0800 Subject: [PATCH 2/2] Give the definition and video separate scroll steps --- layouts/_partials/homepage-demo.html | 4 +- layouts/_partials/landing-body.html | 14 +-- static/assets/homepage-demo/README.md | 10 ++- static/assets/homepage-demo/homepage-demo.css | 14 +-- static/assets/homepage-demo/homepage-demo.js | 85 +++++++++++++++---- 5 files changed, 96 insertions(+), 31 deletions(-) diff --git a/layouts/_partials/homepage-demo.html b/layouts/_partials/homepage-demo.html index 28b060c..6efdd15 100644 --- a/layouts/_partials/homepage-demo.html +++ b/layouts/_partials/homepage-demo.html @@ -1,6 +1,6 @@ -
+
-

What is Telar?¿Qué es Telar?

+

Telar

diff --git a/layouts/_partials/landing-body.html b/layouts/_partials/landing-body.html index 6669f5f..14a77c0 100644 --- a/layouts/_partials/landing-body.html +++ b/layouts/_partials/landing-body.html @@ -39,17 +39,19 @@

Telar

-{{ partial "homepage-demo.html" . }} - - -
-
+
-

What is Telar?¿Qué es Telar?

+

What is Telar?¿Qué es Telar?

Telar (Spanish for "loom") weaves together images, video, audio, narrative text, and layered contextual information into interactive visual narrative exhibitions. It follows minimal computing principles: plain text authoring, static generation, and sustainable hosting.

Telar entreteje imágenes, video, audio, texto narrativo e información contextual en exhibiciones narrativas visuales e interactivas. Sigue los principios de computación mínima: autoría en texto plano, generación estática y alojamiento sostenible.

+
+{{ partial "homepage-demo.html" . }} + + +
+

Key Features

Características principales

diff --git a/static/assets/homepage-demo/README.md b/static/assets/homepage-demo/README.md index 2456bfd..33f796f 100644 --- a/static/assets/homepage-demo/README.md +++ b/static/assets/homepage-demo/README.md @@ -1,6 +1,6 @@ # Telar homepage video The homepage uses normal document flow and the supplied Spanish/English Vimeo -embeds. The canonical definition sits above a full-width film at every screen size. There is no native scroll-film renderer or local MP4 fetch. +embeds. The definition and viewport-fitted film occupy separate scroll steps. There is no native scroll-film renderer or local MP4 fetch. Only the active-language poster is loaded initially. IntersectionObserver creates the iframe and loads Vimeo's SDK as the film enters view. Playback is muted and @@ -10,8 +10,12 @@ The progress bar and auxiliary player controls are disabled. Reduced motion supp uses Vimeo's own controls. A failed load retains the poster and offers a Vimeo link. The supplied video IDs and privacy hashes are preserved. -Lenis and scroll-motion.js provide bounded page smoothing and one section per -arrow press. Film bookmarks and the pinned 2500svh section have been removed. +Lenis and scroll-motion.js animate one stop per wheel/trackpad gesture or arrow +press. A 200ms quiet gap separates wheel gestures; momentum cannot trigger a +second move. Tall sections include reading stops to keep every row reachable. +Native snap points cover touch input. For wheel input inside Vimeo, a scroll +observer hands native movement to the same step controller without covering the +player or disabling its controls. Reduced motion uses immediate transitions. Film bookmarks and the pinned 2500svh section have been removed. The approved palette is terracotta, cream, light grey, charcoal and darker secondary text, with a blue project section. The larger loom and original wordmark scale are retained. All developer/supporter logos, including UT AMPL, remain. diff --git a/static/assets/homepage-demo/homepage-demo.css b/static/assets/homepage-demo/homepage-demo.css index 95272f3..3c6bf0c 100644 --- a/static/assets/homepage-demo/homepage-demo.css +++ b/static/assets/homepage-demo/homepage-demo.css @@ -1,6 +1,9 @@ /* Approved homepage palette and normal-flow Vimeo demonstration. */ :root{--text-medium:#596776;--demo-background:#f4f5f6} -html{scroll-snap-type:none;scroll-behavior:auto} +html{scroll-snap-type:y mandatory;scroll-behavior:auto} +html.is-stepping{scroll-snap-type:none} +main > section,body > footer,.telar-reading-stop{scroll-snap-align:start;scroll-snap-stop:always} +.telar-reading-stop{position:absolute;left:0;width:1px;height:1px;opacity:0;pointer-events:none} body{overflow-x:clip} .visually-hidden,.skip-link:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0} .skip-link:focus{position:fixed;top:12px;left:12px;z-index:2000;padding:12px 18px;background:var(--cream);color:var(--terracotta);border-radius:8px} @@ -17,11 +20,12 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var( .hero__utility a{color:var(--cream);font-size:15px;text-decoration:underline;text-underline-offset:4px;opacity:.9} .hero__utility a:hover{opacity:1} .hero__scroll{bottom:20px} +.telar-about-section{background:var(--demo-background);min-height:100svh;padding:60px 24px} .telar-demo-section{background:var(--demo-background);min-height:100svh;padding:24px} /* Fit the complete film inside one viewport, including on short desktop windows. */ .telar-demo-layout{width:100%;max-width:min(1600px,calc((100svh - 48px) * 16 / 9));margin:auto} -.telar-about-copy{background:transparent;box-shadow:none;border:0;padding:0;margin-bottom:56px} -.telar-about-copy h2{margin-bottom:20px} +.telar-about-copy{background:transparent;box-shadow:none;border:0;padding:0;width:100%;max-width:900px;margin:0 auto} +.telar-about-copy h2{margin-bottom:24px;text-align:center} .telar-about-copy p{font-weight:400;line-height:1.65;margin-bottom:0;font-size:20px} .telar-demo-media{margin:0;min-width:0} /* The Vimeo exports have 109px of blank padding above and below the @@ -51,7 +55,7 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var( .landing-footer a:hover{color:var(--gold)} @media(max-width:900px){ .telar-demo-section{min-height:auto} - .telar-about-copy{margin-bottom:40px} + .telar-about-section{min-height:auto;padding:64px 24px} .telar-demo-media{width:100%} } @media(max-width:600px){ @@ -59,7 +63,7 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var( .hero__lang-toggle button{min-height:44px} .hero__utility{gap:4px 18px} .telar-demo-section{padding:24px 16px} - .telar-about-copy{padding:0 4px} + .telar-about-section{padding:48px 20px} .cta-section{padding:56px 20px} .features__grid{gap:16px} } diff --git a/static/assets/homepage-demo/homepage-demo.js b/static/assets/homepage-demo/homepage-demo.js index 3861bd5..1cd076e 100644 --- a/static/assets/homepage-demo/homepage-demo.js +++ b/static/assets/homepage-demo/homepage-demo.js @@ -1,18 +1,26 @@ -/* Normal page flow with bounded Lenis smoothing. Vimeo loads only on approach. */ +/* Stepped page navigation with bounded transitions. Vimeo loads only on approach. */ (() => { 'use strict'; const motion=matchMedia('(prefers-reduced-motion: reduce)'); const small=matchMedia('(max-width: 900px)'); const travel=new TelarScrollMotion(scrollY); const limit=()=>Math.max(0,document.documentElement.scrollHeight-innerHeight); - const smooth=()=>!motion.matches&&!small.matches; + const smooth=()=>!motion.matches; + let lastWheel=-Infinity,wheelTotal=0,wheelConsumed=false; const lenis=new Lenis({lerp:.06,smoothWheel:!motion.matches,wheelMultiplier:.65,autoRaf:false,anchors:false, virtualScroll({deltaY,event}){ - if(!smooth()||event.type!=='wheel'||event.ctrlKey)return; + if(event.type!=='wheel'||event.ctrlKey||event.metaKey)return; if(event.target.closest('input,textarea,select,iframe,[data-lenis-prevent]'))return false; - event.preventDefault();pendingTarget=null; - if(!travel.active)travel.reset(scrollY); - travel.wheel(deltaY,innerHeight,limit());return false; + event.preventDefault(); + const now=performance.now(); + if(now-lastWheel>200){wheelTotal=0;wheelConsumed=false} + lastWheel=now; + // One gesture owns one move. Its momentum tail must not start another. + if(wheelConsumed||travel.active){wheelConsumed=true;return false} + if(Math.sign(wheelTotal)!==Math.sign(deltaY))wheelTotal=0; + wheelTotal+=deltaY; + if(Math.abs(wheelTotal)>=12){wheelConsumed=true;advance(Math.sign(wheelTotal))} + return false; } }); let raf=0,lastTick=0,pendingTarget=null,onArrival=null; @@ -20,12 +28,12 @@ const dt=lastTick?(time-lastTick)/1000:0;lastTick=time; if(travel.active){ lenis.scrollTo(travel.step(dt,innerHeight,limit()),{immediate:true}); - if(!travel.active){pendingTarget=null;const arrived=onArrival;onArrival=null;arrived?.()} + if(!travel.active){document.documentElement.classList.remove('is-stepping');pendingTarget=null;const arrived=onArrival;onArrival=null;arrived?.()} } lenis.raf(time);raf=requestAnimationFrame(tick); } raf=requestAnimationFrame(tick); - function manualInput(){pendingTarget=null;onArrival=null;travel.reset(scrollY)} + function manualInput(){document.documentElement.classList.remove('is-stepping');pendingTarget=null;onArrival=null;travel.reset(scrollY)} function preference(){manualInput();lenis.options.smoothWheel=!motion.matches;lenis.resize();if(motion.matches)suspend();else syncPlayback()} // Read layout positions so fade-in transforms cannot create an extra arrow stop. function layoutTop(element){ @@ -36,7 +44,8 @@ if(target instanceof Element)target=layoutTop(target); if(!Number.isFinite(target))return; target=Math.min(limit(),Math.max(0,target));onArrival=arrive; - if(!smooth()){lenis.scrollTo(target,{immediate:true});travel.reset(target);pendingTarget=null;onArrival=null;arrive?.();return} + document.documentElement.classList.add('is-stepping'); + if(!smooth()){lenis.scrollTo(target,{immediate:true});travel.reset(target);pendingTarget=null;onArrival=null;document.documentElement.classList.remove('is-stepping');arrive?.();return} if(!travel.active)travel.reset(scrollY); travel.moveTo(target,innerHeight); } @@ -45,17 +54,47 @@ if(!element.hasAttribute('tabindex'))element.setAttribute('tabindex','-1'); element.focus({preventScroll:true}); } + function stops(){ + const points=[]; + for(const section of document.querySelectorAll('main > section,body > section,body > footer')){ + const top=layoutTop(section),overflow=section.offsetHeight-innerHeight; + const offsets=[]; + if(!section.hidden){ + points.push(top); + // Tall content gets reading stops too, so stepping never skips its lower rows. + if(overflow>64){ + const parts=Math.ceil(section.offsetHeight/innerHeight)-1; + for(let i=1;i<=parts;i++)offsets.push(overflow*i/parts); + } + points.push(...offsets.map(offset=>top+offset)); + } + // Native snapping also handles wheel events inside the cross-origin Vimeo frame. + // Keep identical stops available when those events cannot reach this document. + const existing=[...section.querySelectorAll('.telar-reading-stop')]; + if(existing.length!==offsets.length||existing.some((node,i)=>parseFloat(node.style.top)!==offsets[i])){ + existing.forEach(node=>node.remove()); + for(const offset of offsets){ + const marker=document.createElement('span');marker.className='telar-reading-stop'; + marker.setAttribute('aria-hidden','true');marker.style.top=offset+'px';section.append(marker); + } + } + } + return [...new Set(points.map(n=>Math.min(limit(),Math.max(0,n))))].sort((a,b)=>a-b); + } + const stopObserver=new ResizeObserver(stops); + document.querySelectorAll('main > section,body > footer').forEach(section=>stopObserver.observe(section)); + function advance(direction){ + const ordered=stops(),current=pendingTarget??scrollY; + const target=direction>0?ordered.find(n=>n>current+8):ordered.slice().reverse().find(n=>n section,body > section,body > footer')].filter(e=>!e.hidden).map(layoutTop); - const ordered=[...new Set(stops.map(n=>Math.min(limit(),Math.max(0,n))))].sort((a,b)=>a-b); - const current=pendingTarget??scrollY,target=direction>0?ordered.find(n=>n>current+8):ordered.slice().reverse().find(n=>n{ + // Cross-origin iframe wheel events do not bubble. Observe their native movement + // and hand the gesture to the same step controller, keeping Vimeo clickable. + if(!embed.querySelector('iframe')?.matches(':hover')){ + if(!travel.active)travel.reset(scrollY);return; + } + const delta=scrollY-travel.position;if(Math.abs(delta)<2)return; + const now=performance.now(); + if(now-lastWheel>200){wheelTotal=0;wheelConsumed=false} + lastWheel=now; + if(wheelConsumed||travel.active){ + wheelConsumed=true;lenis.scrollTo(travel.position,{immediate:true});return; + } + const previous=travel.position;wheelConsumed=true; + travel.reset(scrollY);pendingTarget=previous;advance(Math.sign(delta)); + },{passive:true}); const languages=JSON.parse(document.getElementById('telar-demo-config').textContent); let sdkPromise=null,player=null,iframe=null,ready=false,playing=false,inView=false,userPaused=false,pauseExpected=false,generation=0,timeout=0,data; function sdk(){