Files
intotheeast-com-content/themes/intotheeast/templates/story.html.twig
T
m038 cc341cc944 fix(story): nav title cross-fades scroll-driven as hero content exits viewport
Replaced IntersectionObserver (discrete threshold) with a scroll RAF loop
using getBoundingClientRect. Opacity is computed from the fraction of
.story-hero__content still visible above the viewport top — so the nav title
fades in gradually as the hero title slides off the top edge, reaching full
opacity only when the element is completely gone.

Removed CSS transition (no longer needed; per-frame JS update is smooth).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:28:12 +02:00

257 lines
11 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'partials/base.html.twig' %}
{% block nav %}
<a class="story-escape" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
<span class="story-nav-title" id="story-nav-title" aria-hidden="true">{{ page.title }}</span>
{% endblock %}
{% block content %}
{% set hero = null %}
{% if page.header.hero_image and page.media[page.header.hero_image] is defined %}
{% set hero = page.media[page.header.hero_image] %}
{% endif %}
{% set date_str = page.date|date('d M Y') %}
{% if page.header.end_date %}
{% set end_str = page.header.end_date|date('d M Y') %}
{% set date_str = page.date|date('d M') ~ '' ~ end_str %}
{% endif %}
{% set location = page.header.location_name ?? '' %}
<div class="story-hero" id="story-hero">
<div class="story-hero__img-wrap">
{% if hero %}
<img src="{{ hero.url }}" alt="{{ page.header.hero_alt ?? page.title }}" class="story-hero__img" loading="eager">
{% else %}
<div class="story-hero__img-placeholder"></div>
{% endif %}
</div>
<div class="story-hero__overlay" id="story-overlay" aria-hidden="true"></div>
<div class="story-hero__content">
<h1 class="story-hero__title">{{ page.title }}</h1>
<p class="story-hero__meta">
<time datetime="{{ page.date|date('Y-m-d') }}">{{ date_str }}</time>
{% if location %} · {{ location }}{% endif %}
</p>
</div>
<div class="story-hero__scroll-cue" id="story-scroll-cue" aria-hidden="true">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div class="story-hero__spacer"></div>
</div>
<div class="story-body">
{{ page.content|raw }}
<footer class="story-footer">
<a href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
</footer>
</div>
<button class="story-totop" id="story-totop" aria-label="Back to top">↑ Top</button>
<script src="https://cdn.jsdelivr.net/npm/scrollama@3/build/scrollama.min.js"></script>
<script>
/* ── Hero scroll effect ──────────────────────────────────── */
(function () {
var overlay = document.getElementById('story-overlay');
var cue = document.getElementById('story-scroll-cue');
var hidden = false;
var ticking = false;
if (!overlay) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
function update() {
var scrollY = window.scrollY;
var vh = window.innerHeight;
var heroEnd = vh * 1.4; // img-wrap (100vh) + spacer (40vh)
var mid = heroEnd * 0.5; // peak darkness at 70vh
var opacity = scrollY <= mid
? (scrollY / mid) * 0.65
: Math.max(0, (1 - (scrollY - mid) / (heroEnd - mid)) * 0.65);
overlay.style.background = 'rgba(0,0,0,' + opacity.toFixed(3) + ')';
overlay.style.display = (opacity < 0.002) ? 'none' : '';
if (!hidden && window.scrollY > 80) {
cue.classList.add('is-hidden');
hidden = true;
}
ticking = false;
}
window.addEventListener('scroll', function () {
if (!ticking) { requestAnimationFrame(update); ticking = true; }
}, { passive: true });
update();
})();
/* ── Story title in nav (cross-fades as hero content exits viewport top) ── */
(function () {
var navTitle = document.getElementById('story-nav-title');
var heroContent = document.querySelector('.story-hero__content');
if (!navTitle || !heroContent) return;
var hasBeenVisible = false;
var ticking = false;
function update() {
var rect = heroContent.getBoundingClientRect();
var inView = rect.bottom > 0 && rect.top < window.innerHeight;
if (inView) hasBeenVisible = true;
if (hasBeenVisible) {
var opacity;
if (rect.bottom <= 0) {
opacity = 1; // fully above viewport
} else if (rect.top <= 0) {
opacity = 1 - rect.bottom / rect.height; // partially exiting top
} else {
opacity = 0; // fully in viewport
}
navTitle.style.opacity = opacity.toFixed(3);
}
ticking = false;
}
window.addEventListener('scroll', function () {
if (!ticking) { requestAnimationFrame(update); ticking = true; }
}, { passive: true });
update();
})();
/* ── Back to top button ─────────────────────────────────── */
(function () {
var btn = document.getElementById('story-totop');
if (!btn) return;
var threshold = window.innerHeight * 0.8;
var shown = false;
btn.addEventListener('click', function () {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
window.addEventListener('scroll', function () {
var shouldShow = window.scrollY > threshold;
if (shouldShow !== shown) {
shown = shouldShow;
btn.classList.toggle('is-visible', shown);
}
}, { passive: true });
})();
/* ── ChapterBreak scroll-reveal ─────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var panels = document.querySelectorAll('.chapter-break__panel');
if (!panels.length) return;
if (reduced) { panels.forEach(function (p) { p.classList.add('is-revealed'); }); return; }
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add('is-revealed'); io.unobserve(e.target); }
});
}, { threshold: 0.25 });
panels.forEach(function (p) { io.observe(p); });
})();
/* ── PullQuote scroll-reveal ─────────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var quotes = document.querySelectorAll('.pull-quote');
if (!quotes.length) return;
if (reduced) { quotes.forEach(function (q) { q.classList.add('is-revealed'); }); return; }
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add('is-revealed'); io.unobserve(e.target); }
});
}, { threshold: 0.2 });
quotes.forEach(function (q) { io.observe(q); });
})();
/* ── SnapGallery dot indicator ───────────────────────────── */
(function () {
document.querySelectorAll('.pgallery').forEach(function (gallery) {
var slides = gallery.querySelectorAll('.pgallery__slide');
var dots = gallery.querySelectorAll('.pgallery__dot');
if (!slides.length || !dots.length) return;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
var idx = parseInt(e.target.dataset.index, 10);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[idx]) dots[idx].classList.add('is-active');
}
});
}, { threshold: 0.5, root: gallery.querySelector('.pgallery__frame') });
slides.forEach(function (s) { io.observe(s); });
});
})();
/* ── ScrollySection (Scrollama) ──────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var sections = document.querySelectorAll('.scrolly');
if (!sections.length || typeof scrollama === 'undefined') return;
var panOffsets = ['50% 40%', '50% 50%', '50% 60%', '50% 45%', '50% 55%'];
sections.forEach(function (section) {
var img = section.querySelector('.scrolly__img');
var overlay = section.querySelector('.scrolly__img-overlay');
var contentEl = section.querySelector('.scrolly__steps-content');
var stepsWrap = section.querySelector('.scrolly__steps');
if (!img || !contentEl || !stepsWrap) return;
/* Split slot content on <hr> into individual step divs */
var nodes = Array.from(contentEl.childNodes);
var groups = [];
var curr = [];
nodes.forEach(function (n) {
if (n.nodeName === 'HR') { if (curr.length) groups.push(curr); curr = []; }
else curr.push(n);
});
if (curr.length) groups.push(curr);
if (!groups.length) groups.push(nodes);
groups.forEach(function (group) {
var step = document.createElement('div');
step.className = 'scrolly-step';
var inner = document.createElement('div');
inner.className = 'scrolly-step__inner';
group.forEach(function (n) { inner.appendChild(n.cloneNode(true)); });
step.appendChild(inner);
stepsWrap.appendChild(step);
});
if (reduced) {
section.querySelectorAll('.scrolly-step').forEach(function (s) { s.classList.add('is-active'); });
return;
}
/* Initial blur */
img.style.filter = 'blur(8px)';
img.style.transform = 'scale(1.04)';
img.style.transition = 'filter 0.7s cubic-bezier(.16,1,.3,1), transform 0.7s cubic-bezier(.16,1,.3,1), object-position 1.2s cubic-bezier(.16,1,.3,1)';
new IntersectionObserver(function (entries, obs) {
if (entries[0].isIntersecting) {
img.style.filter = 'blur(0)';
img.style.transform = 'scale(1)';
obs.disconnect();
}
}, { threshold: 0.1 }).observe(section);
scrollama()
.setup({ step: Array.from(section.querySelectorAll('.scrolly-step')), offset: 0.55 })
.onStepEnter(function (d) {
d.element.classList.add('is-active');
img.style.objectPosition = panOffsets[d.index % panOffsets.length];
if (overlay) overlay.style.background = 'rgba(0,0,0,' + (0.05 + (d.index % 3) * 0.05) + ')';
})
.onStepExit(function (d) {
if (d.direction === 'up') d.element.classList.remove('is-active');
});
});
})();
</script>
{% endblock %}