mcpField
G
glimm

glimm

github.com/nomanjack设计与 UI012 次安装1 天前更新
代码仓库

关于 glimm

glimm 是为 Web 项目添加 WebGL 色带页面转场的技能,提供跨框架接入指导,并强调适用场景评估、无障碍行为和版本适配。

主要功能

  1. 01跨框架转场核心
  2. 02React 和 Next.js 适配器
  3. 03自动拦截站内链接
  4. 04应用内状态切换转场
  5. 05减少动态效果及 WebGL 回退

适用场景

  1. 01为作品集添加品牌转场
  2. 02在应用内操作后显示结果
  3. 03评估频繁导航是否适合转场

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nomanjack/glimm-skill --skill glimm

安装后在 Claude Code 中输入/glimm确认已加载。

附带文件references/react-and-vanilla.mdreferences/tuning.md

详细介绍

glimm — 色彩扫掠页面过渡

glimm 会在导航期间让一束 WebGL 色彩扫过视口:离开的页面始终位于色带前方,新页面则在色带后方显现(遮罩式页面过渡)。核心部分与框架无关,并提供 React 和 Next.js 适配器。零运行时依赖,MIT。

此技能适用于任何 glimm 版本。编写代码前,先检查项目解析到的版本(npm ls glimm,或已安装的 node_modules/glimm/package.json),并以已安装的包为准:确认你使用的每个导出项都存在于其类型定义中(node_modules/glimm/dist/*.d.ts)。如果下面提到的 API 在其中缺失或名称不同,请遵循已安装版本及其更新日志(https://glimm.dev/changelog),不要照搬这里的方案。全新安装时,优先使用 npm install glimm@latest。

页面朝交互位置靠拢、页面随色带弯曲,以及跟随正在进行的请求的时序,都由 GlimmScope 提供,该功能在 glimm 0.4 中加入。如果已安装的类型定义中没有它,请告诉用户,运行 npm install glimm@latest 即可获得。不要用自定义着色器重新实现这些效果。

范围:只实现 glimm,不做其他任何事

只添加并接入 glimm。项目的其他一切都由项目所有者负责,包括文案、错误消息、加载设计、布局、样式和应用逻辑。除非用户明确要求,否则不要编写或重写其中任何内容,也不要围绕扫掠效果添加功能。如果附近有什么看起来不对,用一句话指出,然后保持原样。

第 1 步 — 适用性检查(修改代码前)

glimm 是一种鲜明的表达,不是默认选择。每次导航时,扫掠效果都会占用用户大约一秒的注意力,因此适合每次导航都代表有意义场景转换的网站;对于导航频率很高的工具,它则会很快令人厌烦。先查看项目(框架、路由器、网站类型),并给出诚实的判断:

适合: 营销网站、作品集、个人网站、博客和长篇阅读、产品发布页面、按章节阅读的文档、画廊——任何访客每次会话只导航几次,且品牌个性很重要的场景。

不适合: 管理仪表盘、数据表、设置面板、多步骤表单流程中途、搜索与筛选界面,以及用户每小时会导航数十次的任何场景。每次点击侧边栏都进行 1.1 秒的扫掠,到第三次点击就会变成阻碍。

如果项目不适合,请直言并建议不要安装——正是这份诚实,才能让“适合”的判断值得信赖。如果用户无论如何都想使用,请尊重其选择,但限定使用范围:缩短时长(sweepMs: 650、outroMs: 150),并且只在主要区块切换时执行扫掠;对于高频链接,则通过 data-glimm-skip 排除(第 4 步)。

这个判断针对的是每次导航都执行扫掠的情况。即使某个产品不适合对每个链接都使用扫掠,只要它发生得足够少,且改变的是用户在意的内容,仍然可以在某个特定时刻使用一次。这种情况下,跳过 InterceptLinks,改用第 2 步中的应用内方案。

无障碍和可靠性都不是需要犹豫的理由:glimm 会尊重 prefers-reduced-motion(立即导航),并在 WebGL 不可用时降级为普通导航。这两项都是自动处理的——不要自行编写减少动态效果的保护逻辑。

第 2 步 — 安装并接入

npm install glimm

react 和 next 是可选的对等依赖——不会引入其他依赖。

Next.js App Router(常见情况)

在根布局中挂载一次,并包裹正文内容:

// app/layout.tsx
import { GlimmProvider, InterceptLinks } from 'glimm/next'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <GlimmProvider palette="prism">
          <InterceptLinks />
          {children}
        </GlimmProvider>
      </body>
    </html>
  )
}

安装就这些。<InterceptLinks /> 会自动为每次内部链接点击执行过渡扫描——普通的 <a> 和 next/link 都无需改动即可使用,并且它已自动忽略:修饰键点击、中键点击、target="_blank"、下载链接、外部来源和同页哈希链接。不要重新实现这些保护逻辑。

重要的接入规则及其原因:

  • 使用根布局,而非嵌套布局。 用户跨布局边界导航时,嵌套布局中的提供器会重新挂载,中断着色器上下文并丢弃过渡效果。
  • 每个应用只有一个入口点。 从 glimm/next(或在非 Next 应用中,从 glimm/react)导入所有内容。混用入口点会创建两个 React 上下文,一个入口点的 useGlimm() 无法获取另一个入口点提供的上下文。
  • 根布局可以继续作为服务端组件。 GlimmProvider 和 InterceptLinks 已经是客户端组件;在布局文件中导入它们不需要添加 'use client'。
控制单次导航(可选,仅在需要的地方添加)
'use client'
import { TransitionLink, useTransitionRouter } from 'glimm/next'

// Declarative — a drop-in next/link replacement:
<TransitionLink href="/work" sweep={{ palette: 'berry', direction: 'rtl' }}>
  Work
</TransitionLink>

// Programmatic — for form submits / post-action redirects:
const router = useTransitionRouter()
await save(form)
router.push('/done', { palette: 'ember' })

TransitionLink 和 InterceptLinks 可以安全共存——链接自身的处理器会接管点击,拦截器会发现该点击已处理,因此不会重复触发。

应用内时刻

过渡扫描不需要路由变更。它可以标记当前页面上的状态变化,而这往往是在产品内部使用它的更好方式。有两种实现方法;只要所安装的版本支持,GlimmScope 就是更好的选择:

  • GlimmScope 包裹一个区域,可以是页面、版块或卡片。它会执行应用的实际工作,将变化隐藏在光带之后,并在浏览器支持的情况下将内容移到光带下方。请参阅下一节。
  • useGlimm 中的 sweep() 会覆盖视口,无需更改页面结构。没有 GlimmScope 的版本,或用户只想显示光带时,可选择这种方式。
'use client'
import { useGlimm } from 'glimm/next'  // or 'glimm/react' outside Next

const { sweep } = useGlimm()
sweep(() => setPlan('pro'), { sweepMs: 800 })

只有当某个时刻足够少见,且用户会希望注意到它时,才值得触发过渡扫描:升级套餐、完成付款、启用某种模式、获得等待已久的结果、队友加入。常规操作不值得触发,包括保存、编辑行、勾选复选框,以及会话中反复执行多次的任何操作。没有人主动触发的事件(例如收到付款)也可以用同样的方式触发扫描,只需在处理该事件的位置调用即可。sweep() 的持续时间应短于导航过渡,约为 650 到 900 ms。

使用 GlimmScope 移动页面
'use client'
import { useRef, useState } from 'react'
import { GlimmScope, type GlimmScopeHandle } from 'glimm/next' // or 'glimm/react'

const scope = useRef<GlimmScopeHandle>(null)

const upgrade = (event: React.MouseEvent) => {
  const r = event.currentTarget.getBoundingClientRect()
  setBusy(true) // answers the press, outside the scope's timing
  scope.current?.play({
    at: [r.left + r.width / 2, r.top + r.height / 2], // the page leans in towards the button
    work: async () => {
      const res = await fetch('/api/upgrade', { method: 'POST' })
      if (!res.ok) throw new Error('Upgrade failed') // a rejection means no sweep
    },
    change: () => setPlan('pro'),  // runs as soon as the work is done, behind the band
    onBurst: () => setBusy(false), // the band has broken away
    onFail: () => setBusy(false),  // no sweep, and the page stays as it was
  })
}

<GlimmScope ref={scope} curve={0.12} direction="ttb" sweepMs={2700} style={{ position: 'absolute', inset: 0 }}>
  <Billing />
</GlimmScope>

它不需要 GlimmProvider,因此只想使用某个效果的项目可以单独使用它。使用规则如下:

  • 不要检查浏览器是否支持。 在支持 HTML-in-Canvas 的地方,内容会向动作方向靠拢,并在光带经过时泛起涟漪。其他地方则会在同一个框内呈现普通扫过效果,变化仍然在光带之后发生。它也会处理减少动态效果的设置,因此绝不要编写保护条件或第二条代码路径,也不要因为浏览器版本而放弃某项功能。这就是它可以放心使用、无需询问的原因。
  • work 执行实际工作,change 接收结果。 两者都放在 play() 中,因此不要像使用 sweep() 那样先等待请求完成再调用。按下时工作即开始,页面会在运行期间向内靠拢,无论耗时多久,整个移动过程都如同一口气完成。change 会接收工作结果。失败时要让 work 拒绝,包括响应状态错误的情况,因为 fetch 遇到 500 时不会抛出异常。
  • 按下控件的忙碌状态由外部管理。 在调用 play() 前设置,在 onBurst 和 onFail 中清除,不要等待 onLeave。
  • 仅在无人按下任何控件的情况下传入 anticipate: true,例如收到付款时。它会让页面有时间充分向内靠拢后再发生变化;如果有人刚请求了某项操作,这就意味着要让其等待。不传入 work 时,change 会在按下时立即运行。
  • 后续的 play() 会中断当前播放, 因此不要将播放排队,也不要在一次播放结束前禁用控件。play() 还会返回 { fastForward, cancel }。
  • 为框指定尺寸。 作用域会裁剪到自身的框并填满该框,因此要用 style 设置尺寸,并适配现有布局。示例中的 position: 'absolute', inset: 0 适用于定位容器内的视图;如果没有定位祖先元素,它会填满视口,因此不要在正常文档流中的页面上使用。在 flex 容器中,flex: 1, minHeight: 0 会填满内容区域。区块或卡片需要设置固定高度,因为页面效果运行时,内容不会为框提供尺寸,框会因此收缩为零。可滚动内容必须在作用域内滚动,而不是在作用域外的文档中滚动。作用域外渲染的内容(例如通过 portal 渲染的 toast 或对话框)不会移动,也不会被光带覆盖。
  • 向动作方向靠拢。 对于按下的控件,要传入 at;否则页面会向 origin 属性所指的位置靠拢,默认值为顶部中央。at 的单位是 CSS px,以框的左上角为原点,因此示例之所以有效,是因为它的框从视口左上角开始。对于其他框,要减去框自身的位置。对于不会移动的动作或无人按下控件触发的事件,应将 origin 设为框内的比例值,例如,底部的编辑区可以使用 origin={[0.5, 1]}。
  • 让光带有充足时间扫过整页。 上面的 650 到 900 ms 是针对 sweep() 的。作用域的光带默认持续 1600 ms;扫过整页时,约 2700 ms 较为合适,因为光带到达另一侧前很久就会淡出。
  • 每页只用少量几个,不要每一行都用一个。 每个作用域都会使用自己的 WebGL 上下文,而一个页面最多只能同时保留 16 个。

只有用户要求提供关闭效果的方法时,才添加 usePageEffect() 读取器开关。

其他配置

对于不使用 Next 的 React(Vite + react-router 等)、Next Pages Router 或原生 JS / 其他框架,请阅读 references/react-and-vanilla.md——尤其要注意,Pages Router 必须使用 glimm/react,而不是 glimm/next(Next 适配器基于 next/navigation 构建,仅适用于 App Router)。

变化应发生在扫掠过程中

扫掠会展示变化,因此可见变化绝不能先于扫带发生。把状态更新或导航放进传给 sweep() 的回调中,并确保所有受其影响的内容都位于扫掠区域内。glimm 随后会在扫带经过时运行它。

// Right: the new state appears behind the band
sweep(() => setPublished(true))

// Wrong: the new state is on screen before the band arrives
setPublished(true)
sweep(async () => {})

如果结果来自请求,请先等待请求完成,再在请求返回时调用 sweep()。按下的按钮可以立即显示加载状态,因为这是对按下操作的反馈。所有属于结果的内容,例如图例、计数或徽章,都应随扫掠一起变化。

扫掠绝不让任何人等待

动画跟随应用运行,因此应用绝不等待动画。

  • 立即响应按下操作。 按下的控件应立即显示加载或选中状态,并且在扫掠之外。结果应放进扫掠中,其中也包括清除加载状态,这样旧页面就不会在扫带展示新页面之前短暂恢复空闲状态。
  • 绝不添加延迟。 不要使用 setTimeout,也不要等待 done 或 midpoint,来让变化与扫带同步,或让控件一直处于忙碌状态直到扫掠结束。sweep() 会自行应用变化,并会在尽可能早的时机应用,同时确保变化仍不可见。
  • 不要在回调中执行耗时操作。 回调完成前扫带无法开始,页面也会一直被阻塞。先等待请求完成,让页面保持可用,然后再调用 sweep() 并传入结果。回调中可以返回路由变更,因为它正是要展示的变化。
  • 仅在成功时扫掠。 调用 sweep() 前先检查结果,包括响应状态,因为 fetch 遇到 500 时不会抛出错误。请求失败时不应扫掠,因为扫掠会向用户传达操作成功。应用如何处理失败由项目自行决定,因此不要改动这一部分。
  • 允许新操作中断当前操作。 第二次调用 sweep() 会替换正在播放的扫掠,因此不要排队等待扫掠,也不要在扫掠结束前禁用控件。浏览器支持 View Transitions 时,扫掠播放期间页面不会响应点击,这也是应用内扫掠应保持简短且少用的另一个原因。
根据变化范围设定尺寸,并从操作处开始

扫掠的范围应与变化大致相当,并且看起来应从触发变化的位置开始。

  • 整个页面或视图都发生了变化: 使用 curve={0.12} 的作用域包裹页面,或使用覆盖整个视口的 provider sweep()。
  • 某个位置发生了值得注意的变化: 使用作用域包裹页面,并设置 curve={0.22} span={0.64} fade={[0.16, 0.64]},这样扫带会停留在变化附近,并在页面中部之前消失。
  • 单个元素发生了变化: 仅用作用域包裹该元素,并设置 curve={0.06} bandTight={34}。如果没有 GlimmScope,请使用下方的容器方案。单张卡片的变化却使用整页扫掠,会让一个小瞬间显得比实际更重要。

选择一个能从操作处向外移动的 direction。例如,屏幕底部的编辑器应使用 btt 扫掠,顶部栏中的控件则应使用 ttb。侧边栏中的操作也可以让作用域使用 ltr,或使用 rtl。

在手机上,主要操作通常位于屏幕底部,因此即使桌面布局使用其他方向,btt 扫掠通常也更合适。扫掠不能依赖悬停,因此应从操作本身触发,并通过触摸进行检查。

在容器内扫过

GlimmScope 已经实现了这一切,因此这份做法适用于不包含它的版本。 当扫光在整个页面以外的单个元素内播放时(例如卡片、 图表或表格),光带必须在靠近该容器边缘时逐渐淡出——绝不要 让它止于生硬的边界。带有 createShader 的 feather 选项的 glimm 版本默认会这样处理。0.3.1 及更早版本没有 此选项,因此请先检查已安装的类型,再用 CSS 遮罩画布边缘。这份做法仅用于装饰性扫光,因此请在 onMidpoint 中进行更改,而不是通过 provider 的遮罩功能。做法见 references/react-and-vanilla.md。

第 3 步 — 有目的地选择调色板

别不加考虑地使用默认值。找到项目的品牌色 (CSS 自定义属性、Tailwind 配置、设计令牌),并选择色相最接近它们的预设:

预设色相给人的感觉
spectrum完整的一轮色相循环同时呈现所有颜色——0.4 版起的默认值
prism青色 → 紫罗兰色 → 洋红色虹彩、科技感——0.4 版之前的默认值
berry红粉色 → 洋红色温暖、大胆、编辑风
lagoon蓝色 → 青色 → 绿色清凉、平静、自然
citrus绿色 → 黄色 → 橙色活力、俏皮
azure青色 → 蓝色 → 紫罗兰色企业风、简洁、可靠
ember黄色 → 橙色 → 红粉色炽热、紧迫、戏剧化

如需精确匹配品牌色,可使用品牌自身的十六进制颜色值构建自定义调色板:

import { accentChain } from 'glimm/next'  // also exported from glimm/react and glimm

const brand = accentChain(['#7C3AED', '#EC4899'])  // 2–4 stops, in sweep order
<GlimmProvider palette={brand}>

accentChain 会在 OKLCH 中插值各个色标(感知上均匀,不会产生浑浊的中间色),并使着色器调色板与之匹配。在深色网站上,为 provider 添加 brightness={0.85},避免光带在深色背景上过于明亮。更深入的调整——时序、缓动、方向、光带形状、效果配方——请参阅 references/tuning.md。

第 4 步 — 精选触发扫光的链接

扫光应标记场景变化,而不是每次点击。检查项目的 导航,并使用 data-glimm-skip 排除所有高频或场景内操作:

  • 分页、表格排序、筛选器
  • 页面内的标签页切换
  • 页脚中的实用链接(隐私政策、条款)
  • 会打开模态框或抽屉,而非进入新场景的操作
<Link href="/work?page=2" data-glimm-skip>Next page</Link>

主导航(页眉链接、进入详情页的卡片、醒目的 CTA) 保留扫光。如果筛选后只有两三个链接会触发扫光, 请重新考虑是否使用 InterceptLinks,改为只在这些链接上显式使用 <TransitionLink>——明确表达意图,胜过“全部拦截再逐个排除”。

步骤 5 — 验证效果

切勿交付未经验证的改动。启动开发服务器,实际点击进行两三次页面跳转,然后检查:

  1. 转场扫光正常播放,新页面出现在色带后面(没有未设置样式的内容闪现,也没有重复内容)。全速播放时往往太快,难以判断,因此可以暂时将 sweepMs 设为约 5000,再观察一次。属于结果页面的内容,在色带扫到之前都不应可见。之后将 sweepMs 改回原值。
  2. 浏览器控制台没有问题——没有 glimm 警告,也没有 WebGL 错误。
  3. 按住 Cmd/Ctrl 并点击仍会在新标签页中打开链接,原页面不会播放转场扫光。
  4. 外部链接和锚点链接的行为与之前一致。
  5. 浏览器的前进/后退行为应与当前安装版本的文档一致——在当前版本中,这些操作会立即完成,不播放转场扫光;这是预期行为,不是 bug。
  6. 按下的控件会立即响应,不会在任何操作发生前暂停;转场扫光开始后紧接着按下其他控件也仍然有效。
  7. 在手机屏幕宽度下,以及所选的转场方向下,页面仍然易于阅读。
  8. 对于 GlimmScope,请求失败时不会播放转场扫光,页面保持原样。如果正在检查的浏览器不支持 HTML-in-Canvas,那么只显示普通扫光才是正确结果,并非 bug;页面只会在支持该功能的浏览器中移动。

如果发现问题,请先查看下面的“故障排除”,再修改接线方式。

步骤 6 — 简短自然地交付

最终回复应简短、口语化——几句话即可,不要写成报告。用户刚看过涉及两个文件的改动;不要加上标题和检查清单,照搬这份指南。只需说明:

  • 改了什么(通常是 package.json 和根布局),以及无法运行安装时要执行的安装命令;
  • 他们实际会调整的一两个参数(palette、sweepMs——GlimmProvider 的属性);
  • 一句话说明如何移除(删除 provider 和 InterceptLinks,执行 npm uninstall glimm)——可撤销意味着可以放心尝试。

需要特别说明的事项用一句话带过,不要单独成节——“我无法在离线环境中运行,所以请在执行 npm install 后点击浏览几个页面。”就足够了。如果网站正在运行,询问是否想看看 lagoon 预设,比描述调色板更好。

故障排除

症状原因 → 解决方法
useGlimm must be used inside <GlimmProvider>组件导入的入口点与 provider 不同,或者 provider 位于嵌套布局中。使用同一个入口点,并将其放在根布局中。
没有扫掠效果,立即导航,也没有错误操作系统启用了减少动态效果,或者没有 WebGL(无头浏览器、某些虚拟机)。这是预期的回退行为,并非故障。
新内容出现后,效果带才在其上方播放状态在扫掠回调之外发生了变化。将更新移入 sweep(() => ...)。
页面冻结,然后扫掠效果延迟播放耗时操作位于扫掠回调中。先等待请求完成,再将结果传给 sweep()。
用户希望页面像 glimm.dev 演示一样弯曲或向内收拢使用 GlimmScope。如果已安装的类型中没有它,请使用 npm install glimm@latest 升级,不要编写自定义着色器。
GlimmScope 播放了扫掠效果,但页面没有移动浏览器不支持 HTML-In-Canvas(Chrome 157 及更高版本);这是预期的回退行为。也可能是读者通过 usePageEffect 将其关闭了。
GlimmScope 内什么都不显示,或者效果带远远超出屏幕容器尺寸不合适。通过 style 设置高度,并让内容在容器内滚动。
扫掠效果播放完毕,但页面在结束时发生“跳动”使用的是较旧的 glimm(<0.3.1),没有 View-Transition 遮罩。请使用 npm install glimm@latest。
跳转到其他源的链接触发了异常的扫掠效果/出现故障这些链接根本不应该被拦截——检查自定义的 shouldIntercept 是否漏掉了源检查。
卡片或图表中的扫掠效果以明显的硬边结束canvas 的 feather 为 0,或者 glimm 版本早于 feather 的引入版本。将 feather 保持为 'auto',或者在 CSS 中遮罩 canvas 边缘(参见 references/react-and-vanilla.md)。
深色主题下效果带太亮/褪色在 provider 上设置 brightness={0.85}(或更低)。
TypeScript 找不到 glimm/nextmoduleResolution 版本较旧。tsconfig 中需要设置 "bundler" 或 "node16"/"nodenext"——glimm 仅通过 exports 映射发布 ESM。
React 18 StrictMode 中仅在开发环境下出现重复导航(非 Next)确保 navigate 回调是幂等的;provider 每次扫掠只会等待它执行一次,但应用级监听器如果被添加了两次,就会触发两次。

评论

支持 Markdown