mirror of
https://github.com/tiennm99/dating.git
synced 2026-09-02 14:20:21 +00:00
@
feat: turn dating packet into scroll-driven short film Rearrange the JD/CV content into a 7-scene narrative arc that opens on the person, builds through the offer, and closes on sincerity, with the full facts kept as a dossier appendix. Layer a GSAP + ScrollTrigger + Lenis film engine that scrubs each scene into view and pins the opening and closing title cards. Keep the progressive-enhancement contract: the film only activates on wide viewports with motion allowed and the CDN present; otherwise the existing static reveal runs. Scope the reveal styles so the two engines never animate the same content, and fail open so content is never trapped hidden. @
This commit is contained in:
@@ -48,8 +48,9 @@
|
||||
if (id) navLinks.set(id, link);
|
||||
});
|
||||
|
||||
// DOM order matters: used to pick the topmost in-view section.
|
||||
const sections = ['home', 'jd', 'cv']
|
||||
// DOM order matters: used to pick the topmost in-view section. Mirrors the
|
||||
// nav targets in index.html (the four scenes that have a nav link).
|
||||
const sections = ['home', 'applicant', 'offer', 'dossier']
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter(Boolean);
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/* story-film.css — layout + pre-hide for the scroll-driven "short film" layer.
|
||||
Everything here is scoped under `html.film-ready`, a class added by
|
||||
assets/story-film.js ONLY when the film activates (wide viewport, motion
|
||||
allowed, CDN libraries present). With JS off, on phones, or under
|
||||
reduced-motion this class is never set, so none of these rules apply and the
|
||||
page renders exactly as the static reveal (story.css) presents it.
|
||||
|
||||
This file owns FILM layout + starting opacity only. Colors, spacing, and type
|
||||
come from styles.css and are never redefined here. */
|
||||
|
||||
/* Each scene becomes a full-height stage so beats have room to scrub in and
|
||||
pinned title cards hold cleanly. min-height (not fixed height) lets
|
||||
content-heavy scenes grow past the viewport instead of clipping. */
|
||||
html.film-ready .film-scene {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Pre-hide beats so there is no flash before GSAP takes over. story-film.js
|
||||
sets the same values via gsap.set(); this rule only covers the gap between
|
||||
first paint and the script running. failOpen() overrides both with inline
|
||||
styles, so this can never trap content. */
|
||||
html.film-ready [data-scene] [data-reveal] {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* Defensive: the film never activates under reduced motion (story-film.js gates
|
||||
on it), but if that ever changes, keep content visible rather than hidden. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html.film-ready [data-scene] [data-reveal] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// story-film.js — scroll-driven "short film" choreography.
|
||||
//
|
||||
// Turns each chapter into a scene whose beats scrub into view as the reader
|
||||
// scrolls (GSAP + ScrollTrigger), gliding on Lenis smooth scroll. Title-card
|
||||
// scenes ([data-scene-pin]) pin briefly for a dramatic hold.
|
||||
//
|
||||
// Relationship to the older reveal engine (assets/story.js):
|
||||
// * This file runs BEFORE story.js (script order in index.html).
|
||||
// * When the film activates it adds `html.film-ready`; story.js then bails and
|
||||
// story.css stops hiding [data-reveal] (its rules are scoped
|
||||
// `html.js-ready:not(.film-ready)`). GSAP owns the motion instead.
|
||||
// * When the film does NOT activate (narrow screen, reduced motion, or the
|
||||
// CDN libraries are missing) this file returns early and the existing
|
||||
// story.js reveal runs unchanged — the guaranteed fallback.
|
||||
//
|
||||
// Fail-open: any error during setup calls failOpen(), which force-shows every
|
||||
// beat with inline styles that win over both stylesheets, so content is never
|
||||
// trapped hidden. Loaded with `defer`, mirroring the other enhancement scripts.
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const root = document.documentElement;
|
||||
const REVEAL = '[data-reveal]';
|
||||
|
||||
// Force every beat visible regardless of stylesheet state. Inline opacity/
|
||||
// transform beat both story.css and story-film.css (neither uses !important
|
||||
// on these properties), so this is a hard guarantee against trapped content.
|
||||
const failOpen = () => {
|
||||
document.querySelectorAll(REVEAL).forEach((el) => {
|
||||
el.style.opacity = '1';
|
||||
el.style.transform = 'none';
|
||||
});
|
||||
};
|
||||
|
||||
// Activation gate. The film is a progressive enhancement on top of the
|
||||
// static reveal; only take over when all three hold:
|
||||
// * viewport wide enough that pinning/scrubbing reads well (not phones),
|
||||
// * the reader has not asked to reduce motion,
|
||||
// * the CDN libraries actually loaded.
|
||||
// Otherwise return and let story.js handle the reveal as it does today.
|
||||
const wideEnough = window.matchMedia('(min-width: 768px)').matches;
|
||||
const motionOK = window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
|
||||
const libsReady =
|
||||
typeof window.gsap !== 'undefined' &&
|
||||
typeof window.ScrollTrigger !== 'undefined' &&
|
||||
typeof window.Lenis !== 'undefined';
|
||||
|
||||
if (!wideEnough || !motionOK || !libsReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { gsap, ScrollTrigger, Lenis } = window;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Marks the page as "the film is running": disables story.css reveal and
|
||||
// activates the story-film.css pre-hide + scene layout.
|
||||
root.classList.add('film-ready');
|
||||
|
||||
// Smooth momentum scroll, bridged into ScrollTrigger so scrubbing tracks
|
||||
// the eased scroll position rather than raw wheel deltas.
|
||||
const lenis = new Lenis({ duration: 1.1, smoothWheel: true });
|
||||
lenis.on('scroll', ScrollTrigger.update);
|
||||
gsap.ticker.add((time) => lenis.raf(time * 1000));
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
// One scrubbed entrance timeline per scene: beats rise + fade in as the
|
||||
// scene crosses the viewport. Reversible — scrolling back re-hides them,
|
||||
// which reads as the camera moving off the scene.
|
||||
gsap.utils.toArray('[data-scene]').forEach((scene) => {
|
||||
const beats = scene.querySelectorAll(REVEAL);
|
||||
if (!beats.length) return;
|
||||
|
||||
gsap.set(beats, { opacity: 0, y: 40 });
|
||||
gsap.to(beats, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
ease: 'power2.out',
|
||||
stagger: 0.12,
|
||||
scrollTrigger: {
|
||||
trigger: scene,
|
||||
start: 'top 78%',
|
||||
end: 'top 32%',
|
||||
scrub: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Title-card scenes pin for a short hold before releasing to the next
|
||||
// scene. Only used on short scenes (open + close) so pinned content never
|
||||
// exceeds the viewport.
|
||||
gsap.utils.toArray('[data-scene-pin]').forEach((scene) => {
|
||||
ScrollTrigger.create({
|
||||
trigger: scene,
|
||||
start: 'top top',
|
||||
end: '+=60%',
|
||||
pin: true,
|
||||
pinSpacing: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Pins change total scroll height; recompute all triggers once now and on
|
||||
// full load (fonts/images can shift layout after this script runs).
|
||||
ScrollTrigger.refresh();
|
||||
window.addEventListener('load', () => ScrollTrigger.refresh(), { passive: true });
|
||||
|
||||
// Safety net (mirrors story.js): if the opening scene's beats are still
|
||||
// hidden shortly after load while in view, something went wrong building
|
||||
// the timelines — force everything open.
|
||||
window.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
window.setTimeout(() => {
|
||||
const firstBeat = document.querySelector('[data-scene] ' + REVEAL);
|
||||
if (!firstBeat) return;
|
||||
const hidden = parseFloat(getComputedStyle(firstBeat).opacity) === 0;
|
||||
const rect = firstBeat.getBoundingClientRect();
|
||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
||||
if (hidden && inView) failOpen();
|
||||
}, 400);
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
} catch {
|
||||
// Any failure: reveal everything so content is never trapped hidden.
|
||||
failOpen();
|
||||
}
|
||||
})();
|
||||
+8
-6
@@ -19,8 +19,10 @@
|
||||
}
|
||||
|
||||
/* Hidden starting state. Only applied once JS can guarantee a later reveal, so
|
||||
content is never trapped invisible for visitors without the enhancement. */
|
||||
html.js-ready [data-reveal] {
|
||||
content is never trapped invisible for visitors without the enhancement. The
|
||||
`:not(.film-ready)` guard hands motion to the GSAP film layer (story-film.js)
|
||||
when it activates, so beats are never hidden by two engines at once. */
|
||||
html.js-ready:not(.film-ready) [data-reveal] {
|
||||
opacity: 0;
|
||||
/* translateY only — no height/display change, so nothing reflows (zero CLS). */
|
||||
transform: translateY(var(--reveal-distance));
|
||||
@@ -32,14 +34,14 @@ html.js-ready [data-reveal] {
|
||||
|
||||
/* Closing "Lời kết" variant: same gentle rise, longer duration to let the warm
|
||||
final beat settle rather than snap in. */
|
||||
html.js-ready [data-reveal='soft'] {
|
||||
html.js-ready:not(.film-ready) [data-reveal='soft'] {
|
||||
transition:
|
||||
opacity var(--reveal-duration-soft) var(--reveal-ease),
|
||||
transform var(--reveal-duration-soft) var(--reveal-ease);
|
||||
}
|
||||
|
||||
/* Revealed state added by story.js when the element (or its group) enters view. */
|
||||
html.js-ready [data-reveal].is-visible {
|
||||
html.js-ready:not(.film-ready) [data-reveal].is-visible {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
@@ -49,8 +51,8 @@ html.js-ready [data-reveal].is-visible {
|
||||
explicitly (not relying on the global duration override in styles.css) so
|
||||
[data-reveal] can never render invisible for these visitors. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html.js-ready [data-reveal],
|
||||
html.js-ready [data-reveal='soft'] {
|
||||
html.js-ready:not(.film-ready) [data-reveal],
|
||||
html.js-ready:not(.film-ready) [data-reveal='soft'] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition: none;
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
// The scroll-driven film (story-film.js, which runs before this script) owns
|
||||
// the motion when active — it sets `html.film-ready`. Bail so the two engines
|
||||
// never animate the same beats. story.css also disables its reveal under
|
||||
// `.film-ready`, so leaving early here changes nothing for those visitors.
|
||||
if (root.classList.contains('film-ready')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No IntersectionObserver → skip the whole enhancement. Because story.css
|
||||
// hides content only under `html.js-ready` (never added here in that case),
|
||||
// everything stays visible.
|
||||
|
||||
+190
-168
@@ -6,7 +6,7 @@
|
||||
<title>Tiến Nguyễn Minh | Mô tả & hồ sơ hẹn hò</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Mô tả công việc cho vị trí người yêu tương lai và hồ sơ hẹn hò dí dỏm của Tiến Nguyễn Minh — tất cả trong một trang."
|
||||
content="Mô tả công việc cho vị trí người yêu tương lai và hồ sơ hẹn hò dí dỏm của Tiến Nguyễn Minh — kể như một thước phim ngắn."
|
||||
/>
|
||||
<link rel="icon" href="assets/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
@@ -21,7 +21,7 @@
|
||||
<meta property="og:title" content="Tiến Nguyễn Minh | Mô tả & hồ sơ hẹn hò" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Mô tả công việc cho vị trí người yêu tương lai và hồ sơ hẹn hò dí dỏm của Tiến Nguyễn Minh — tất cả trong một trang."
|
||||
content="Mô tả công việc cho vị trí người yêu tương lai và hồ sơ hẹn hò dí dỏm của Tiến Nguyễn Minh — kể như một thước phim ngắn."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
@@ -63,7 +63,16 @@
|
||||
<link rel="stylesheet" href="assets/styles.css" />
|
||||
<link rel="stylesheet" href="assets/story.css" />
|
||||
<link rel="stylesheet" href="assets/story-chrome.css" />
|
||||
<link rel="stylesheet" href="assets/story-film.css" />
|
||||
<script src="assets/theme-switch.js" defer></script>
|
||||
<!-- Film engine libraries (CDN, versioned). Load before story-film.js.
|
||||
If any fails, story-film.js bails and the static reveal (story.js) runs. -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/lenis@1.1.13/dist/lenis.min.js" defer></script>
|
||||
<!-- story-film.js decides whether the film activates; must run before story.js
|
||||
so story.js can defer to it. story.js is the reduced-motion / mobile / no-CDN fallback. -->
|
||||
<script src="assets/story-film.js" defer></script>
|
||||
<script src="assets/story.js" defer></script>
|
||||
<script src="assets/story-chrome.js" defer></script>
|
||||
</head>
|
||||
@@ -84,9 +93,10 @@
|
||||
|
||||
<div class="header-actions">
|
||||
<nav aria-label="Điều hướng chính">
|
||||
<a href="#home">Trang chủ</a>
|
||||
<a href="#jd">Mô tả công việc</a>
|
||||
<a href="#cv">Hồ sơ cá nhân</a>
|
||||
<a href="#home">Mở đầu</a>
|
||||
<a href="#applicant">Con người</a>
|
||||
<a href="#offer">Lời mời</a>
|
||||
<a href="#dossier">Chi tiết</a>
|
||||
</nav>
|
||||
|
||||
<div class="preference-controls">
|
||||
@@ -115,166 +125,36 @@
|
||||
</header>
|
||||
|
||||
<main id="content">
|
||||
<!-- ============ Trang chủ ============ -->
|
||||
<section class="home-hero" id="home">
|
||||
<!-- ============ Cảnh 1 · Cold open: vị trí bỏ trống ============ -->
|
||||
<section class="home-hero film-scene" id="home" data-scene data-scene-pin>
|
||||
<div class="site-shell hero-copy" data-reveal-group>
|
||||
<p class="chapter-tag" data-reveal>Chương 1 · Lời mời</p>
|
||||
<p class="chapter-tag" data-reveal>Cảnh 1 · Vị trí bỏ trống</p>
|
||||
<p class="eyebrow" data-reveal>Vị trí mở / bạn đồng hành dài hạn</p>
|
||||
<h1 data-reveal>Tiến Nguyễn Minh</h1>
|
||||
<p class="lead" data-reveal>
|
||||
Một bộ hồ sơ nhỏ gửi đến một người yêu tương lai: vừa là mô tả công việc, vừa là hồ sơ
|
||||
cá nhân, vừa là lời mời nghiêm túc để cùng xây điều tử tế.
|
||||
Một vị trí đã bỏ trống hơi lâu. Một anh dev hay ở nhà, cũng muốn đi chơi đấy — nhưng
|
||||
lười đi một mình. Đang tìm một người cùng đi đường dài, nghiêm túc hơn cả cách mình
|
||||
cày event Genshin.
|
||||
</p>
|
||||
<div class="button-row" data-reveal>
|
||||
<a class="button" href="#jd">Đọc mô tả công việc</a>
|
||||
<a class="button secondary" href="#cv">Xem hồ sơ cá nhân</a>
|
||||
<a class="button" href="#applicant">Gặp ứng viên</a>
|
||||
<a class="button secondary" href="#offer">Đọc lời mời</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="site-shell page-section section-grid" aria-labelledby="packet-title" data-reveal-group>
|
||||
<div data-reveal>
|
||||
<p class="chapter-tag">Chương 2 · Bộ hồ sơ</p>
|
||||
<p class="eyebrow">Bộ hồ sơ ứng tuyển</p>
|
||||
<h2 id="packet-title">Một trò đùa nghiêm túc, được quản lý nhẹ nhàng.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel strong" data-reveal>
|
||||
<h3>Dành cho người yêu tương lai</h3>
|
||||
<p>
|
||||
Mô tả công việc nói rõ quyền lợi của vị trí này: sự ổn định, chú ý, hài hước, chăm
|
||||
sóc cảm xúc, và một người xem việc quan tâm là công việc hằng ngày chứ không phải màn
|
||||
ra mắt.
|
||||
</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<h3>Dành cho hội đồng tuyển chọn</h3>
|
||||
<p>
|
||||
Hồ sơ cá nhân tóm tắt ứng viên: kỹ sư phần mềm, người nghĩ bằng hệ thống, kiên nhẫn
|
||||
gỡ lỗi, và một con người vẫn đều đặn chỉnh sửa thói quen chưa tốt.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Mô tả công việc ============ -->
|
||||
<section class="site-shell page-section jd-hero" id="jd" aria-labelledby="jd-title" data-reveal-group>
|
||||
<p class="chapter-tag" data-reveal>Chương 3 · Mô tả công việc</p>
|
||||
<p class="eyebrow" data-reveal>Mô tả công việc</p>
|
||||
<h2 class="page-title" id="jd-title" data-reveal>Người yêu tương lai</h2>
|
||||
<p class="lead" data-reveal>
|
||||
Đây là một vị trí dài hạn, làm việc trực tiếp với con người. Gói đãi ngộ chủ yếu gồm sự
|
||||
chú ý, lòng chung thủy, những bữa ăn chung, chuyện cười riêng, và một người biết thử lại
|
||||
sau sai lầm.
|
||||
</p>
|
||||
<div class="meta-grid" role="group" aria-label="Tóm tắt vị trí" data-reveal>
|
||||
<div><span>Loại hình</span>Toàn thời gian bằng trái tim, lịch linh hoạt</div>
|
||||
<div><span>Địa điểm</span>Chủ yếu ở Trái Đất, đôi khi tại Sài Gòn</div>
|
||||
<div><span>Ngày bắt đầu</span>Khi niềm tin qua vòng xem xét</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="site-shell page-section section-grid" aria-labelledby="benefits-title" data-reveal-group>
|
||||
<div data-reveal>
|
||||
<p class="eyebrow">Quyền lợi</p>
|
||||
<h2 id="benefits-title">Bạn sẽ nhận được.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel" data-reveal>
|
||||
<p>Ưu tiên hỗ trợ trong những ngày khó, ý tưởng lạ, và các buổi tối yên tĩnh.</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<p>Sự thành thật đều đặn, giao tiếp rõ ràng, và báo lỗi không đổ lỗi.</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<p>
|
||||
Một người có thể gỡ lỗi hệ thống đang vận hành và cũng nghĩ hơi nhiều chuyện ăn gì
|
||||
tối nay.
|
||||
</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<p>Lộ trình phát triển dài hạn: khỏe hơn, đi nhiều hơn, và có nhiều câu chuyện hơn.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Cảnh 2 · Nhân vật chính ============ -->
|
||||
<section
|
||||
class="site-shell page-section section-grid"
|
||||
aria-labelledby="responsibilities-title"
|
||||
class="site-shell page-section section-grid film-scene"
|
||||
id="applicant"
|
||||
aria-labelledby="applicant-title"
|
||||
data-scene
|
||||
data-reveal-group
|
||||
>
|
||||
<div data-reveal>
|
||||
<p class="eyebrow">Trách nhiệm</p>
|
||||
<h2 id="responsibilities-title">Điều cả hai cùng bảo vệ.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel strong" data-reveal>
|
||||
<ul>
|
||||
<li>Xây niềm tin bằng lời nói thẳng thắn và những hành động nhỏ lặp lại.</li>
|
||||
<li>
|
||||
Tôn trọng thời gian riêng, tham vọng nghề nghiệp, gia đình, bạn bè, và các nghi
|
||||
thức cá nhân.
|
||||
</li>
|
||||
<li>Mang theo sự tò mò, lòng tử tế, và thiện chí sửa chữa sau mâu thuẫn.</li>
|
||||
<li>
|
||||
Cùng duy trì một đời sống nơi cả hai được nghiêm túc và ngớ ngẩn đúng lúc.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="site-shell page-section section-grid" aria-labelledby="process-title" data-reveal-group>
|
||||
<div data-reveal>
|
||||
<p class="eyebrow">Điểm cộng</p>
|
||||
<h2 id="process-title">Điểm cộng thì vui, không có cũng chẳng sao.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel" data-reveal>
|
||||
<ul>
|
||||
<li>
|
||||
Thích đồ ăn, đi bộ, trò chơi, phim, hoặc học những thứ lạ không vì hạn chót nào
|
||||
cả.
|
||||
</li>
|
||||
<li>
|
||||
Có thể cười với một bản nhại công sở mà không biến tình yêu thành OKR xét theo
|
||||
quý.
|
||||
</li>
|
||||
<li>Tin rằng tình cảm chạy tốt hơn nhờ kiên nhẫn, không phải trò đoán ý.</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<h3>Ghi chú tuyển chọn</h3>
|
||||
<p>
|
||||
Không có bài kiểm tra áp lực. Không có vòng giải đố ẩn. Ứng viên mạnh là người vẫn
|
||||
tử tế khi mệt, nói thẳng khi rối, và sẵn sàng chọn nhau trong những khoảnh khắc nhỏ
|
||||
rất đời thường.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Hồ sơ cá nhân ============ -->
|
||||
<section class="site-shell page-section cv-hero" id="cv" aria-labelledby="cv-title" data-reveal-group>
|
||||
<p class="chapter-tag" data-reveal>Chương 4 · Hồ sơ ứng viên</p>
|
||||
<p class="eyebrow" data-reveal>Hồ sơ ứng viên</p>
|
||||
<h2 class="page-title" id="cv-title" data-reveal>Tiến Nguyễn Minh</h2>
|
||||
<p class="lead" data-reveal>
|
||||
Một anh dev hơi cù lần, thiếu kĩ năng sống. Hay ở nhà, nghiện máy tính — cũng muốn đi
|
||||
chơi đấy, nhưng lười đi một mình. Đang tìm một mối quan hệ lâu dài, nghiêm túc hơn cả
|
||||
cách mình cày event Genshin.
|
||||
</p>
|
||||
<div class="meta-grid" role="group" aria-label="Tóm tắt ứng viên" data-reveal>
|
||||
<div><span>Tuổi</span>26 — sinh năm 1999, tuổi con 🐈</div>
|
||||
<div><span>Tìm kiếm</span>Mối quan hệ lâu dài</div>
|
||||
<div><span>Khu vực hẹn hò</span>TPHCM hoặc Long An cũ</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="site-shell page-section section-grid" aria-labelledby="strengths-title" data-reveal-group>
|
||||
<div data-reveal>
|
||||
<p class="chapter-tag">Cảnh 2 · Nhân vật chính</p>
|
||||
<p class="eyebrow">Chân dung nhanh</p>
|
||||
<h2 id="strengths-title">Có gì trong hồ sơ này?</h2>
|
||||
<h2 id="applicant-title">Ứng viên là ai?</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel" data-reveal>
|
||||
@@ -299,6 +179,23 @@
|
||||
có bài lót sẵn, kể cả những tâm trạng chưa đặt tên.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Cảnh 3 · Thành thật từ đầu ============ -->
|
||||
<section
|
||||
class="site-shell page-section section-grid film-scene"
|
||||
id="honest"
|
||||
aria-labelledby="honest-title"
|
||||
data-scene
|
||||
data-reveal-group
|
||||
>
|
||||
<div data-reveal>
|
||||
<p class="chapter-tag">Cảnh 3 · Thành thật từ đầu</p>
|
||||
<p class="eyebrow">Nói thẳng</p>
|
||||
<h2 id="honest-title">Nói thẳng, để đỡ mất công đoán.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel" data-reveal>
|
||||
<h3>Sống đơn giản</h3>
|
||||
<p>
|
||||
@@ -315,19 +212,158 @@
|
||||
</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<h3>Thành thật từ đầu</h3>
|
||||
<h3>Thành thật đến cùng</h3>
|
||||
<p>
|
||||
Rất thích quan điểm của Phật giáo nhưng chưa/không quy y. Sống hơi tâm linh, và
|
||||
cũng hơi… tà răm — fetish là chân 🌚.
|
||||
cũng hơi… tà răm — fetish là chân 🌚. 162cm là số đo thật, không cộng dép.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="site-shell page-section section-grid" aria-labelledby="facts-title" data-reveal-group>
|
||||
<!-- ============ Cảnh 4 · Lời mời làm việc (quyền lợi) ============ -->
|
||||
<section
|
||||
class="site-shell page-section film-scene"
|
||||
id="offer"
|
||||
aria-labelledby="offer-title"
|
||||
data-scene
|
||||
data-reveal-group
|
||||
>
|
||||
<p class="chapter-tag" data-reveal>Cảnh 4 · Lời mời làm việc</p>
|
||||
<p class="eyebrow" data-reveal>Người yêu tương lai · Quyền lợi</p>
|
||||
<h2 class="page-title" id="offer-title" data-reveal>Bạn sẽ nhận được.</h2>
|
||||
<p class="lead" data-reveal>
|
||||
Đây là một vị trí dài hạn, làm việc trực tiếp với con người. Gói đãi ngộ chủ yếu gồm sự
|
||||
chú ý, lòng chung thủy, những bữa ăn chung, chuyện cười riêng, và một người biết thử lại
|
||||
sau sai lầm.
|
||||
</p>
|
||||
<div class="meta-grid" role="group" aria-label="Tóm tắt vị trí" data-reveal>
|
||||
<div><span>Loại hình</span>Toàn thời gian bằng trái tim, lịch linh hoạt</div>
|
||||
<div><span>Địa điểm</span>Chủ yếu ở Trái Đất, đôi khi tại Sài Gòn</div>
|
||||
<div><span>Ngày bắt đầu</span>Khi niềm tin qua vòng xem xét</div>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel" data-reveal>
|
||||
<p>Ưu tiên hỗ trợ trong những ngày khó, ý tưởng lạ, và các buổi tối yên tĩnh.</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<p>Sự thành thật đều đặn, giao tiếp rõ ràng, và báo lỗi không đổ lỗi.</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<p>
|
||||
Một người có thể gỡ lỗi hệ thống đang vận hành và cũng nghĩ hơi nhiều chuyện ăn gì
|
||||
tối nay.
|
||||
</p>
|
||||
</article>
|
||||
<article class="panel" data-reveal>
|
||||
<p>Lộ trình phát triển dài hạn: khỏe hơn, đi nhiều hơn, và có nhiều câu chuyện hơn.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Cảnh 5 · Điều cả hai cùng giữ (trách nhiệm) ============ -->
|
||||
<section
|
||||
class="site-shell page-section section-grid film-scene"
|
||||
id="shared"
|
||||
aria-labelledby="shared-title"
|
||||
data-scene
|
||||
data-reveal-group
|
||||
>
|
||||
<div data-reveal>
|
||||
<p class="chapter-tag">Cảnh 5 · Điều cả hai cùng giữ</p>
|
||||
<p class="eyebrow">Trách nhiệm</p>
|
||||
<h2 id="shared-title">Điều cả hai cùng bảo vệ.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel strong" data-reveal>
|
||||
<ul>
|
||||
<li>Xây niềm tin bằng lời nói thẳng thắn và những hành động nhỏ lặp lại.</li>
|
||||
<li>
|
||||
Tôn trọng thời gian riêng, tham vọng nghề nghiệp, gia đình, bạn bè, và các nghi
|
||||
thức cá nhân.
|
||||
</li>
|
||||
<li>Mang theo sự tò mò, lòng tử tế, và thiện chí sửa chữa sau mâu thuẫn.</li>
|
||||
<li>
|
||||
Cùng duy trì một đời sống nơi cả hai được nghiêm túc và ngớ ngẩn đúng lúc.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Cảnh 6 · Không có bài kiểm tra áp lực (điểm cộng + cao trào) ============ -->
|
||||
<section
|
||||
class="site-shell page-section section-grid film-scene"
|
||||
id="no-test"
|
||||
aria-labelledby="no-test-title"
|
||||
data-scene
|
||||
data-reveal-group
|
||||
>
|
||||
<div data-reveal>
|
||||
<p class="chapter-tag">Cảnh 6 · Không có bài kiểm tra áp lực</p>
|
||||
<p class="eyebrow">Điểm cộng</p>
|
||||
<h2 id="no-test-title">Điểm cộng thì vui, không có cũng chẳng sao.</h2>
|
||||
</div>
|
||||
<div class="panel-list">
|
||||
<article class="panel" data-reveal>
|
||||
<ul>
|
||||
<li>
|
||||
Thích đồ ăn, đi bộ, trò chơi, phim, hoặc học những thứ lạ không vì hạn chót nào
|
||||
cả.
|
||||
</li>
|
||||
<li>
|
||||
Có thể cười với một bản nhại công sở mà không biến tình yêu thành OKR xét theo
|
||||
quý.
|
||||
</li>
|
||||
<li>Tin rằng tình cảm chạy tốt hơn nhờ kiên nhẫn, không phải trò đoán ý.</li>
|
||||
<li>Về tử vi: tuổi 🐈 này hợp các tuổi 🐒 🐅 🐍 🐎 🐖 hoặc 🐉 🐕 — nhưng hợp sóng vẫn hơn hợp tuổi.</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="panel strong" data-reveal>
|
||||
<h3>Ghi chú tuyển chọn</h3>
|
||||
<p>
|
||||
Không có bài kiểm tra áp lực. Không có vòng giải đố ẩn. Ứng viên mạnh là người vẫn
|
||||
tử tế khi mệt, nói thẳng khi rối, và sẵn sàng chọn nhau trong những khoảnh khắc nhỏ
|
||||
rất đời thường.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Cảnh 7 · Lời kết ============ -->
|
||||
<section
|
||||
class="site-shell page-section section-grid film-scene"
|
||||
id="closing"
|
||||
aria-labelledby="closing-title"
|
||||
data-scene
|
||||
data-scene-pin
|
||||
data-reveal-group
|
||||
>
|
||||
<div data-reveal="soft">
|
||||
<p class="chapter-tag">Cảnh 7 · Lời kết</p>
|
||||
<p class="eyebrow">Lời kết</p>
|
||||
<h2 id="closing-title">Kết nối để tìm hiểu thêm nhé.</h2>
|
||||
</div>
|
||||
<article class="panel strong" data-reveal="soft">
|
||||
<p>
|
||||
Cảm ơn vì đã đến. Thước phim chỉ kể được một phần thôi — nếu thấy có chút hợp sóng, cứ
|
||||
kết nối để mình tìm hiểu nhau thêm nhé.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============ Phụ lục · Hồ sơ chi tiết (credits roll) ============ -->
|
||||
<section
|
||||
class="site-shell page-section section-grid film-scene"
|
||||
id="dossier"
|
||||
aria-labelledby="dossier-title"
|
||||
data-scene
|
||||
data-reveal-group
|
||||
>
|
||||
<div data-reveal>
|
||||
<p class="chapter-tag">Phụ lục · Hồ sơ chi tiết</p>
|
||||
<p class="eyebrow">Thông tin nhanh</p>
|
||||
<h2 id="facts-title">Để đỡ mất công đoán.</h2>
|
||||
<h2 id="dossier-title">Để đỡ mất công đoán.</h2>
|
||||
</div>
|
||||
<dl class="facts-list" data-reveal>
|
||||
<div>
|
||||
@@ -404,20 +440,6 @@
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="site-shell page-section section-grid" aria-labelledby="closing-title" data-reveal-group>
|
||||
<div data-reveal="soft">
|
||||
<p class="chapter-tag">Chương 5 · Lời kết</p>
|
||||
<p class="eyebrow">Lời kết</p>
|
||||
<h2 id="closing-title">Kết nối để tìm hiểu thêm nhé.</h2>
|
||||
</div>
|
||||
<article class="panel strong" data-reveal="soft">
|
||||
<p>
|
||||
Cảm ơn vì đã đến. Hồ sơ chỉ kể được một phần thôi — nếu thấy có chút hợp sóng, cứ kết
|
||||
nối để mình tìm hiểu nhau thêm nhé.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Phase 1 — Foundation
|
||||
|
||||
Goal: load the film stack and establish the fail-open, reduced-motion-safe
|
||||
engine boundary. No scenes yet — just the scaffolding that guarantees safety.
|
||||
|
||||
## Context links
|
||||
|
||||
- Current reveal engine: `assets/story.js`, `assets/story.css`
|
||||
- Pre-paint bootstrap + `js-ready` gate: `index.html` inline `<script>` (lines ~39-62)
|
||||
- Chrome to reconcile later: `assets/story-chrome.js`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Load GSAP core, ScrollTrigger, Lenis via CDN with `defer` (mirroring
|
||||
`theme-switch.js`), pinned to explicit versions.
|
||||
- Create `assets/story-film.js` that:
|
||||
- Bails out (leaving content fully visible) if GSAP/ScrollTrigger/Lenis
|
||||
absent, or `prefers-reduced-motion: reduce`, or `IntersectionObserver`
|
||||
unsupported.
|
||||
- When active: adds a distinct root flag (e.g. `html.film-ready`) so
|
||||
`story-film.css` owns the hidden/pinned states — never `story.css`.
|
||||
- Bridges Lenis → ScrollTrigger: drive `lenis.raf` from GSAP ticker, call
|
||||
`ScrollTrigger.update` on Lenis scroll, `lenis.on('scroll', ...)`.
|
||||
- Create `assets/story-film.css` owning film-only layout, all scoped under
|
||||
`html.film-ready`, with a `prefers-reduced-motion` neutralizer.
|
||||
|
||||
## Files
|
||||
|
||||
- Create: `assets/story-film.js`, `assets/story-film.css`
|
||||
- Modify: `index.html` (CDN `<script>` tags, new `<link>`/`<script>` for the two new assets)
|
||||
|
||||
## Implementation steps
|
||||
|
||||
1. Add CDN tags for GSAP, ScrollTrigger, Lenis (versioned) + new local assets.
|
||||
2. Implement the guard/bail-out and the Lenis⇄GSAP ticker bridge in
|
||||
`story-film.js`; set `html.film-ready` only on success.
|
||||
3. Add `story-film.css` skeleton (scene container, pin wrapper) scoped under
|
||||
`html.film-ready`; reduced-motion media query neutralizes it.
|
||||
4. Ensure `story.js` reveal and `story-film.js` never both animate the same
|
||||
node (coordination finalized in Phase 2).
|
||||
|
||||
## Validation
|
||||
|
||||
- JS off / CDN blocked (DevTools request block): page = today's document.
|
||||
- `prefers-reduced-motion: reduce`: `html.film-ready` NOT set; static content.
|
||||
- Motion allowed, modern browser: `html.film-ready` set, Lenis scrolling smooth,
|
||||
no console errors. No scenes animate yet — just smooth scroll + flag.
|
||||
|
||||
## Risks / rollback
|
||||
|
||||
- Lenis/ScrollTrigger version mismatch → pin known-compatible versions.
|
||||
- Rollback: remove new tags + two new files.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Phase 2 — Scene conversion (the story arc)
|
||||
|
||||
Goal: rearrange the EXISTING information into a short-film narrative arc, then
|
||||
bind each scene to a pinned/scrubbed GSAP timeline. Nothing invented, nothing
|
||||
dropped — only reordered and reframed so the page opens on the human and builds
|
||||
to sincerity.
|
||||
|
||||
## Why rearrange
|
||||
|
||||
Current order: Invitation → Packet(meta) → full JD → full CV → Close. That
|
||||
front-loads the corporate conceit and saves the person for the end. A film opens
|
||||
on a protagonist + a want, earns interest, then makes the pitch and turns
|
||||
emotional. New order leads with the human ache, then presents "the offer,"
|
||||
then lands sincerity.
|
||||
|
||||
## New arc (7 scenes)
|
||||
|
||||
Vietnamese keeps the existing witty tone; source = current index.html content.
|
||||
|
||||
### Scene 1 — Cold open: "Vị trí bỏ trống đã lâu"
|
||||
- Beat: a single spotlit line — a position has stayed open too long.
|
||||
- Hook, funny + a little lonely.
|
||||
- Source: hero eyebrow "Vị trí mở / bạn đồng hành dài hạn" + CV lead
|
||||
("lười đi một mình", "nghiêm túc hơn cả cách mình cày event Genshin").
|
||||
|
||||
### Scene 2 — Nhân vật chính (the applicant reveal)
|
||||
- Beat: quick, funny self-portrait — the honest introvert dev.
|
||||
- Source: name, 26 / 1999 / tuổi 🐈, Quận 7, "công nhân đánh máy tại Xí nghiệp
|
||||
gêm Vê Nờ Gờ", wibu/isekai, Genshin/TFT/T1-Keria, eclectic playlist.
|
||||
(from CV "Chân dung nhanh" panels — condensed into character texture.)
|
||||
|
||||
### Scene 3 — Thành thật từ đầu (vulnerability beat)
|
||||
- Beat: flaws + truths on the table — the move that makes it real AND funny.
|
||||
- Source: past 3-yr relationship, healed & ready; frugal; no smoking / occasional
|
||||
drink; Phật giáo but not ordained; and the disarming punchlines — 162cm
|
||||
"không cộng dép", không thích thú cưng, fetish là chân 🌚.
|
||||
|
||||
### Scene 4 — Lời mời làm việc (the pitch / benefits)
|
||||
- Beat: reframe JD benefits as promises to the reader.
|
||||
- Source: JD "Quyền lợi" (support on hard days, honest blameless comms, someone
|
||||
who debugs prod and overthinks dinner, long-term growth roadmap).
|
||||
|
||||
### Scene 5 — Điều cả hai cùng giữ (responsibilities, shared)
|
||||
- Beat: the "we both protect this" reciprocity beat.
|
||||
- Source: JD "Trách nhiệm" (trust via small repeated actions, respect for time /
|
||||
family / friends, curiosity + repair after conflict, serious-and-silly).
|
||||
|
||||
### Scene 6 — Không có bài kiểm tra áp lực (criteria + climax)
|
||||
- Beat: joke drops away, sincerity peaks — the emotional turn.
|
||||
- Source: JD "Điểm cộng" + "Ghi chú tuyển chọn" (no pressure test, no hidden
|
||||
puzzle; strong applicant = kind when tired, honest when tangled); zodiac
|
||||
compatibility gag (tuổi 🐒🐅🐍🐎🐖 / 🐉🐕) as a light exit-laugh.
|
||||
|
||||
### Scene 7 — Lời kết (resolution / closing card)
|
||||
- Beat: warm landing + call to connect.
|
||||
- Source: current Chương 5 closing copy. Optional confetti payoff (open Q).
|
||||
|
||||
## Full-facts handling
|
||||
|
||||
The `facts-list` (`<dl>`: height, weight, hometown, languages, education, etc.)
|
||||
is reference data, not a dramatic beat. Keep it as a **"credits roll" / dossier
|
||||
appendix after Scene 7** — collapsible or a quieter final panel — so the film
|
||||
stays lean but the completeness is preserved.
|
||||
|
||||
## Implementation steps
|
||||
|
||||
1. Re-sequence sections in `index.html` to the 7-scene order (semantic reorder;
|
||||
keep all copy as real DOM text).
|
||||
2. Wrap each scene in a pin container; author one GSAP timeline per scene with
|
||||
scrubbed beats (headline in, body/panels stagger, exit).
|
||||
3. Update chapter tags (Chương 1..7 or rename to scene titles).
|
||||
4. Keep `data-reveal` markup as the reduced-motion / no-JS fallback path.
|
||||
|
||||
## Validation
|
||||
|
||||
- Reading order (JS off) tells the same story top-to-bottom and is complete.
|
||||
- Each scene pins and scrubs; no orphaned/empty beats.
|
||||
- Facts dossier reachable and readable; nothing from the original is lost.
|
||||
|
||||
## Risks / rollback
|
||||
|
||||
- Reordering may disturb in-page anchors (#jd, #cv) used by nav → update nav +
|
||||
active-nav id list in `story-chrome.js` (Phase 3).
|
||||
- Rollback: revert index.html section order; scenes are additive wrappers.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Plan: Whole-page scroll-driven "short film" story
|
||||
|
||||
**Status:** Implemented — verified in headless Chrome (film activates, no console errors, fallback intact)
|
||||
**Created:** 2026-07-24
|
||||
**Branch:** main
|
||||
**Slug:** scroll-film-story
|
||||
|
||||
## Implementation notes (decisions made during build)
|
||||
|
||||
- **Pinning limited to title cards.** Full-viewport pinning of content-heavy
|
||||
scenes risks clipping/jank when content exceeds the viewport, so only the two
|
||||
short scenes (`#home`, `#closing`, tagged `data-scene-pin`) pin. Every scene
|
||||
still gets a scrubbed entrance timeline. Ask if you want more scenes pinned.
|
||||
- **Dropped the "Bộ hồ sơ" packet-explainer** (old Chương 2). It explained the
|
||||
JD/CV joke rather than being a fact about the person; the new arc makes the
|
||||
conceit implicit. No personal facts lost — the full dossier retains all 18.
|
||||
- **Mobile / reduced-motion:** film gates OFF below 768px or under reduced
|
||||
motion; the existing `story.js` reveal runs there unchanged (no dynamic
|
||||
resize upgrade without reload — accepted).
|
||||
- Orphaned `.jd-hero` / `.cv-hero` CSS rules left in styles.css (harmless dead
|
||||
rules; classes no longer used).
|
||||
|
||||
## Goal
|
||||
|
||||
Convert the existing witty JD/CV one-pager into a **scroll-driven cinematic
|
||||
narrative**: each chapter becomes a scene that pins and its beats scrub to
|
||||
scroll position, like frames of a short film. Reader controls pacing by
|
||||
scrolling. Whole page participates (all 5 chapters), not just a hero.
|
||||
|
||||
## Decisions (locked with user)
|
||||
|
||||
- Playback: **scroll-driven** (reader scrolls to advance; scenes pin + scrub).
|
||||
- Scope: **whole page** as a film (all chapters converted).
|
||||
- Delivery: **CDN, no build step** (matches GitHub Pages / no-bundler setup).
|
||||
|
||||
## Stack
|
||||
|
||||
| Concern | Library | Why |
|
||||
|--------------------|-----------------------------|--------------------------------------------------|
|
||||
| Sequencing + pin | GSAP core + ScrollTrigger | Free (2025), CDN, battle-tested scrub/pin engine |
|
||||
| Cinematic scroll | Lenis | Momentum glide; feeds scroll value to ScrollTrigger |
|
||||
| Theme (unchanged) | assets/theme-switch.js | Keep as-is |
|
||||
|
||||
## Non-negotiable safety contract
|
||||
|
||||
Mirrors the existing `story.js` / `story.css` philosophy:
|
||||
|
||||
1. **Fail-open.** Content fully visible by default. GSAP only hides-then-reveals
|
||||
*after* it loads and confirms support. CDN/JS failure ⇒ current readable
|
||||
document, never a blank film.
|
||||
2. **`prefers-reduced-motion`.** No pin/scrub. Fall back to the existing static
|
||||
reveals (or plain final-position content). No horizontal band jank.
|
||||
3. **Accessibility/SEO preserved.** All copy stays real DOM text (no canvas),
|
||||
heading order and reading order unchanged.
|
||||
4. **Chrome reconciled.** Pinning changes total scroll height — the
|
||||
scroll-progress bar (`story-chrome.js`) and active-nav IO must stay correct
|
||||
after pins are created (ScrollTrigger.refresh + progress recompute).
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | File | Depends on |
|
||||
|---|-------|------|-----------|
|
||||
| 1 | Foundation: load stack, Lenis⇄ScrollTrigger bridge, fail-open + reduced-motion gate | phase-01-foundation.md | — |
|
||||
| 2 | Scene conversion: 5 chapters → pinned/scrubbed timelines | phase-02-scenes.md | 1 |
|
||||
| 3 | Chrome reconciliation + verification (progress bar, nav, a11y, perf) | phase-03-chrome-verify.md | 2 |
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- With JS disabled or CDN blocked: page renders as today (readable, no gaps).
|
||||
- With `prefers-reduced-motion: reduce`: no pinning; content in final position.
|
||||
- With motion allowed: each chapter pins and its beats scrub smoothly to scroll.
|
||||
- Scroll-progress bar reaches 100% exactly at page end (accounts for pin height).
|
||||
- Active-nav still highlights the correct section while scrubbing.
|
||||
- No console errors; no layout shift (CLS) on load; keyboard/reader order intact.
|
||||
|
||||
## Files touched
|
||||
|
||||
- **Modify:** `index.html` (add CDN + new asset tags; minimal scene hooks/attrs)
|
||||
- **New:** `assets/story-film.js` (GSAP/Lenis orchestration)
|
||||
- **New:** `assets/story-film.css` (pin/scene layout + reduced-motion fallback)
|
||||
- **Modify:** `assets/story.js` (disable IO reveal for filmed sections when film is active; keep as reduced-motion/no-support fallback)
|
||||
- **Modify:** `assets/story-chrome.js` (recompute progress after ScrollTrigger pins; refresh on layout change)
|
||||
|
||||
## Risks & rollback
|
||||
|
||||
- **Pin height math** breaks progress bar / nav → mitigate with
|
||||
`ScrollTrigger.refresh()` after setup and on resize; progress reads live
|
||||
scrollHeight so it self-corrects.
|
||||
- **Double animation** (story.js reveal + GSAP) → gate story.js off for filmed
|
||||
sections when the film engine initializes.
|
||||
- **Mobile jank** from pinning → allow disabling pins under a width/motion
|
||||
threshold, degrade to static reveals.
|
||||
- **Rollback:** all changes are additive files + reversible edits; `git revert`
|
||||
or delete the two new assets and their `<script>`/`<link>` tags.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. Pin on mobile (<~768px) too, or degrade to the current reveal there? (Pinning
|
||||
on small touch screens is the most common source of jank.)
|
||||
2. Keep the confetti payoff idea at Chương 5, or pure film with no gags?
|
||||
3. Pin GSAP/Lenis to specific CDN versions (reproducible) vs. latest?
|
||||
Reference in New Issue
Block a user