Communitygithub.com

anhlamtruong/living-ui-swarm

为 React / Next.js 构建“智能代理群组”页面滚动构建动画。一队带有角色颜色标识的光标(规划者、编码者、设计者、布局者、测试者、审查者、部署者)在页面滚动时逐段组装内容。

living-ui-swarm 是什么?

Living UI Swarm 是一个用于 React/Next.js 应用的动画技能,能够在用户滚动页面时,模拟一队 AI 智能代理(光标)实时构建页面内容的效果。此技能的核心原理是在固定 canvas 覆盖层上绘制多个带有角色标识的光标,它们以规划、构建和审查三个步骤,对页面中标记了 `data-build-*` 属性的 HTML 元素进行动画操作,包括绘制框架、打字、拖入图片、复制列表项、拾色、绘制网格等。引擎使用 GSAP 和 Lenis 实现流畅动画和滚动监听,并通过 `next/dynamic` 延迟加载,确保首屏性能。它支持 Next.js 15+、React 18+/19+、Tailwind 4,并通过 `sessionStorage` 记忆构建状态,避免重复播放。适合用于个人作品集、产品发布页等一次性滚动体验页面,但不适用于仪表盘、文档等日常高频访问页面。该技能包含约5000行 TypeScript 代码,提供完整的 DOM 合约、调优指南和架构文档。

兼容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/anhlamtruong/living-ui-skills/tree/HEAD/skills/living-ui-swarm

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

预览

来自技能 README

living-ui-swarm preview

文档

智能代理群组构建动画

一个固定 Canvas 覆盖层会绘制一小队光标,当页面的每个部分滚动到视口时,这些光标会"构建"该部分。内容始终是真实的、服务端渲染的 HTML。引擎仅在短时间内隐藏元素,并为每个元素播放一小段编排动画。

在线示例:https://www.lamanhtruong.com。其源代码固定在 portfolio-anh@a5d1651。assets/swarm/ 目录包含相同的引擎,已清理干净以便复用。

何时使用,何时不使用

在页面本身就是卖点的场景下使用:例如作品集、产品发布或产品故事页面、案例研究、"关于我们的 AI" 页面。用户只会访问一次,从上到下滚动,而构建动画会向他们传递某种信息(如"这个人通过智能代理交付产品")。

对于用户日常使用的页面则跳过:例如仪表盘、文档、设置、结账、长文阅读。每次构建会花费几秒钟,而回访用户没有理由再看一遍。这就是为什么引擎会按标签页记住已完成的构建(通过 sessionStorage),并在重新加载时不再重播。

它的开销:

  • 约5000行 TypeScript 代码。构建层通过 next/dynamic 加载,在 hydration 之后才执行,因此首屏渲染无需等待它;平滑滚动和视差部分(Lenis、GSAP、ScrollTrigger)随页面一起加载。一个包含所有四个 living-ui 技能的全新 Next.js 15 应用,其路由 JS 约为 67 kB(首次加载 170 kB)。
  • 当实时日志填充时,会有较小的布局偏移得分(在测试页面中约为 0.03,远低于 0.1 的"良好"线):新的日志行会在其固定高度的框内移动。页面其余部分不会偏移。
  • 一个额外的固定 Canvas 和一个 gsap.ticker 循环。当没有元素在运动时,循环会停止。
  • GSAP 3.13+(免费,包括 ScrollTrigger)、@gsap/react、Lenis。

一句话工作原理

一个微小的内联脚本放在 <head> 中,在首屏渲染之前设置 html[data-build="on"],CSS 随后将每个未构建的 [data-build-item] 通过 opacity: 0 隐藏。使用透明度而非 display 或 visibility 意味着没有布局偏移,也不会从无障碍树中移除任何内容。Hydration 后,引擎将页面扫描为每个 [data-build-section] 的任务图,为每个部分装备一个 ScrollTrigger,当某个部分进入视口时,执行 规划 → 构建 → 审查:

  • 规划: 规划者绘制参考线并分发任务。
  • 构建: 工作者根据距离、角色和关键路径认领任务,每个任务播放一个 GSAP 时间轴。
  • 审查: 审查者勾勒出该部分,勾选检查清单并盖章。

完成的元素和部分会获得 [data-built] 属性。如果引擎从未启动,头部脚本会在 3 秒后移除该标志,从而确保没有任何内容保持隐藏。

架构、数据流以及如何扩展:references/architecture.md。

安装(Next.js App Router)

  1. 依赖项。 已在 Next.js 15.1 + React 18.3(工作示例)和 Next.js 15.5 + React 19.1 + Tailwind 4(一个全新的 create-next-app)上测试通过,TypeScript 5.8–5.9、[email protected]–3.15、@gsap/[email protected]、[email protected]:

    npm i gsap@^3.13 @gsap/react@^2.1 lenis@^1.3
    
  2. 复制引擎。 将 assets/swarm/ 复制到 src/components/swarm/。其内部的导入路径是相对路径,因此任何文件夹都可以。

  3. 将标志脚本 添加到根布局的 <head> 中。它会在 React 进行 hydration 之前设置 <html> 上的一个属性,因此请为 <html> 添加 suppressHydrationWarning:

    // app/layout.tsx
    import { BuildFlagScript } from "@/components/swarm/build-flag-script";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en" suppressHydrationWarning>
          <head>
            <BuildFlagScript />
          </head>
          <body>{children}</body>
        </html>
      );
    }
    
  4. 只挂载一次引擎,在使用它的页面上,放在内容之后:

    // app/page.tsx(一个服务端组件;保持静态)
    import "@/components/swarm/swarm.css";
    import { SwarmRoot } from "@/components/swarm/swarm-root";
    
    export default function Page() {
      return (
        <>
          <main>{/* 使用 data-build-* 标记的部分 */}</main>
          <SwarmRoot />
        </>
      );
    }
    

    SwarmRoot 也会挂载由 gsap.ticker 驱动的 Lenis 平滑滚动。如果应用已运行 Lenis,请从 swarm-root.tsx 中删除 <SmoothScroll />。build-layer.tsx 通过 useLenis() 读取实例,这需要页面上某处有一个 root ReactLenis 实例(<ReactLenis root />,如 smooth-scroll.tsx 所挂载)。它不必是父级。

  5. 保持路由静态。 引擎仅装饰已存在的 HTML。不要在页面的布局树中调用 cookies()、headers() 或 auth() 来读取会话。而是在客户端获取它,这样页面可以预渲染并从 CDN 提供。在那些没有挂载 SwarmRoot 的页面上,标志脚本是无害的:没有元素携带 data-build-item,并且故障保护机制会清除该标志。

  6. 设置主题。 设置 CSS 变量(--swarm-font-mono、--swarm-accent、--swarm-text、--swarm-muted、--swarm-dim、--swarm-line、--swarm-neutral-text|fill|stroke),然后编辑 palette.ts 中的 COLORS 和 ROLE_COLOR。Canvas 无法在每帧低成本地读取 CSS 变量,因此其调色板存储在 TypeScript 中。

标记页面

所有内容都由服务端渲染的标记上的数据属性驱动。完整的参考文档(包含默认值和手机行为)在 references/dom-contract.md 中。用于 Hero 区域、卡片网格、列表、图片或视频、指标条和结局的复制粘贴配方在 references/recipes.md 中。

属性作用在含义
data-build-section="name"部分包裹器一个构建单元,拥有自己的规划、团队和审查
data-build-start="load"section启动后立即构建,而不是滚动时,具有更大的 Hero 区域预算
data-build-item + data-build="frame|type|reveal|image|dup"元素该类型的一个任务
data-build-list列表容器其第一个项目是主项(frame);dup 兄弟项目从它复制
data-build-grid容器或 section布局代理首先绘制网格并框出每个单元格
data-build-font / -weight / -scaletype 项目等宽草稿后字体切换;字重扫描;从小的开始,然后调整大小和移动
data-build-color="text|fill|stroke"任何项目颜色选择器在项目出现后选取真实颜色
data-build-tuneframe打开圆角和内边距检查器
data-build-from="left|right|top|bottom"image图片拖入的起始边缘
data-build-keep / ="mobile"任何项目从不隐藏(仅在手机上使用 mobile),仍会被注释
data-build-label、-order、-role任何项目标签文本、排序提示、由哪个角色处理
data-swarm-spawn="role"Hero 区域中的 SVG <g>团队在启动时从中出现的网格节点(≥1024 px)
data-swarm-log容器服务端渲染的回退行会被实时日志替换
data-build-finale + data-deploy-target + data-deploy-text最后一个部分及其 CTA部署者在最后一次审查后"发布"页面
?build=off / ?build=forceURL为此视图禁用 / 即使此标签页已观看过也重新播放

引擎设置:html[data-build="on"](活动时)、[data-built](完成的项目和部分)、[data-lit](启动时的网格节点)、[data-deployed](结局目标)。

保持其正常工作的规则:

  • 部分大小。 每个部分大约包含一个视口的项目。拆分长页面:时间轴中的每个工作作为一个部分,大网格中的每张卡片作为一个部分,部分标题作为一个独立部分。小部分在其预算内完成,看起来是有意为之;大部分会回退到追赶速度。
  • 嵌套。 嵌套在另一个项目内的项目会等待其父项。将卡片标记为 frame,将其标题、文本和标签放在里面。
  • 类型。 仅对短文本(约120个字符以内)使用 type。较长文本会自动回退到 reveal。输入会读取一次字形框,因此由 React 管理的、在 hydration 后变化的文本(计数器、查看次数)应为 frame,而不是 type。
  • LCP。 使用 data-build-keep="mobile" 在手机上保持主标题可见。在手机上,Hero 标题是最大内容绘制(LCP),隐藏它会损失核心网页指标。桌面版仍可获得完整的字体切换构建。
  • 实时日志。 data-swarm-log 容器必须使用真实的回退行和固定高度(.swarm-log)进行服务端渲染。引擎会将其替换为实时行,并在 Skip 或卸载时恢复。

最小 Hero 示例:

<section data-build-section="hero" data-build-start="load">
  <p data-build-item data-build="type">your eyebrow line</p>
  <h1
    data-build-item data-build="type" data-build-font data-build-weight
    data-build-keep="mobile" data-build-order="-1" data-build-role="coder"
  >
    Your Name <span className="serif">(Accent)</span>
  </h1>
  <div data-build-item data-build="image" data-build-from="right" className="avatar">
    <img src="/me.jpg" alt="Portrait" />
  </div>
  {/* next/image 也可以使用(它会渲染一个引擎可读取的内部 <img>)。 */}
  <div data-build-list className="buttons">
    <a data-build-item data-build="frame" href="#">GitHub</a>
    <a data-build-item data-build="dup" href="#">LinkedIn</a>
    <a data-build-item data-build="dup" data-build-color="fill" href="#">Resume</a>
  </div>
</section>

平滑度规则

这些规则源于观察实际构建的反馈。每一条都修复了一个可见的问题。

  • 绝不在截止时间强制完成。 早期版本会将每个正在运行的时间轴跳跃到 5 倍速度,并在每个部分的截止时间弹出未认领的项目。用户在每一部分的末尾都看到了一个卡顿。预算现在只是规划目标。
    • estimate() 会缩放任务持续时间以适应计划。
    • 当某个部分延迟时,pace() 会平滑地提升追赶因子,在 0.7 秒内最多达到 1.7 倍。
    • 即使在完全追赶的情况下,每个补间动画至少保持 120 毫秒长,并且打字步骤至少保持 16 毫秒。没有跳跃效果。
    • 如果需要更快,请使光标更快(palette.ts 中的 speed)。不要缩短动画。
  • 只动画化 opacity、clip-path、filter 和 transform。 没有布局写入。字体切换的草稿是脱离流的布局层中的克隆,而缩放任务会在最后移除其内联 transform。
  • 一个时钟。 模拟和 Canvas 绘制在单个 gsap.ticker 回调中运行。React 状态仅在构建开始或停止时更改,而不是每帧更改。当没有代理在屏幕上时,ticker 会停止。
  • 提前测量。 在视口下方大约两个视口范围内的部分会在 requestIdleCallback 切片中测量。启动某个部分永远不会在滚动帧上导致大的布局读取突发。
  • 尊重阅读者。
    • 聚焦未构建部分内的任何内容会立即完成该部分,因此键盘用户永远不需要等待。
    • 构建过程中的大小调整会完成正在运行的部分,因为它们的测量矩形已经过时。
    • 隐藏的标签页会暂停所有内容。
    • HUD 始终提供 Skip、Replay 和 Inspect。
  • 手机获得更少。 每个配置文件都有自己的预算、团队和细节(palette.ts 中的 detectProfile())。
    • 手机:8 个代理,每个部分约 2.2 秒,每页一次字体切换,每部分一次颜色选取,每个列表一次 dup 扫描,无检查器面板,无缩放任务,无部署者。
    • 平板:11 个代理。
    • 桌面:15 个代理,每个部分约 3.9 秒,Hero 区域 5.25 秒。

用于"更快"、"更平静"和"更多代理"的调节旋钮:references/tuning.md。

减少运动

引擎为每位访客播放,包括那些在操作系统中要求减少运动的用户。站点所有者认为构建动画是页面的重点,并且 HUD 始终提供 Skip,因此这是一个有意为之的选择,并非盲目复制的默认行为。如果要尊重该设置,请进行以下全部三项更改:

// build-flag-script.tsx:在 CODE 内部,在 d.setAttribute('data-build','on') 之前
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;

// motion.ts
export function offByUrl(): boolean {
  return OFF_RE.test(window.location.search) ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

// smooth-scroll.tsx
respectReducedMotion: true,

两种方式都进行测试:某些浏览器(以及一些自动化设置)会报告 reduce,即使没有人选择它,这也是这里默认设置为"开启"的原因之一。

与其他效果配合使用

  • 以 [data-built] 为门控。 引擎在构建期间读取元素矩形并切换 opacity 和 clip-path。倾斜、闪烁、加扰、内容上的视差,或任何其他转换项目的效果,如果提前运行,都会破坏这些测量。仅当项目及其部分携带 [data-built],或 html[data-build] 不为 "on" 时,才启动此类效果。一个现成的辅助函数:

    export function isBuilt(el: Element): boolean {
      if (document.documentElement.getAttribute("data-build") !== "on") return true;
      const item = el.closest("[data-build-item]");
      if (item && !item.hasAttribute("data-built")) return false;
      const section = el.closest("[data-build-section]");
      return !section || section.hasAttribute("data-built");
    }
    
  • 视频。 包裹 <video poster> 的 image 项目,使用其海报帧作为缩略图拖入。为包裹器提供一个 data-build-label,例如 clip.mp4,因为没有 <img> 来读取文件名。

  • 装饰性视差 在仅移动装饰元素的地方可以正常使用。永远不要将其放在引擎测量的任何东西上。

使其成为你自己的

这里的引擎是一个原创实现,同样的标准适用于你从中构建的任何东西。如果一个站点启发了你,请写下是什么让该站点与众不同,并远离那些确切的元素。借鉴是可以的,但不要复制。

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能