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