mcpField
C
cinematic-scroll-storytelling

cinematic-scroll-storytelling

github.com/mengtoデザイン・UI7.1k2.2k 回インストール3 日前に更新
リポジトリ

cinematic-scroll-storytelling について

GSAP、ScrollTrigger、Lenisでスクロール連動ページを作るデザイナーや開発者向けのガイドです。プリローダー、テキスト表示、視差、カードスタックを扱い、構築手順やモバイル、動きを抑える設定の確認点も示します。

主な機能

  1. 01プリローダーとヒーロー表示
  2. 02分割テキストの表示
  3. 03スクロール起動・連動モーション
  4. 04スティッキーカードスタック
  5. 05視差と固定シーン

活用シーン

  1. 01セクションごとに展開する製品ページの制作
  2. 02ポートフォリオ用の固定プロジェクトカード制作
  3. 03エディトリアルサイトのスクロール演出設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mengto/skills --skill cinematic-scroll-storytelling

インストール後、Claude Code で/cinematic-scroll-storytellingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

REFERENCES.md
demo/
index.html
PROMPT.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まで。

ページ構成

  1. プリローダー:黒い画面、プログレスバー、ブランド名/タイトル、イントロのフェード。
  2. ヒーロー:画像のパララックス、マスクによる見出し表示、控えめなスクロール案内。
  3. イントロ:単語単位のキネティックタイポグラフィ。
  4. ストーリーセクション:スクロールトリガーによるフェードアップ、ぼかしからの表示、クリップ表示。
  5. 最近のプロジェクト:スケールとレイヤーによる奥行きを使ったスティッキーカードスタック。
  6. ギャラリーまたは実績:スクロールスクラブによる横方向の表示、または段階的な表示。
  7. フッター:パララックス表示、またはゆっくりと上へ引き継ぐ演出。

マークアップのパターン

<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,
      },
    }
  );
}

実装順序

  1. まず静的なページを作成します。
  2. プリローダーとヒーローの登場演出を追加します。
  3. テキスト分割による表示を追加します。
  4. セクションごとの表示を追加します。
  5. スティッキーカードスタックの展開を追加します。
  6. パララックスレイヤーを追加します。
  7. ストーリー上必要な箇所にのみ、スクラブするピン留めシーンを追加します。
  8. モーション低減設定とタッチ操作向けのフォールバックを追加します。
  9. デスクトップとモバイルの両方で、ブラウザーを使った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() が実行されます。
  • モバイルではピン留めを簡略化し、パフォーマンスが低下する場合はピン留めを行いません。

コメント