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. 在桌面端和移动端进行浏览器质量检查。

提示词模板

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