载尘望星 · 交互生成艺术

丝缕四面体

Silk-Tetrahedron

拖动旋转,看它的四个面 · 点一面进去 · 或掷右下角的骰子

退回博客

如切如磋,如琢如磨。——《诗经·卫风·淇奥》

丝是一根线,织起来才成纹。四面体是最省的立体:四个面,不多不少,每个面都能独自撑起一套秩序。这一次我把线交给四个面去织。

于是有了这一页:首屏是一颗纯由光粒聚成的正四面体,四个面各刻着一个字;拖动它转,点其中一面,镜头就推近进那一面;右下角那枚骰子也是粒子聚的,掷下去,四面体会飞快旋起来、慢下来、停住,朝你的那一面自己走进场景。

四个面里,三面是画布,一面是壁纸。技术上说,这是一次把 Canvas2D 的对称绘画引擎和 Three.js 的粒子系统拼在一个页面里的尝试。

二、四个面#

四面各刻一字,也是各自的路数。

绢 · 原版 Silk#

白底,纤而软的丝。笔不是一条折线,而是一条会自己蠕动的短链——指针只负责往链尾塞新粒子,链本身被一层噪声方向场一直推着走。于是三件事是同一条力学的必然结果:按住不动它仍然在画(力没有停,绳一直在动),拖得快会甩出优雅的长弧,抬笔之后还会自己收一会儿尾。一笔下去同时长出多份——镜像、环绕或向心螺旋,份数可调。

用的是加色混合(lighter)配透明画布(底色来自页面):在不透明白底上画加色,颜色会被一路加到白,等于什么都画不出来。每一遍只有 0.09 的透明度,画面上那层通透感和细密的丝理,是同一片地方被叠了几百遍自己堆出来的——不是叠了一层滤镜。全站没有一处 shadowBlur:几万条线段上开阴影会直接掉帧。

格 · Pixel-Silk#

同一套对称、同一套快捷键、同一个调色盘,只换渲染层——线不再是线,而是硬边的方块。于是同一笔在绢面上是丝,在格面上是织物的经纬。切面不改画,只是换一层皮。

霓 · Cyber-Silk#

深底,霓虹,虚线描边加横向扫描纹理。多一个故障偏移滑块:拉高它,每一段线会在渲染时随机错开一点,像信号不稳的画面。可选铺一层暗网格做底。

归 · 归境#

这一面不能画,只能扰动。整张壁纸被逐像素拆成四万余颗光粒:悬停,粒子从光标处荡开一圈涟漪;点击,一圈冲击波推出去;拖拽,路过的粒子被划亮,拖出一条光的尾迹;长按,粒子开始往外飘、画面一寸寸碎开。

然后松手。所有光粒沿各自的弹性弹回原位,轮廓收得急,背景收得慢——像水面归平。

你可以扰动这些光粒,但它们终将回到自己的位置。

三、怎么玩#

操作枢纽画布(绢 / 格 / 霓)归境
拖动旋转四面体作画划出光尾迹
点击进入所指的面—放出冲击波
悬停——荡开涟漪
长按——画面碎开
空格—从头再织(清空)—
Z—退回一笔—
Esc—回到四面体回到四面体

骰子掷出的结果不是随便跳一个页,而是让四面体自己转过去:先加速自转,再阻尼减速,最后落到目标面上给你看一秒,才推近进场景。地址栏会同步成 #d 这样的深链,可以直接把某一面发给别人,浏览器后退键也能在场景之间回退。

音效全部现场合成(WebAudio 的正弦与带通噪声),没有加载任何音频文件:运笔有声,快笔音高上行;掷骰子停稳时四个面各有一个音;枢纽有一个很轻的低频环境音。

四、几个技术上的决定#

拾取打在不可见的实体四面体上,不打在粒子点云上。 Points 的射线检测要靠 threshold 猜一个命中半径,边缘会飘、还会误判成相邻的面。做法是另备一个实体四面体专门挨打,intersect.faceIndex 直接给出面号——它压根不加进场景,只是每帧把旋转组的姿态同步过去再 updateMatrixWorld。

加色必须预乘。 gl_FragColor 输出 vec4(rgb × a, a),配 blendFunc(ONE, ONE) 与 premultipliedAlpha: true。用常见的 (SRC_ALPHA, ONE) 亮度会掉一个数量级——画面上就是”怎么调都不亮”。

不开深度测试,靠面法线压暗背面。 加色叠加本来就不需要深度,但四面体是立体的,不处理的话背面的字会透过正面糊成一片。按 dot(面法线, 视线) 把背面压到 13%,体积感就回来了。

四个场景共用一个引擎。 噪声方向场、那条会蠕动的链(createPen / stepPen)、对称变换、历史栈、音效都在 silk-symmetry.ts 里,A/B/C 只是把 kind 传进同一个 paintStroke。作画中的增量绘制和撤销后的全量重绘画的是同一批曲线,否则撤销一次画面就会走样。

不做真路由,做 hash 深链。 真跳转会销毁并重建 WebGL 上下文,枢纽那颗四面体的粒子状态就丢了,“推近再跳转”的连续感也就没了。用 pushState 写 hash,场景停在同一个页面里换,白送浏览器后退键。

归境的模拟放在 CPU。 要”惯性 + 弹性归位”就必须同时持有位置与速度两个状态,无状态的顶点着色器做不到(能凑一个位移函数,但那就不叫弹簧了)。四万颗粒子的定长数组循环,每帧 1–3ms,比写一套 ping-pong 离屏渲染便宜太多。粒子位置数组就是渲染用的那个 position 缓冲,模拟直接往里写,省掉一次逐帧拷贝。

五、踩过的坑#

  • 白底画布上不能用 lighter。 加色在不透明白底上会把颜色一路加到纯白,什么都画不出来。所以画布本身是透明的,白色来自 CSS 背景。
  • opacity: 0 不挡命中测试。 引导文字淡出后仍然盖在画布上,第一次作画会被它吃掉。
  • 全站装饰会糊上来。 fullBleed 页面绕开了主布局,站点的樱花、壁纸、顶部渐变、欢迎条会一起压在画面上;压掉它们只能用全局样式,而全局样式必须用 :has() 钉住作用域,否则会去打别的页面。
  • Space 不能全局抢。 首屏下面就是正文,抢了空格翻页就没了——快捷键只在对应画布激活时生效。
  • 底部渐隐会和底色打架。 场景层渐隐到透明之后露出来的是舞台自己的底色,浅色场景在白页上就会看到底下一道黑边。舞台底色得用同一条曲线一起渐隐。

六、素材说明#

首屏与四个场景不加载任何图片:四面体、丝纹、烟花、涟漪、声波全是当场算出来的。唯一一张图是归境那面的壁纸——它被逐像素采样成粒子,所以必须在站内(跨域图会被画布污染、直接抛错)、也只需要 960 宽。

换图只改 public/assets/images/silk/wallpaper.webp 一个文件:亮度地板与轮廓阈值都是按分位数现算的,换一张整体偏暗或偏亮的图也不用回来改数字。壁纸里的角色美术版权属原作者,这里仅作个人非商用的展示与采样。


四面皆丝,掷则归位。

WELCOME欢迎来到载尘望星

很高兴在这里遇见你。