feat: add story.html.twig with hero scroll effect and shortcode JS
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% block nav %}
|
||||
<a class="story-escape" href="{{ page.parent().url }}">← Stories</a>
|
||||
{% 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 }}">← Back to stories</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<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 progress = Math.min(window.scrollY / window.innerHeight, 1);
|
||||
overlay.style.background = 'rgba(0,0,0,' + (progress * 0.65).toFixed(3) + ')';
|
||||
if (progress >= 1) overlay.style.display = 'none';
|
||||
else overlay.style.display = '';
|
||||
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();
|
||||
})();
|
||||
|
||||
/* ── 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 %}
|
||||
Reference in New Issue
Block a user