关于 infinite-webgl-grid
infinite-webgl-grid 是用于网页作品集和图片墙的 Three.js 技能,可通过拖动或滚轮浏览向各方向延伸的图片网格。它使用坐标环绕复用固定数量的网格和纹理,而非持续创建或销毁网格。
主要功能
- 01双向拖动和滚轮浏览
- 02坐标环绕实现无限网格
- 03固定网格复用纹理
- 04可配置网格与图块参数
- 05支持离线占位图演示
适用场景
- 01为作品集制作可拖动图片墙
- 02构建可向各方向浏览的海报墙
- 03制作无尽延伸的图片画廊
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rheinmir/ setup --skill infinite-webgl-grid安装后在 Claude Code 中输入/infinite-webgl-grid确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/demo.cssdemo.htmldemo.js详细介绍
技能:infinite-webgl-grid
纯 Three.js 组件——固定的 N×N 图片平铺网格,通过拖动(pointer)或滚动(wheel)进行浏览,仿佛网格向各个方向无限延伸。使用坐标环绕(modulo offset)技术,不会持续生成/销毁 mesh。
内容
用途与背景
- **用途:**使用坐标环绕构建可向各个方向无限浏览(拖动/滚动)的 WebGL N×N 图片网格,优先原样复制
Rheinmir/uiux-asset中的原始版本。 - 触发条件(适用场景):
- 作品集/画廊/海报墙需要营造向各个方向“无限浏览”的感觉(而不是常规的单向滚动)。
- 用户提到“无限图片网格”、“infinite scroll grid webgl”、“可拖动图片墙”、“无限滚动海报墙”。
- 不适用场景:
- 需要粒子随滚动进度汇聚成一张特定图片时不要使用(这应使用
threejs-particle-morph——它有“已到达目标”的状态,而这里没有目标,只能持续浏览)。 - 不要用于同一画框中两张图片之间的切换效果(这应使用
blur)。
- 需要粒子随滚动进度汇聚成一张特定图片时不要使用(这应使用
心智模型
虚拟 offset(拖动/滚动/inertia)→ 每个 mesh 固定不动:wrap(localX - offset.x) → 显示位置 → 虚拟 tile 索引变化 → 从固定纹理池中更换 material.map。相机保持静止,运行时不添加/移除 mesh(详情:参考 — 机制)。
输入与输出约定
| 字段 | 必填? | 含义 | |
|---|---|---|---|
| 输入 | 图片/内容 + 颜色 | 否 | 缺少时 → demo 会自动绘制 placeholder canvas,可离线运行 |
| 输入 | 项目约束:是否允许引入外部 lib/file | 是 | 允许 → 原始版本;不允许 → assets/ vanilla |
| 输入 | gridSize/tileSize/gap/poolSize | 否 | InfiniteGrid 的初始化参数 |
| 输出 | .html 文件(+ _assets/)或已适配到项目中的 demo.html/.css/.js | 是 | 如果是原始版本,首行保留署名注释 |
| 输出 | Playwright 验证证据 | 是 | 向 4 个方向拖动前/中/后的截图 + 无错误的 console |
规则与能力
- RULE-01(MUST):限制同时存在的 tile/texture 数量:移动设备 GPU 对同时工作的 texture unit 数量有限制。实际显示的网格(不包括 wrap 缓冲区)应保持在约 ≤ 5×5 个 tile 同时显示;demo 默认
gridSize=7(显示 5 个 + 每侧 1 个缓冲 tile,以便 tile 平滑跳转,不出现 pop-in)。如果需要的不同图片数量超过可见 tile 数量 → 使用 texture atlas(将多张图片合并到 1 个 texture,通过更改 UV offset 替代为每张图片单独更改material.map),不要无限增加poolSize——如果代码保留引用,每个独立 texture 即使不同时显示,也会占用 1 个 texture unit。 - RULE-02(MUST):必须使用 Passive: false:
wheel和pointermove监听器必须使用{ passive: false }注册,并在适当的位置调用preventDefault()(仅在正在拖动或位于 canvas 内时)——缺少此标记时,现代浏览器默认采用 passive,preventDefault()会被静默忽略(没有错误,也没有警告),页面背景仍会与网格同时滚动/缩放,导致移动设备上出现卡顿/冲突。容器上的 CSStouch-action: none也必须设置,但不能替代preventDefault。 - RULE-03(SHOULD):demo 使用 Ortho camera(网格平面,无透视变形)——如果需要深度效果(远处 tile 逐渐模糊/缩小),可改用
PerspectiveCamera;此时 wrap 公式保持不变,只需更改 world-unit 到 pixel 的映射方式。 - RULE-04(MUST):使用
playwright-verify进行验证——大力拖动时截取前/中/后的画面(测试全部 4 个方向),并读取 console(WebGL 上下文错误通常不会在 canvas 上显示,也不会明确抛出 JS error)。 - 能力:从外部资源仓库(或技能的
assets/)读取/复制文件;将文件写入项目;通过本地 HTTP 提供服务;控制无头浏览器进行截图并读取 console。
失败边界
- 项目不允许引入外部 lib → partial 是有效结果:使用精简版
assets/,而不是原始版本。 - 通过
file://打开 → texture/asset 出错 → 在通过 HTTP 运行之前视为 blocked。 - console 出现 WebGL 错误,或页面背景与网格同时滚动 → 验证 failed;修复(passive:false +
touch-action: none)后重新验证。 - 实际需求是粒子汇聚/两张图片切换 → clarify,转用
threejs-particle-morph/blur。
HOW
主要工作流
| 步骤 | 类型 | 输入 | 操作 | 输出/退出 | 失败/下一步 |
|---|---|---|---|---|---|
| W01 | judgment | 需求 | 确认匹配该技能(无限滚动网格,没有终点),并选择原版或 assets/ | 已选择的来源 | 不允许引入外部库 → B01 |
| W02 | effect | 原版 pen | 打开运行链接查看效果 → 将完整的 .html 文件(以及 _assets/)复制到项目中,保留署名注释 | 项目中的文件 | — |
| W03 | judgment | 已复制的文件 | 只修改内容/图片/颜色 | 已适配的文件 | — |
| W04 | effect | 文件 | 通过 HTTP 在本地运行,不要使用 file:// | 正在运行的页面 | 资源出错 → 检查路径 |
| W05 | deterministic | 页面 | 使用 playwright-verify 验证:向 4 个方向大力拖动,并在开始/中途/结束时截图 + 检查 console | 截图 + 无错误的 console | 出错 → 修复后重复 W04 |
各步骤详情(W01–W05 的权威来源):
原版 — 优先使用(复制完整文件,不要重写)
作者的原始代码(公开的 CodePen = MIT)位于外部仓库 Rheinmir/uiux-asset — 可运行的图库:https://rheinmir.github.io/uiux-asset/scroll-effects/ 。
流程:打开运行链接查看准确效果 → 将完整的 .html 文件(如果文件引用了 _assets/ 文件夹,也一并复制)复制到项目中 → 保留首行署名注释 → 只修改内容/图片/颜色。通过 HTTP 在本地运行,不要使用 file://。
如果需要完整的离线版本,运行 git clone --depth 1 https://github.com/Rheinmir/uiux-asset。
| 原版 Pen — 作者 | 链接 |
|---|---|
| Infinite Scrollable and Draggable WebGL Grid — ReGGae | 运行 · 代码 |
| Infinite 3D Poster Scroll Wall — photodow | 运行 · 代码 |
| Infinite Scrolling with Image Cards — jkantner | 运行 · 代码 |
此技能中的 assets/ 只是自行编写的精简 vanilla 版本 — 仅在项目不允许引入外部库时使用;默认使用上面的原版。
分支
| ID | 类型 | 条件 | 行为 | 跳过/失败 | 汇合 |
|---|---|---|---|---|---|
| B01 | conditional_required | 项目不允许引入外部库 | 使用 assets/demo.html + demo.css + demo.js,适配参数,并将占位符替换为真实图片(见 Assets 部分) | — | W04 |
| B02 | user_optional | 需要深度效果 | 将 Ortho 改为 PerspectiveCamera,保留 wrap 公式,只更改 world-unit → pixel 映射 | — | W05 |
| B03 | conditional_required | 需要的不同图片数量多于显示的 tile 数量 | 使用 texture atlas(更改 UV offset),而不是增大 poolSize | — | W05 |
验证与停止条件
完成条件:Playwright 已向 4 个方向拖动,并在开始/中途/结束时完成截图;网格没有 pop-in,页面背景不会随之滚动,console 没有错误(WebGL 错误通常不会明显显示 — 必须检查 console,不能只看截图)。
示例
- 正向示例:“为作品集制作无限滚动的 poster wall” → 从
Rheinmir/uiux-asset复制infinite-3d-poster-scroll-wall.html,替换图片,通过 HTTP 在本地运行,使用 Playwright 向 4 个方向拖动 → tile 平滑跳转,console 无错误。 - **边界/失败:**使用
assets/demo.js,但注册wheel时没有设置{ passive: false }→preventDefault()被静默忽略,页面背景和网格一起滚动 → 验证失败;添加标志和touch-action: none后重新运行。
参考 — 资源
assets/demo.html + assets/demo.css + assets/demo.js — 阅读后进行适配(初始化 InfiniteGrid 时更改 gridSize/tileSize/gap/poolSize,将 _drawPlaceholder() 替换为真实图片,通过 THREE.TextureLoader 加载 URL,而不是使用 canvas data URL)。Demo 会自行绘制占位符(canvas 数字 + hash 颜色),因此无需图片/网络即可立即运行。
参考 — 机制
- 网格固定,不会新增:初始化时一次性构建
gridSize × gridSize个网格(PlaneGeometry共用 1 个实例,每个网格使用单独的材质),运行时绝不添加或移除网格——这与天真地“拖到尚无网格的区域时创建新网格”的做法截然不同。 - 虚拟偏移,不会让摄像机无限远移:拖动或滚动只会累加到
offset.x/offset.y(浮点数,理论上可以无限增长,实际上永远不会达到极限)。摄像机保持静止。 - 使用 modulo 环绕:每个网格的局部坐标
localX/localY固定不变(在原始 N×N 网格中的位置)。每帧的实际显示位置 =wrap(localX - offset.x),其中wrap(raw) = ((raw + half) % full + full) % full - half(full = gridSize*pitch,half = full/2)——将无限坐标折叠到中心周围的显示范围内,这是此技术使用的标准取模公式。 - 瓦片跳跃时复用纹理:仅当网格的“虚拟瓦片索引”发生变化时,才会更改其
material.map(根据wrapped_position + offset计算,并按pitch四舍五入)——也就是瓦片从视口一侧离开、并从另一侧重新出现时。纹理取自固定纹理池(通过THREE.TextureLoader一次性加载poolSize个纹理),跳跃时绝不创建新纹理。 - 不依赖网络的占位图:纹理池由
<canvas>绘制数字和颜色生成(使用 golden-angle hue spread,便于肉眼区分),通过canvas.toDataURL()获取数据 URL,然后仍然通过THREE.TextureLoader.load(dataUrl)加载——演示可离线运行,同时仍使用真实图片加载的 API 调用路径(替换为真实图片时只需更改 URL,无需更改代码流程)。 - 惯性:指针拖动会记录
velocity(最后一次pointermove的移动量);松开后,每帧执行offset += velocity; velocity *= damping (0.92),直到速度低于阈值——逐渐插值回 0,并非真正的物理模拟。 - 清理:
destroy()移除 pointerdown/pointermove/pointerup/pointercancel/wheel/resize 监听器,释放每个材质、释放共用几何体(仅 1 次)、释放纹理池中的每个纹理(仅 1 次——不要按网格释放,因为多个网格会共用纹理)、释放渲染器,并从 DOM 中移除 canvas。