cinematic-scroll-storytelling について
GSAP、ScrollTrigger、Lenisでスクロール連動ページを作るデザイナーや開発者向けのガイドです。プリローダー、テキスト表示、視差、カードスタックを扱い、構築手順やモバイル、動きを抑える設定の確認点も示します。
主な機能
- 01プリローダーとヒーロー表示
- 02分割テキストの表示
- 03スクロール起動・連動モーション
- 04スティッキーカードスタック
- 05視差と固定シーン
活用シーン
- 01セクションごとに展開する製品ページの制作
- 02ポートフォリオ用の固定プロジェクトカード制作
- 03エディトリアルサイトのスクロール演出設計
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mengto/ skills --skill cinematic-scroll-storytellingインストール後、Claude Code で/cinematic-scroll-storytellingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
REFERENCES.mddemo/index.htmlPROMPT.md詳細
シネマティックなスクロールストーリーテリング
使用する場面
- ユーザーがスクロールするにつれて展開する、高級感のあるエディトリアルストーリーのようなページを作りたい場合。
- ユーザーが、スクロール駆動型ストーリーテリング、スクロール連動アニメーション、スティッキーカードスタック、パララックス、テキスト分割、プリローダー、またはシネマティックな展開について言及した場合。
- ポートフォリオ、スタジオ、製品、またはランディングページで、レイヤーによる奥行きを表現しながら、セクションごとに要素を表示する必要がある場合。
- GSAP、ScrollTrigger、Lenisを使用できる場合。
エフェクト用語
- スクロール駆動型ストーリーテリング:スクロールに合わせて、セクションが順番に表示されます。
- スクロール連動アニメーション:
scrubを使い、スクロールと進行度を直接連動させます。 - スクロールトリガー型モーション:セクションがビューポートに入ると、アニメーションが開始します。
- スタガー表示:単語、行、カード、または要素を、少しずつ時間差をつけて表示します。
- プログレッシブ表示:スクロールの進行に合わせて、不透明度、スケール、ぼかし、クリップ、または位置を変化させます。
- スティッキーカードスタック:カードを固定して重ね、次のカードが現れると縮小して奥へ退かせます。
- パララックススクロール:背景レイヤーと前景レイヤーを異なる速度で動かします。
- スクロールスクラブ:
scrub: trueまたはscrub: 1を使い、アニメーションをスクロールバーの動きに追従させます。 - キネティックタイポグラフィ:マスクを使ったテキスト分割アニメーションで、通常は単語単位または行単位で動かします。
- プリローダー:オープニングのローディング画面、プログレスバー、イントロのトランジションです。
目指す印象
- 高級感のあるエディトリアルサイト。
- ハイエンドなクリエイティブスタジオのポートフォリオ。
- Appleレベルの洗練されたモーション。
- 現代的なAwwwardsのインタラクション表現。
- 没入感のあるシネマティックなランディングページ。
避けるべき表現:
- バウンス、弾性、またはバネのような動き。
- 急激なスケール変化。
- 派手なゲーム風エフェクト。
- 同時に動かすスクロールエフェクトが多すぎること。
- ページを読みづらくするスクロールの乗っ取り。
コアスタック
npm i gsap lenis
import Lenis from "lenis";
import "lenis/dist/lenis.css";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduceMotion) {
const lenis = new Lenis({
lerp: 0.08,
smoothWheel: true,
wheelMultiplier: 0.9,
});
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
}
window.addEventListener("load", () => ScrollTrigger.refresh());
モーショントークン
- 表示時のイージング:
power3.outまたはpower4.out。 - スクラブするシーン:
ease: "none"とscrub: 0.8〜1.4。 - テキスト表示の時間:
0.8s〜1.1s。 - カード表示の時間:
0.9s〜1.2s。 - 単語のスタッガー:
0.035s〜0.07s。 - 行のスタッガー:
0.08s〜0.14s。 - カードのスタッガー:
0.06s〜0.1s。 - 表示開始時のオフセット:
y: 24〜48。 - ぼかし:
4px〜10pxから0pxへ。 - スティッキーカードのスケールによる奥行き:
1から0.92まで。
ページ構成
- プリローダー:黒い画面、プログレスバー、ブランド名/タイトル、イントロのフェード。
- ヒーロー:画像のパララックス、マスクによる見出し表示、控えめなスクロール案内。
- イントロ:単語単位のキネティックタイポグラフィ。
- ストーリーセクション:スクロールトリガーによるフェードアップ、ぼかしからの表示、クリップ表示。
- 最近のプロジェクト:スケールとレイヤーによる奥行きを使ったスティッキーカードスタック。
- ギャラリーまたは実績:スクロールスクラブによる横方向の表示、または段階的な表示。
- フッター:パララックス表示、またはゆっくりと上へ引き継ぐ演出。
マークアップのパターン
<div class="preloader" data-preloader>
<div class="preloader__bar" data-preloader-bar></div>
</div>
<main>
<section class="hero" data-parallax-section>
<img data-parallax-layer data-speed="-0.18" src="/hero.jpg" alt="">
<h1 data-split-reveal>Design that unfolds with cinematic restraint.</h1>
</section>
<section data-story-section>
<p data-split-reveal="words">Every block arrives with quiet intent.</p>
</section>
<section class="project-stack" data-sticky-stack>
<article data-stack-card>Project One</article>
<article data-stack-card>Project Two</article>
<article data-stack-card>Project Three</article>
</section>
<footer data-footer-parallax>...</footer>
</main>
プリローダーのシーケンス
プリローダーでシネマティックな雰囲気を作り、その後、ヒーローの表示へとつなげます。
function initPreloader() {
const loader = document.querySelector("[data-preloader]");
const bar = document.querySelector("[data-preloader-bar]");
if (!loader) return Promise.resolve();
if (reduceMotion) {
loader.remove();
return Promise.resolve();
}
return new Promise((resolve) => {
const tl = gsap.timeline({
defaults: { ease: "power3.out" },
onComplete: () => {
loader.remove();
resolve();
},
});
tl.fromTo(bar, { scaleX: 0, transformOrigin: "left" }, { scaleX: 1, duration: 1.1 })
.to(loader, { yPercent: -100, duration: 0.9, ease: "power4.inOut" }, "+=0.15");
});
}
テキスト分割による表示
マスク付きのオーバーフローコンテナーを使用します。リンクや意味のあるインラインマークアップを含むテキストは分割しないでください。
function splitWords(element) {
if (element.dataset.splitReady === "true") return;
const text = element.textContent || "";
const parts = text.split(/(\s+)/);
element.textContent = "";
element.setAttribute("aria-label", text.trim());
parts.forEach((part) => {
if (!part.trim()) {
element.appendChild(document.createTextNode(part));
return;
}
const mask = document.createElement("span");
const word = document.createElement("span");
mask.className = "split-word-mask";
word.className = "split-word";
word.textContent = part;
mask.setAttribute("aria-hidden", "true");
mask.appendChild(word);
element.appendChild(mask);
});
element.dataset.splitReady = "true";
}
function initSplitReveals() {
if (reduceMotion) {
gsap.set("[data-split-reveal]", { autoAlpha: 1 });
return;
}
gsap.utils.toArray("[data-split-reveal]").forEach((element) => {
splitWords(element);
const words = element.querySelectorAll(".split-word");
gsap.fromTo(
words,
{ yPercent: 110, autoAlpha: 0, filter: "blur(8px)" },
{
yPercent: 0,
autoAlpha: 1,
filter: "blur(0px)",
duration: 0.95,
ease: "power4.out",
stagger: 0.05,
scrollTrigger: {
trigger: element,
start: "top 82%",
once: true,
},
}
);
});
}
.split-word-mask {
display: inline-block;
overflow: hidden;
vertical-align: top;
}
.split-word {
display: inline-block;
will-change: transform, opacity, filter;
}
スクロールトリガーによる表示
通常のセクションにはこれを使用します。動きが不安定にならないよう、落ち着いた印象で一度だけ再生するようにします。
function initSectionReveals() {
if (reduceMotion) {
gsap.set("[data-story-section], [data-reveal-item]", { autoAlpha: 1, clearProps: "all" });
return;
}
gsap.utils.toArray("[data-story-section]").forEach((section) => {
const items = section.querySelectorAll("[data-reveal-item]");
const targets = items.length ? items : section.children;
gsap.fromTo(
targets,
{ y: 36, autoAlpha: 0, filter: "blur(8px)" },
{
y: 0,
autoAlpha: 1,
filter: "blur(0px)",
duration: 1,
ease: "power4.out",
stagger: 0.08,
scrollTrigger: {
trigger: section,
start: "top 82%",
once: true,
},
}
);
});
}
スクロール連動の展開
シネマティックな展開には、スクラブするタイムラインを使用します。スクラブするアニメーションは線形に保ち、タイミングはスクロール位置に委ねます。
function initProgressionScenes() {
if (reduceMotion) return;
gsap.utils.toArray("[data-progress-scene]").forEach((scene) => {
const media = scene.querySelector("[data-progress-media]");
const copy = scene.querySelectorAll("[data-progress-copy]");
gsap.timeline({
scrollTrigger: {
trigger: scene,
start: "top top",
end: "+=140%",
scrub: 1.1,
pin: true,
anticipatePin: 1,
},
})
.fromTo(media, { scale: 1.08 }, { scale: 1, ease: "none" })
.fromTo(copy, { autoAlpha: 0, y: 40 }, { autoAlpha: 1, y: 0, stagger: 0.15, ease: "none" }, 0.15);
});
}
スティッキーカードスタック
レイアウトにはposition: stickyを使用し、レイヤーによるスケールと奥行きにはScrollTriggerを使用します。後続のカードが現れるにつれて、先行するカードを奥へ退かせます。
[data-sticky-stack] {
position: relative;
}
[data-stack-card] {
position: sticky;
top: 12vh;
transform-origin: center top;
will-change: transform, opacity;
}
function initStickyCardStack() {
if (reduceMotion) return;
gsap.utils.toArray("[data-sticky-stack]").forEach((stack) => {
const cards = gsap.utils.toArray(stack.querySelectorAll("[data-stack-card]"));
cards.forEach((card, index) => {
const nextCard = cards[index + 1];
if (!nextCard) return;
gsap.to(card, {
scale: 0.92 + index * 0.015,
autoAlpha: 0.72,
y: -24,
ease: "none",
scrollTrigger: {
trigger: nextCard,
start: "top 78%",
end: "top 24%",
scrub: true,
invalidateOnRefresh: true,
},
});
});
});
}
パララックス
ヒーロー画像、背景レイヤー、フッターの表示にパララックスを使用します。移動距離は小さく保ちます。
フッターのパララックス表示:
function initParallax() {
if (reduceMotion) return;
gsap.utils.toArray("[data-parallax-layer]").forEach((layer) => {
const speed = Number(layer.dataset.speed || -0.16);
const section = layer.closest("[data-parallax-section]") || layer;
gsap.to(layer, {
y: () => window.innerHeight * speed,
ease: "none",
scrollTrigger: {
trigger: section,
start: "top bottom",
end: "bottom top",
scrub: 1,
invalidateOnRefresh: true,
},
});
});
}
function initFooterReveal() {
if (reduceMotion) return;
const footer = document.querySelector("[data-footer-parallax]");
if (!footer) return;
gsap.fromTo(
footer,
{ yPercent: -12, autoAlpha: 0.85 },
{
yPercent: 0,
autoAlpha: 1,
ease: "none",
scrollTrigger: {
trigger: footer,
start: "top bottom",
end: "top 45%",
scrub: 1,
},
}
);
}
実装順序
- まず静的なページを作成します。
- プリローダーとヒーローの登場演出を追加します。
- テキスト分割による表示を追加します。
- セクションごとの表示を追加します。
- スティッキーカードスタックの展開を追加します。
- パララックスレイヤーを追加します。
- ストーリー上必要な箇所にのみ、スクラブするピン留めシーンを追加します。
- モーション低減設定とタッチ操作向けのフォールバックを追加します。
- デスクトップとモバイルの両方で、ブラウザーを使ったQAを行います。
プロンプトのテンプレート
Create a cinematic scroll-driven landing page with smooth Lenis scrolling, GSAP ScrollTrigger animations, staggered text reveals, sticky card stack progression, parallax backgrounds, scroll-scrubbed transitions, section-by-section storytelling, and an immersive preloader animation. Use layered depth, scaling transitions, progressive opacity changes, and smooth viewport-triggered motion for a premium editorial experience.
QA チェックリスト
- JavaScript が無効でもコンテンツを読めます。
- 視差効果を低減する設定を使用しているユーザーには、静的コンテンツが表示され、スムーズスクロールのレイヤーは表示されません。
- スクロールをトリガーとする表示アニメーションは一度だけ再生されます。
- スクロールに連動するシーンでは、
scrubを意図的に使用します。 - スティッキーカードがフッターに重なったり、ページのスクロールを妨げたりしません。
- パララックスの動きは控えめで、読みやすさを損ないません。
- 画像の読み込みが遅い場合でも、プリローダーが確実に終了します。
- 画像、フォント、レイアウトの変化後に
ScrollTrigger.refresh()が実行されます。 - モバイルではピン留めを簡略化し、パフォーマンスが低下する場合はピン留めを行いません。
コメント
Markdown 対応