Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion skills/scroll-world/references/index-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
hint: 'scroll to fly in',
diveScroll: 1.3,
connScroll: 0.9,
hashNavigation: true, // #section-id links jump to scenes; set false to disable
sections: [
// One object per scene, in order. `accent` colours the copy/route for that scene.
// One object per scene, in order. `id` becomes a URL hash target such as #finale.
// `accent` colours the copy/route for that scene.
// First section is the hero (its copy greets on landing); last carries the CTA.
{
id: 'sceneA', label: 'Scene A',
Expand Down
47 changes: 44 additions & 3 deletions skills/scroll-world/references/scrub-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
connScroll: 0.9, // ...per connector clip
hint: 'scroll to fly in',
nav: true, // show the top section nav
hashNavigation: true, // enable #section-id links + nav hash updates
atmosphere: true, // subtle gradient + drifting particles behind the clips
sections: [
{ id, label, still, stillMobile, clip, clipMobile, accent,
Expand Down Expand Up @@ -77,8 +78,11 @@ function mountScrollWorld(container, config) {
const DIVE_W = config.diveScroll || 1.3;
const CONN_W = config.connScroll || 0.9;
const CROSSFADE = (config.crossfade != null) ? config.crossfade : 0.12; // seam dissolve width (vh)
const HASH_NAV = config.hashNavigation !== false;
const N = SECTIONS.length;
if (!N) return;
const sectionIds = Object.create(null);
SECTIONS.forEach((s, i) => { if (s.id) sectionIds[String(s.id)] = i; });

injectCSS();
container.classList.add('sw-root');
Expand Down Expand Up @@ -161,11 +165,11 @@ function mountScrollWorld(container, config) {

const dot = el('button', 'sw-route__dot'); dot.style.setProperty('--sw-accent', s.accent || '');
dot.innerHTML = `<span class="sw-route__label">${esc(s.label || '')}</span><i></i>`;
dot.addEventListener('click', () => jumpTo(i)); route.appendChild(dot); dots.push(dot);
dot.addEventListener('click', () => jumpTo(i, { updateHash: HASH_NAV })); route.appendChild(dot); dots.push(dot);

if (config.nav !== false) {
const b = el('button', 'sw-nav__item'); b.textContent = s.label || '';
b.addEventListener('click', () => jumpTo(i)); nav.appendChild(b);
b.addEventListener('click', () => jumpTo(i, { updateHash: HASH_NAV })); nav.appendChild(b);
}
});

Expand All @@ -190,11 +194,32 @@ function mountScrollWorld(container, config) {
read();
}

function jumpTo(i) {
function jumpTo(i, opts) {
const seg = SECTIONS[i]._seg;
if (!seg) return;
opts = opts || {};
if (opts.updateHash) updateHash(SECTIONS[i].id);
window.scrollTo({ top: seg.start + (seg.end - seg.start) * 0.5, behavior: reduce ? 'auto' : 'smooth' });
}

function sectionIndexFromHash() {
if (!HASH_NAV || !window.location.hash) return -1;
let id = window.location.hash.slice(1);
try { id = decodeURIComponent(id); } catch (e) {}
return Object.prototype.hasOwnProperty.call(sectionIds, id) ? sectionIds[id] : -1;
}

function updateHash(id) {
if (!id || !window.history || !window.history.pushState) return;
const hash = '#' + encodeURIComponent(String(id));
if (window.location.hash !== hash) window.history.pushState(null, '', hash);
}

function jumpToHash() {
const i = sectionIndexFromHash();
if (i >= 0) jumpTo(i);
}

function loadClip(s) {
// Under prefers-reduced-motion we never load the clips at all — the stills stay up
// and simply cross-dissolve as you scroll. No scrubbed video motion, no decode cost.
Expand Down Expand Up @@ -321,7 +346,23 @@ function mountScrollWorld(container, config) {
window.addEventListener('resize', onResize);
window.addEventListener('orientationchange', layout);
window.addEventListener('load', layout);
if (HASH_NAV) {
container.addEventListener('click', e => {
const a = e.target.closest && e.target.closest('a[href^="#"]');
if (!a || !container.contains(a)) return;
const raw = a.getAttribute('href');
if (!raw || raw === '#') return;
let id = raw.slice(1);
try { id = decodeURIComponent(id); } catch (err) {}
if (!Object.prototype.hasOwnProperty.call(sectionIds, id)) return;
e.preventDefault();
updateHash(id);
jumpTo(sectionIds[id]);
});
window.addEventListener('hashchange', jumpToHash);
}
layout();
jumpToHash();
requestAnimationFrame(raf);

// ---- helpers ----
Expand Down