specimen

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Specimen: make an icon

Specimen:制作图标

You are making one Specimen icon: a single glossy object on a soft rounded tile, lit like a piece in a display case. The colour arrives only as light. It loops without a jump. From one piece file it ships in a dark and a light theme, as path traced video or as a live raster element.
The pieces in
examples/
are the bar. Read the one closest to the request before you write anything. If what you make would look out of place next to them, it is not done.
You write one thing: the piece file,
<name>.specimen.js
. The tile, the studio, the lights, the post, the themes, the path tracer, the denoiser and the encode are fixed and live in
runtime/
. Never edit
runtime/
, never paste a changed copy of it, never re-implement what it already does. If you think the runtime is missing something, tell the person instead of working around it.
Every file named below is in this skill's folder. The piece is written in the person's working directory (or where they say); media goes where they say (
public/icons/
, …) or beside the piece, prefixed with the icon's name. Never create a folder of your own in their project. Run the scripts from there, by their path in this folder.
Specimen is only this skill. There is no npm package and no install step. Never run
npm install
, never add a dependency to the person's project, never import
three
in the piece: it gets everything through
ctx
.
你正在制作一个 Specimen 图标:一个置于柔和圆角 tile 上的单一光泽对象,打光方式如同陈列柜中的展品。颜色只以光的形式呈现。动画循环时不会发生跳变。只需一个 piece 文件,即可交付包含深色和浅色主题的版本,形式可以是 path tracing 视频,也可以是 live raster 元素。
examples/
中的各个 piece 就是质量基准。在开始编写任何内容之前,先阅读与需求最接近的那个。如果你的作品放在它们旁边显得格格不入,那就还没有完成。
你只需要编写一个文件:piece 文件
<name>.specimen.js
。tile、studio、灯光、后期处理、主题、path tracer、降噪器和编码流程都是固定的,位于
runtime/
中。绝不编辑
runtime/
,绝不粘贴修改后的副本,也绝不重新实现其中已有的功能。如果你认为 runtime 缺少某项能力,应告知对方,而不是自行绕过。
下文提到的每个文件都位于此 skill 的文件夹中。piece 写在对方的工作目录中(或对方指定的位置);媒体文件写入对方指定的位置(如
public/icons/
),或者放在 piece 旁边,并以图标名称作为前缀。不要在对方项目中创建自己的文件夹。应从该目录运行脚本,并通过此文件夹中的路径调用脚本。
Specimen 仅指这个 skill。不存在 npm package,也没有安装步骤。绝不运行
npm install
,绝不向对方项目添加依赖,也绝不在 piece 中导入
three
;所有能力都通过
ctx
提供。

The skill folder

skill 文件夹

specimen/
  SKILL.md          this file
  concepts.md       objects that read well, metaphors by product area, what failed
  package.json      { "type": "module" }, so Node reads this folder's scripts the same under any project
  render.mjs        piece → webm + mp4 + poster per theme (video), or runtime + piece + posters (--live)
  sheet.mjs         contact sheet: raster frames, a dark row and a light row per piece; --pt, --guide
  validate.mjs      the measurable rules (section 5)
  piece.mjs         reads a piece in Node: its name, its syntax errors with the line, the contract
  session.mjs       opens a piece on the runtime page in headless Chrome
  cdp.mjs           Chrome finder, a minimal DevTools Protocol client and a static server
  runtime/          fixed — never edited
    specimen.js     the bundle: three (WebGPU), the path tracer, the denoiser, the stage, <specimen-icon>
    weights.tza     the denoiser's weights
    THIRD_PARTY_NOTICES.md  the bundled packages' and the weights' licences; --live ships it as specimen.js.LICENSE.txt
    page.html       the page render, sheet and validate load a piece into
  examples/         the reference pieces (section 7)
The scripts run with plain
node
(22 or newer) and use only Node built-ins. They drive the Chrome already on the machine (or
SPECIMEN_CHROME
), and video needs
ffmpeg
. If one is missing, the script names it: tell the person exactly what is missing and how to get it (
brew install ffmpeg
); don't install anything on your own.

specimen/
  SKILL.md          this file
  concepts.md       objects that read well, metaphors by product area, what failed
  package.json      { "type": "module" }, so Node reads this folder's scripts the same under any project
  render.mjs        piece → webm + mp4 + poster per theme (video), or runtime + piece + posters (--live)
  sheet.mjs         contact sheet: raster frames, a dark row and a light row per piece; --pt, --guide
  validate.mjs      the measurable rules (section 5)
  piece.mjs         reads a piece in Node: its name, its syntax errors with the line, the contract
  session.mjs       opens a piece on the runtime page in headless Chrome
  cdp.mjs           Chrome finder, a minimal DevTools Protocol client and a static server
  runtime/          fixed — never edited
    specimen.js     the bundle: three (WebGPU), the path tracer, the denoiser, the stage, <specimen-icon>
    weights.tza     the denoiser's weights
    THIRD_PARTY_NOTICES.md  the bundled packages' and the weights' licences; --live ships it as specimen.js.LICENSE.txt
    page.html       the page render, sheet and validate load a piece into
  examples/         the reference pieces (section 7)
这些脚本使用纯
node
(22 或更高版本)运行,并且只使用 Node 内置模块。它们会调用机器上已有的 Chrome(或
SPECIMEN_CHROME
),视频渲染还需要
ffmpeg
。如果缺少其中任何一项,脚本会明确指出:请准确告知对方缺少什么以及如何获取(例如
brew install ffmpeg
),不要自行安装任何东西。

0. Delivery

0. 交付

Ask this first, in one message, before any concept:
Video (default) — path traced,
<name>-dark.webm/.mp4
and
<name>-light.webm/.mp4
with a poster each, and a
<video>
snippet. The best look, a few hundred KB per theme, plays everywhere. Live —
specimen.js
(with its licence file) and the piece copied into their static folder, a poster per theme, and a
<specimen-icon>
snippet. It is raster, not path traced: softer reflections, no denoiser. Say so plainly. It needs WebGPU and falls back to the poster without it or with reduced motion. Both — video by default, live on top only where they turn it on.
Both themes are rendered unless the person asks for one. Also ask where the files go; if they don't say, put them beside the piece. If there is nobody to ask, take video, both themes, beside the piece, and say so.
在提出任何概念之前,先在一条消息中询问:
Video(默认)——采用 path tracing,输出
<name>-dark.webm/.mp4
和
<name>-light.webm/.mp4
,每个主题各附一张 poster,并提供
<video>
代码片段。效果最佳,每个主题通常只有数百 KB,可在各种环境中播放。 Live——提供
specimen.js
(及其 licence 文件),并将 piece 复制到对方的静态资源文件夹;每个主题各有一张 poster,并提供
<specimen-icon>
代码片段。它采用 raster,而非 path tracing:反射更柔和,且没有 denoiser。请如实说明这一点。它需要 WebGPU;如果没有 WebGPU 或用户启用了 reduced motion,则回退为 poster。 Both——默认使用 video,仅在对方启用 live 的位置额外叠加 live 元素。
除非对方要求只输出一个主题,否则深色和浅色主题都要渲染。还要询问文件应放在哪里;如果对方没有指定,就放在 piece 旁边。如果无法询问,则选择 video、两个主题,并放在 piece 旁边,同时明确说明。

Size

尺寸

Render at the size the icon is shown at: a video stretched past its pixels goes soft at the edges, and no encode setting gets them back. Find the width first in the project, in the CSS or component the icon goes into, at its widest breakpoint. Ask only when you can't tell, in the same message as the delivery:
shown at (CSS px)
--size
videoper theme
up to 160384320 px~4 min
up to 320768640 px~17 min
up to 4801152960 px~36 min
The video has to be at least twice the CSS width for retina screens, and it comes out at 5/6 of
--size
: take the first row that covers the width. Wider than 480:
--size
is 2.4 × the width rounded up to a multiple of 12, and the time grows with its square. For live, the size only sets the posters. If there is nobody to ask and nothing to read, take 768 and say so.
按照图标实际展示的尺寸进行渲染:视频如果被拉伸到超过自身像素尺寸,边缘就会变软,而且任何编码参数都无法恢复清晰度。先在项目中查找图标的显示宽度,在 CSS 或承载该图标的组件中,以最宽断点为准。只有在无法判断时才询问,并且要在同一消息中询问交付方式:
显示尺寸(CSS px)
--size
视频每个主题
最大 160384320 px约 4 分钟
最大 320768640 px约 17 分钟
最大 4801152960 px约 36 分钟
视频像素宽度至少要是 CSS 宽度的两倍,以适配 retina 屏幕。最终尺寸为
--size
的 5/6:选择表格中第一行能够覆盖目标宽度的配置。宽度超过 480 px 时,
--size
为目标宽度的 2.4 倍,并向上取整为 12 的倍数;耗时随其平方增长。对于 live,尺寸只决定 poster 的尺寸。如果无法询问且项目中也没有可读取的信息,则使用 768,并说明这一点。

1. Concept

1. 概念

Read
concepts.md
. Offer two or three concepts, one line each:
Name. The object · what moves · where the accent lives · the finishes.
Example:
Bolt. A thick soft-bevelled lightning bolt · a charge of light runs down through it twice per loop and pools in the tip · the charge and the pool under the tile · gloss body.
Then suggest two or three palettes for the accent, the one colour the icon carries, and let the person pick or name their own:
Palette. Cyan
#22d3ee
(a cool, technical read) · amber
#f59e0b
(warm, an alert) · neutral
#a1a1aa
(no colour at all: the object reads by its finishes alone).
The colour is the person's call: their brand, their section, their grid. Suggest from the product area (security reads cool, alerts warm, a monochrome grid neutral) and from the icons already beside it on the page; never pick one silently. The palette sets
scene.accent
and, if they want a different emission,
scene.glow
.
Wait for the person to pick. Skip this only when they arrived with the object and the motion already chosen. If there is nobody to ask, take the concept with the clearest silhouette and say which one you took.
What makes a concept good:
  • One object, one idea. A concept that needs a caption is not one yet.
  • Silhouette first. Squint: if the outline alone doesn't say what it is, change the object.
  • The object means something in the product. Auth gets a lock, alerts a bell. See
    concepts.md
    .
  • One motion that tells the story, small and slow: a turn, a swing, a charge, a pulse. Not three things at once.
阅读
concepts.md
。提出两到三个概念,每个概念用一行描述:
名称。 对象 · 运动方式 · 强调色出现的位置 · 表面 finish。
示例:
闪电。 一枚粗壮、圆润倒角的闪电 · 每轮循环中有两股光脉冲沿着它向下奔涌,并在尖端汇聚 · 强调色与光池落在 tile 下方 · 光泽材质。
然后建议两到三种强调色 palette,即图标承载的唯一一种颜色,让对方选择或指定自己的颜色:
Palette。 青色
#22d3ee
(冷色调,具有技术感)· 琥珀色
#f59e0b
(暖色调,适合告警)· 中性色
#a1a1aa
(完全不使用颜色:对象仅依靠 finish 来呈现)。
颜色由对方决定:可以对应其品牌、页面板块或图标网格。根据产品领域进行建议——安全类通常适合冷色,告警类适合暖色,单色网格适合中性色——同时参考页面上已有的相邻图标;绝不默默替对方选择。palette 会设置
scene.accent
;如果对方希望使用不同的自发光颜色,还会设置
scene.glow
。
等待对方选择。只有当对方已经同时确定了对象和运动方式时,才可以跳过这一步。如果无法询问,则选择轮廓最清晰的方案,并说明采用了哪一个。
一个好的概念应具备以下特点:
  • 一个对象,一个想法。 如果一个概念需要文字说明才能理解,就还不算完整。
  • 轮廓优先。 眯起眼睛看:如果仅凭外轮廓无法辨认对象,就更换对象。
  • 对象要在产品中表达明确含义。 Auth 使用锁,告警使用铃铛。参见
    concepts.md
    。
  • **用一种运动讲述故事,**动作要小且缓慢:旋转、摆动、充能或脉动。不要同时做三件事。

2. Read the closest example

2. 阅读最接近的示例

Open the example in
examples/
nearest to the concept and read it whole. Copy its structure: the
scene
block, how it builds geometry with
geo.*
, how its
update(phase)
keeps every term periodic.
打开
examples/
中与概念最接近的示例,并完整阅读。复制它的结构:
scene
block、使用
geo.*
构建几何体的方式,以及
update(phase)
如何确保每个动画项都保持周期性。

3. Write the piece

3. 编写 piece

An ES module with two exports and no imports:
js
export const scene = {
	accent: '#38bdf8',              // the piece's only colour, always as light (a Tailwind 500 reads well)
	glow: '#7dd3fc',                // optional: the colour of its emission; default is the accent a step lighter
	duration: 8,                    // seconds, 4–12; the loop must not jump
	fit: { scale: 0.9, x: 0, y: 0 },// optical fit into the 58% safe area (section 4)
	lights: { tint: 0.45 },         // optional: accent, rim, under, underFill, floor, tint, strips
};

export function object( ctx ) {
	// build with ctx.mat.*, ctx.geo.*, ctx.texture()
	return { mesh, update( phase ) { /* phase runs 0 → 1 once per loop */ } };
}
这是一个仅包含两个导出、没有任何 import 的 ES module:
js
export const scene = {
	accent: '#38bdf8',              // the piece's only colour, always as light (a Tailwind 500 reads well)
	glow: '#7dd3fc',                // optional: the colour of its emission; default is the accent a step lighter
	duration: 8,                    // seconds, 4–12; the loop must not jump
	fit: { scale: 0.9, x: 0, y: 0 },// optical fit into the 58% safe area (section 4)
	lights: { tint: 0.45 },         // optional: accent, rim, under, underFill, floor, tint, strips
};

export function object( ctx ) {
	// build with ctx.mat.*, ctx.geo.*, ctx.texture()
	return { mesh, update( phase ) { /* phase runs 0 → 1 once per loop */ } };
}

ctx

ctx

KeyWhat it is
THREE
the bundle's three instance; the piece never imports three
mat
the material roles below, resolved by the current theme
geo.roundedRect(shape, w, h, r)
a rounded rectangle on a
Shape
or
Path
(holes);
w
,
h
are half extents
geo.bevelExtrude(shape, { depth, bevel, segments, curveSegments })
a shape extruded with soft rounded edges, centred on z = 0
geo.lathe(profile, segments)
a profile of
[x, y]
pairs revolved around +Y
geo.planarUV(geometry, w, h)
uv 0..1 across
[-w, w] × [-h, h]
, for the dynamic texture
texture(w, h)
the one dynamic texture:
{ tex, draw(fn(u, v) → [r, g, b]) }
. Pass
tex
as
emissiveMap
to a role and call
draw
in
update
. A second call throws: the path tracer holds one dynamic texture per object. Details below
gradient(stops, { srgb })
a static vertical gradient, stops
[t, css colour]
from top (0) to bottom (1); for flames and trails
accent
,
glow
the two colours as
THREE.Color
, for the texture's
draw
theme
'dark'
or
'light'
, read only; a last resort
TAU
,
bump(x, c, w)
,
smoothstep(x, a, b)
animation helpers:
bump
is a gaussian
exp(-((x − c) / w)²)
, 1 at
c
and near 0 past
2w
(never exactly 0);
smoothstep
rises 0 → 1 between
a
and
b
键含义
THREE
bundle 中的 three 实例;piece 绝不导入 three
mat
下文所述的材质角色,会根据当前主题解析
geo.roundedRect(shape, w, h, r)
在
Shape
或
Path
上创建圆角矩形,也可创建带孔形状;
w
、
h
为半尺寸
geo.bevelExtrude(shape, { depth, bevel, segments, curveSegments })
将形状挤出为边缘柔和圆润的实体,并以 z = 0 为中心
geo.lathe(profile, segments)
将由
[x, y]
数对组成的 profile 围绕 +Y 旋转成形
geo.planarUV(geometry, w, h)
在
[-w, w] × [-h, h]
范围内生成 0..1 的 UV,供动态纹理使用
texture(w, h)
唯一的动态纹理:
{ tex, draw(fn(u, v) → [r, g, b]) }
。将
tex
作为某个角色的
emissiveMap
传入,并在
update
中调用
draw
。第二次调用会抛出错误:path tracer 每个对象只能持有一张动态纹理
gradient(stops, { srgb })
静态垂直渐变;
stops
格式为
[t, css colour]
,从上到下对应 0 到 1;适用于火焰和拖尾
accent
、
glow
以
THREE.Color
表示的两种颜色,供纹理的
draw
使用
theme
'dark'
或
'light'
,只读;仅在别无办法时使用
TAU
、
bump(x, c, w)
、
smoothstep(x, a, b)
动画辅助函数:
bump
是高斯函数
exp(-((x − c) / w)²)
,在
c
处为 1,超过
2w
后接近 0,但不会恰好为 0;
smoothstep
在
a
到
b
之间从 0 升至 1

Space and size

空间与尺寸

The tile is 2 × 2 units, centred on the origin, facing the camera (+x right, +y up, +z towards the camera). The safe box is 1.16 units square (58% of the tile), centred at y = +0.04. Build the object about 1.0–1.3 units wide and at most ~1.6 tall, centred on its own origin, at
z ≈ 0.45
: the examples are this size at
fit.scale
0.8–0.9.
scene.fit
wraps the object in a group:
scale
multiplies it about the world origin (it does not re-centre), and
x
,
y
move it in world units (
y: -0.08
is 4% of the tile down). Nothing centres the object for you: if it sits off centre on the sheet, centre its geometry first (
geometry.center()
, or move the parts inside a group), then tune
fit
.
Thickness that reads at 64 px: tubes and rods of radius 0.12 or more, bevels of 0.05 or more. The key light is a soft box above and to the right, so a negative
rotation.y
(around −0.3, which turns the object's right side towards the camera) catches it on the bevels.
tile 大小为 2 × 2 个单位,以原点为中心,正对 camera(+x 向右,+y 向上,+z 朝向 camera)。safe box 为边长 1.16 个单位的正方形,即 tile 的 58%,中心位于 y = +0.04。将对象构建为宽约 1.0–1.3 个单位、最高约 1.6 个单位,以自身原点为中心,并放置在 z ≈ 0.45:示例在
fit.scale
为 0.8–0.9 时采用的就是这一尺寸。
scene.fit
会将对象包裹在一个 group 中:
scale
会围绕世界原点缩放对象,但不会重新居中;
x
、
y
以世界单位移动对象(
y: -0.08
表示向下移动 tile 的 4%)。系统不会自动让对象居中:如果它在 sheet 上偏离中心,先对几何体本身进行居中(例如调用
geometry.center()
,或在 group 内移动各部分),然后再调整
fit
。
在 64 px 下仍应清晰可读的厚度:管状物和杆状物的半径至少为 0.12,倒角至少为 0.05。主光源位于右上方,是一块柔和的 soft box,因此负的
rotation.y
(约 −0.3,使对象右侧转向 camera)会让倒角捕捉到主光。

The dynamic texture

动态纹理

draw(fn)
calls
fn(u, v)
once per texel, with
u
and
v
in 0..1 across the texture (
v = 0
is its first row), and the texture is read through the mesh's own UVs. Extrusions and flat shapes need
geo.planarUV(geometry, w, h)
, which maps
v = 0
to the bottom (y = −h) and
u = 0
to the left. A lathe, a tube or a sphere already has UVs (a
TubeGeometry
's
u
runs along its path). Return linear colour in 0..1: values are clamped, and
emissiveIntensity
(the bolt uses 3) scales them. Use
glow.r * k, glow.g * k, glow.b * k
with
k
in 0..1, and keep a dark base (
k ≈ 0
) where the body should stay black.
texture(256, 16)
is enough for a band along a path,
128 × 128
for a face:
draw
runs on the CPU every frame.
draw(fn)
会为每个 texel 调用一次
fn(u, v)
,其中
u
和
v
的范围为 0..1,覆盖整张纹理(
v = 0
对应第一行)。纹理通过 mesh 自身的 UV 进行采样。挤出体和扁平形状需要
geo.planarUV(geometry, w, h)
,它会将
v = 0
映射到底部(y = −h),将
u = 0
映射到左侧。lathe、tube 或 sphere 已经自带 UV(
TubeGeometry
的
u
沿路径延伸)。返回 0..1 范围内的线性颜色:这些值会被截断,并由
emissiveIntensity
(闪电使用 3)进行缩放。使用
glow.r * k, glow.g * k, glow.b * k
,其中 k 为 0..1;主体应保持黑色的区域使用较暗的基准值(k ≈ 0)。路径上的光带使用
texture(256, 16)
已足够,平面使用
128 × 128
即可;
draw
每帧都会在 CPU 上运行。

Material roles

材质角色

Every material comes from
mat.*
. Never construct one (
new THREE.MeshStandardMaterial(…)
fails validation): a hand-made material has one colour, and the light theme needs another.
RoleDarkLightUse it for
mat.body(o)
glossy blackwhite glazed ceramicthe object's mass
mat.chrome(o)
chromechromerings, bands, hinges, the shackle
mat.glass(o)
dark glassclear glass over a white coredies, lenses, screens
mat.inlay(o)
black a step lighterpearl greyraised details on the body
mat.glow(k, o)
emission in the glow colourthe glow as surface colour, faint emissionbadges, keyholes, a tip bead: one small lit part, never the mass
mat.light(k, o)
warm white glowwarm, faint glowwindows, portholes, flames
o
overrides physical values only:
finish
,
finishRepeat
,
roughness
,
metalness
,
clearcoat
,
normalMap
,
side
,
transparent
,
depthWrite
,
emissiveMap
,
alphaMap
,
emissiveIntensity
. Passing a colour (
color
,
emissive
, …) throws: the theme picks colours. A dynamic texture on
emissiveMap
emits in dark and becomes pigment on white ceramic in light, so the same
draw
works in both. A
mat.glow
part is the one place the accent shows as a surface in the light theme, which is why it stays small: a badge, a bead, a keyhole.
所有材质都来自
mat.*
。绝不自行创建材质(
new THREE.MeshStandardMaterial(…)
会导致校验失败):手动创建的材质只有一种颜色,而浅色主题需要另一种颜色。
角色深色主题浅色主题用途
mat.body(o)
光泽黑色白色釉面陶瓷对象主体
mat.chrome(o)
镀铬镀铬环、箍、铰链、锁梁
mat.glass(o)
深色玻璃透明玻璃覆盖白色核心骰子、镜头、屏幕
mat.inlay(o)
略浅的黑色珍珠灰主体上的凸起细节
mat.glow(k, o)
使用 glow 色发光glow 作为表面色,并带微弱自发光徽章、锁孔、尖端珠饰:一个小面积的发光部件,绝不能成为主体
mat.light(k, o)
暖白色辉光微弱的暖色辉光窗户、舷窗、火焰
o
只能覆盖物理属性:
finish
、
finishRepeat
、
roughness
、
metalness
、
clearcoat
、
normalMap
、
side
、
transparent
、
depthWrite
、
emissiveMap
、
alphaMap
、
emissiveIntensity
。传入颜色(
color
、
emissive
等)会抛出错误,因为颜色由主题决定。深色主题中,动态纹理会作为
emissiveMap
发光;浅色主题中,同一个动态纹理会成为白色陶瓷上的颜料,因此同一份
draw
可以同时适用于两个主题。
mat.glow
部件是浅色主题中唯一会以强调色显示为表面色的区域,因此它必须保持小巧,例如徽章、珠饰或锁孔。

Finishes

Finish

Every role takes a surface finish:
mat.body({ finish: 'brushed', finishRepeat: 5 })
. The finish sets the physical values (and, for the textured ones, a fine static map) and keeps the role's colour, so it works in both themes. Values the piece passes itself still win over the finish.
FinishWhat the light does on itGood on
gloss
a mirror under clearcoat: sharp reflections of the studio stripsbodies that should read as lacquer
matte
no reflection, only soft form shadingskirts, cores, parts that must sit back
brushed
metal with fine lines: the highlight stretches into a streak across themplates, frames, collars, a lock's body
perforated
metal with a grid of round holes, each catching its own glintgrilles, keycap plates, speaker faces
speckle
a rough field with sparse bright glints, like stone or sparkle paintcaps, keytops, a bell's body
finishRepeat
is how many tiles per UV unit (default 10). Extrusions have UVs in scene units, so the pitch is the same at any size; lathes and tubes run 0..1, so they want a smaller number (3–5). A fine finish reads as a material; a coarse one reads as a print (brushed lines too wide looked like wood grain).
The two thin strips that draw those crisp lines can be scaled with
scene.lights.strips
(0–1, default 1). Turn them down when a fine bevel sweeps through one as the object turns: the reflection is thinner than a pixel and flickers on and off across frames. The bolt uses
strips: 0
.
validate.mjs
reads the source as text, so even a comment with
…Material(
in it fails
materials
.
每个角色都可以指定表面 finish,例如
mat.body({ finish: 'brushed', finishRepeat: 5 })
。finish 决定物理属性;对于带纹理的 finish,还会生成精细的静态贴图,同时保留角色原有的颜色,因此可以同时适用于两个主题。piece 自身传入的值仍优先于 finish。
Finish光线在其上的表现适合用于
gloss
clearcoat 下呈镜面效果,可清晰反射 studio 条形灯需要呈现漆面质感的物体
matte
不产生反射,仅呈现柔和的形体明暗裙边、核心,以及需要后退到背景中的部件
brushed
带细密纹路的金属,高光会横跨表面形成拉长的光 streak板件、框架、套环、锁体
perforated
带圆孔网格的金属,每个孔都会捕捉独立的高光格栅、键帽底板、扬声器面板
speckle
粗糙表面散布少量明亮闪光,类似石材或闪粉涂料盖子、键帽顶部、铃铛主体
finishRepeat
表示每个 UV 单位包含多少个纹理分块,默认值为 10。挤出体的 UV 以场景单位计算,因此无论尺寸大小,纹理间距都保持一致;lathe 和 tube 的 UV 范围为 0..1,因此通常需要更小的值(3–5)。精细的 finish 看起来像一种材质;粗糙的 finish 则像印花(过宽的 brushed 纹路看起来会像木纹)。
用于绘制清晰线条的两条细窄条形灯可以通过
scene.lights.strips
(0–1,默认 1)进行缩放。当细倒角在对象旋转时扫过条形灯,应将其调低:反射可能细于一个像素,并会在不同帧中忽明忽暗。闪电使用了
strips: 0
。
validate.mjs
会以文本形式读取源代码,因此即使注释中出现
…Material(
,也会导致
materials
规则失败。

The craft rules

工艺规则

  • One object, one idea. No second object, no props, no text.
  • Silhouette first. It reads at 64 px or it doesn't read.
  • Polished light on every icon. The studio has broad soft boxes and two thin hard strips: the strips draw crisp lines of light along bevels and edges. Give the icon at least two finishes so those lines change character across it (a gloss top over a matte skirt, a brushed frame around a speckled body). The icon should read by how its surfaces take the light, not only by its outline.
  • The accent only as light. The body stays black (dark) or white (light). The accent appears as glow, as the pool under the tile and on the rim. Never paint a surface the accent colour.
  • Fit in the 58% box. The solid mass sits in the safe area, centred a touch above the middle, and never comes within 4% of the tile edge. Use
    scene.fit
    , not the object's own position, to place it.
  • Loop without a jump. Every term of
    update
    is a function of
    phase · TAU
    with integer harmonics, or a
    smoothstep
    envelope that is 0 at both ends of the loop. Phase 1 must look exactly like phase 0.
  • Light stays in the tile. Glow and halos that spill past the tile edge fail validation.
  • Never edit
    runtime/
    .
  • 一个对象,一个想法。 不添加第二个对象、道具或文字。
  • 轮廓优先。 如果在 64 px 下无法辨认,就没有通过。
  • 每个图标都要有精致的光照。 studio 包含宽大的 soft box 和两条细窄的 hard strip:这些条形灯会沿倒角和边缘绘制清晰的光线线条。图标至少应使用两种 finish,让这些光线线条在不同表面上呈现不同质感,例如光泽顶部搭配 matte 裙边,或 brushed 框架围绕 speckled 主体。图标应通过表面如何承接光线来呈现,而不只是依靠轮廓。
  • 强调色只以光的形式出现。 主体在深色主题中保持黑色,在浅色主题中保持白色。强调色只表现为辉光、tile 下方的光池以及边缘光。绝不能用强调色涂绘表面。
  • 适配 58% safe box。 实体主体位于安全区域内,略高于中心,且绝不能进入 tile 边缘 4% 以内。使用
    scene.fit
    放置对象,而不是修改对象自身的位置。
  • 循环时不跳变。
    update
    中的每一项都必须是
    phase · TAU
    的函数,并使用整数谐波;或者使用在循环首尾均为 0 的
    smoothstep
    envelope。phase 1 必须与 phase 0 完全一致。
  • 光线始终留在 tile 内。 溢出 tile 边缘的辉光和光晕会导致校验失败。
  • 绝不编辑
    runtime/
    。

4. Sheet → read → fix

4. Sheet → 读取 → 修正

node <skill>/sheet.mjs <piece...> [--frames 0,60,120,180] [--guide] [--pt] [--out sheet.png]
It renders raster frames 0, 60, 120 and 180 (or
--frames
) as 256 px cells, a dark row and a light row per piece, with
--guide
drawing the 58% safe box in violet.
--pt
adds a fifth cell to each row: frame 0 path traced and denoised. A raster run takes a few seconds per piece;
--pt
adds about half a minute per piece. Pass an example after your piece to compare them row by row.
Read the PNG. Check: the silhouette reads, the mass sits in the box in every frame, the light row reads as well as the dark one (glow visible on white, nothing grey and muddy), the motion is visible across the frames. Fix and repeat until it would sit beside the examples. Run
--pt
at least once before the full render: the raster cells are brighter than the final, and a black part that reads in raster can sink into the dark tile once path traced. To inspect a detail at 256 px, sheet a throwaway copy with a larger
fit.scale
in the scratch directory, never in the person's project.
node <skill>/sheet.mjs <piece...> [--frames 0,60,120,180] [--guide] [--pt] [--out sheet.png]
它会将 raster 帧 0、60、120 和 180(或
--frames
指定的帧)渲染为 256 px 单元格;每个 piece 包含一行深色主题和一行浅色主题。
--guide
会以紫色绘制 58% safe box。
--pt
会为每行增加第五个单元格:帧 0 的 path tracing 与降噪结果。一次 raster 渲染每个 piece 需要几秒钟;使用
--pt
时,每个 piece 约增加半分钟。可以将一个示例放在 piece 后面,以便逐行对比。
读取 PNG。检查:轮廓是否清晰可辨;每一帧中主体是否都位于 safe box 内;浅色行是否与深色行一样清晰(在白色背景上应能看到辉光,且不能出现灰色、发浑的效果);跨帧观察时运动是否清楚。修正并重复,直到作品能够与示例并列展示。完整渲染前至少运行一次
--pt
:raster 单元格比最终效果更亮,因此在 raster 中能够看清的黑色部件,经过 path tracing 后可能会沉入深色 tile。若要在 256 px 下检查细节,可在 scratch 目录中生成一份
fit.scale
更大的临时副本并制作 sheet,绝不要在对方项目中这样做。

5. Validate

5. 校验

node <skill>/validate.mjs <name>.specimen.js
It prints each failed rule with its reason and a number, and exits 1 on any failure. Repeat until it prints
ok
.
RuleWhat fails it
contract
no
scene
or
object
export;
accent
not a hex colour;
duration
outside 4–12;
object()
throws or returns no
mesh
materials
a
…Material(
constructor in the source
seam
raster phase 0 against phase 1 differs by more than 0.5 on average (0–255)
fit
the solid mass (the middle 90% of the object's silhouette, with transparent meshes such as flames and halos left out; opaque
mat.glow
parts count) leaves the safe box by more than 3% of the tile, or the whole silhouette comes within 4% of the tile edge, on frames at 0, ¼, ½ and ¾ of the loop
light-in-tile
anything outside the tile is more than 2/255 brighter than the page
one-texture
texture()
called twice
node <skill>/validate.mjs <name>.specimen.js
它会打印每一条失败规则及其原因和编号;只要存在失败项,就会以状态码 1 退出。重复执行,直到输出
ok
。
规则失败条件
contract
没有导出
scene
或
object
;
accent
不是十六进制颜色;
duration
不在 4–12 范围内;
object()
抛出异常或没有返回
mesh
materials
源代码中出现
…Material(
构造函数
seam
raster 的 phase 0 与 phase 1 平均差异超过 0.5(范围为 0–255)
fit
在循环的 0、¼、½ 和 ¾ 帧中,实体主体(对象轮廓中间 90% 的区域, flames 和 halos 等透明 mesh 不计入,但 opaque 的
mat.glow
部件需要计入)超出 safe box 超过 tile 的 3%,或者整个轮廓进入 tile 边缘 4% 以内
light-in-tile
tile 外的任何区域比页面背景亮出超过 2/255
one-texture
调用了两次
texture()

6. Render and hand over

6. 渲染与交付

Announce the time before you start, from the size table in §0: at
--size 768
it is about 4 s a frame on a recent Mac, so an 8 s loop (240 frames) is about 17 minutes per theme, about 35 for both. Live alone renders only the two posters, a minute or so. A video render outlasts any foreground command timeout, so run it in the background and watch its progress lines (
30/240 · 666 s left
, every 30 frames) until it prints the files; never run it in the foreground, where a timeout kills it halfway.
node <skill>/render.mjs <name>.specimen.js --out public/icons --size 768            # video, both themes
node <skill>/render.mjs <name>.specimen.js --out public/icons --live     # live
node <skill>/render.mjs <name>.specimen.js --out public/icons --live --video   # both
--themes dark
renders one theme. It prints the files with their sizes and the snippet; the snippet's paths assume the folder is served at
/<folder name>
: set the prefix to where it is really served.
Hand over the files and the snippet, then ask whether the site follows the OS theme or has a toggle (
[data-theme]
). The printed video snippet follows the OS:
html
<video autoplay muted loop playsinline poster="/icons/bolt-dark-poster.jpg">
  <source src="/icons/bolt-light.webm" type="video/webm" media="(prefers-color-scheme: light)">
  <source src="/icons/bolt-light.mp4" type="video/mp4" media="(prefers-color-scheme: light)">
  <source src="/icons/bolt-dark.webm" type="video/webm">
  <source src="/icons/bolt-dark.mp4" type="video/mp4">
</video>
For a site with a
[data-theme]
toggle, give two videos and the CSS that picks one:
html
<video class="specimen-dark" autoplay muted loop playsinline poster="/icons/bolt-dark-poster.jpg">
  <source src="/icons/bolt-dark.webm" type="video/webm">
  <source src="/icons/bolt-dark.mp4" type="video/mp4">
</video>
<video class="specimen-light" autoplay muted loop playsinline poster="/icons/bolt-light-poster.jpg">
  <source src="/icons/bolt-light.webm" type="video/webm">
  <source src="/icons/bolt-light.mp4" type="video/mp4">
</video>
<style>
  .specimen-light { display: none; }
  [data-theme="light"] .specimen-light { display: block; }
  [data-theme="light"] .specimen-dark { display: none; }
</style>
Live:
html
<specimen-icon src="/icons/bolt.specimen.js" poster="/icons/bolt-dark-poster.jpg" poster-light="/icons/bolt-light-poster.jpg"></specimen-icon>
<script type="module" src="/icons/specimen.js"></script>
<specimen-icon>
follows
prefers-color-scheme
unless it has
theme="dark|light"
(set it from the toggle on a
[data-theme]
site), pauses off screen, and keeps the poster when WebGPU is missing or reduced motion is set:
poster-light
in the light theme,
poster
otherwise. Size it with CSS
width
(it stays square; 160 px by default).
Outside the tile every frame is exactly the page colour (black or white). On a section of another colour, blend it in:
mix-blend-mode: screen
for dark,
multiply
for light.
开始前,根据 §0 的尺寸表说明预计耗时:在
--size 768
下,近年款 Mac 每帧约需 4 秒,因此一个 8 秒循环(240 帧)每个主题约需 17 分钟,两个主题合计约 35 分钟。仅使用 live 时只会渲染两张 poster,大约一分钟即可完成。视频渲染时间可能超过任何前台命令的 timeout,因此应在后台运行,并持续观察进度行(例如
30/240 · 666 s left
,每 30 帧输出一次),直到脚本打印出文件;绝不要在前台运行,否则 timeout 会在渲染途中将其终止。
node <skill>/render.mjs <name>.specimen.js --out public/icons --size 768            # video, both themes
node <skill>/render.mjs <name>.specimen.js --out public/icons --live     # live
node <skill>/render.mjs <name>.specimen.js --out public/icons --live --video   # both
--themes dark
只渲染一个主题。脚本会打印文件及其大小,并输出代码片段;代码片段中的路径假定该文件夹通过
/<folder name>
提供,应将前缀设置为实际服务路径。
交付文件和代码片段,然后询问网站是跟随 OS 主题,还是提供主题切换按钮(
[data-theme]
)。打印出的 video 代码片段会跟随 OS 主题:
html
<video autoplay muted loop playsinline poster="/icons/bolt-dark-poster.jpg">
  <source src="/icons/bolt-light.webm" type="video/webm" media="(prefers-color-scheme: light)">
  <source src="/icons/bolt-light.mp4" type="video/mp4" media="(prefers-color-scheme: light)">
  <source src="/icons/bolt-dark.webm" type="video/webm">
  <source src="/icons/bolt-dark.mp4" type="video/mp4">
</video>
对于带
[data-theme]
切换功能的网站,提供两段视频以及用于选择主题的 CSS:
html
<video class="specimen-dark" autoplay muted loop playsinline poster="/icons/bolt-dark-poster.jpg">
  <source src="/icons/bolt-dark.webm" type="video/webm">
  <source src="/icons/bolt-dark.mp4" type="video/mp4">
</video>
<video class="specimen-light" autoplay muted loop playsinline poster="/icons/bolt-light-poster.jpg">
  <source src="/icons/bolt-light.webm" type="video/webm">
  <source src="/icons/bolt-light.mp4" type="video/mp4">
</video>
<style>
  .specimen-light { display: none; }
  [data-theme="light"] .specimen-light { display: block; }
  [data-theme="light"] .specimen-dark { display: none; }
</style>
Live:
html
<specimen-icon src="/icons/bolt.specimen.js" poster="/icons/bolt-dark-poster.jpg" poster-light="/icons/bolt-light-poster.jpg"></specimen-icon>
<script type="module" src="/icons/specimen.js"></script>
除非设置
theme="dark|light"
(在带
[data-theme]
的网站中,应由主题切换按钮设置),否则
<specimen-icon>
会跟随
prefers-color-scheme
。当它离开可视区域时会暂停;如果缺少 WebGPU 或启用了 reduced motion,则保留 poster:浅色主题使用
poster-light
,其他情况使用
poster
。通过 CSS
width
设置尺寸(始终保持正方形;默认宽度为 160 px)。
每一帧中,tile 外的区域都精确使用页面颜色(黑色或白色)。如果所在 section 使用其他颜色,则进行混合:深色主题使用
mix-blend-mode: screen
,浅色主题使用
multiply
。

7. The bar

7. 质量基准

  • examples/rocket.specimen.js
    — a rocket flying up-right, one barrel roll per loop; a flame from
    gradient
    that lights the tile; chrome band and nozzle;
    mat.light
    porthole.
  • examples/bolt.specimen.js
    — a thick bevelled bolt; the one dynamic texture runs a charge down it and pools it in the tip.
  • examples/bell.specimen.js
    — a lathed bell that rings twice per loop, a chrome clapper lagging behind; a
    mat.glow
    badge that pops when it rings.
  • examples/lock.specimen.js
    — a padlock whose chrome shackle lifts and swings open once per loop while the keyhole lights;
    fit.y
    keeps the lifted shackle off the top.
  • examples/chip.specimen.js
    — a chip with chrome pins on every side; a
    mat.glass
    die where square pulses of light, drawn through the dynamic texture, ripple outward twice per loop.
  • examples/cube.specimen.js
    — a colourless puzzle cube: 26 cubies, every sticker a different finish of the same black (white in light), layers that snap a quarter turn four times per loop and close solved. The neutral palette: it reads by finishes alone.
  • examples/key.specimen.js
    — a keyboard keycap on a perforated chrome plate, a speckled top over a matte skirt, a lit return-arrow legend; it presses twice per loop with a bounce while the light under it flares.
  • examples/gears.specimen.js
    — two meshed chrome gears, the large one with lightening holes, that tick one tooth at a time four times per loop while the hub flares. Rotating parts with symmetric geometry keep plain chrome: a textured finish would turn with them and break the loop.
  • examples/rocket.specimen.js
    ——一枚向右上方飞行的火箭,每轮循环完成一次整圈滚转;由
    gradient
    生成并照亮 tile 的火焰;镀铬环带和喷嘴;
    mat.light
    舷窗。
  • examples/bolt.specimen.js
    ——一枚粗壮、圆润倒角的闪电;唯一的动态纹理让一道充能光沿其向下移动,并汇聚在尖端。
  • examples/bell.specimen.js
    ——一只通过 lathe 构建的铃铛,每轮循环鸣响两次;镀铬铃舌滞后摆动;
    mat.glow
    徽章在铃响时弹出。
  • examples/lock.specimen.js
    ——一把挂锁,其镀铬锁梁在每轮循环中抬起并摆动打开一次,同时锁孔亮起;
    fit.y
    可防止抬起的锁梁超出顶部。
  • examples/chip.specimen.js
    ——一块四边带有镀铬引脚的芯片;
    mat.glass
    骰子上的方形光脉冲通过动态纹理绘制,并在每轮循环中向外扩散两次。
  • examples/cube.specimen.js
    ——一个无色魔方:26 个小方块,每张贴纸采用相同的黑色但不同 finish(浅色主题中则为白色);各层每轮循环咔哒转动四分之一圈,共四次,最终回到已解状态。使用中性 palette:仅依靠 finish 就能清晰呈现。
  • examples/key.specimen.js
    ——一个位于 perforated 镀铬底板上的键盘键帽,speckled 顶部搭配 matte 裙边,带有发光的回车箭头图例;它每轮循环按压两次并产生回弹,同时下方光线增强。
  • examples/gears.specimen.js
    ——两个相互啮合的镀铬齿轮,大齿轮带有减重孔;它们每轮循环逐齿转动四次,同时轮毂产生辉光。采用对称几何体的旋转部件应保持普通镀铬材质:带纹理的 finish 会随部件一起转动,从而破坏循环。

8. Never

8. 绝不执行

  • Never edit
    runtime/
    , and never ship a changed copy of
    specimen.js
    .
  • Never construct a material or pick a colour in the piece; every surface comes from
    mat.*
    .
  • Never call
    texture()
    twice, or import anything in the piece.
  • Never run
    npm install
    , add a dependency, or create a folder of your own in the person's project.
  • Never call live "path traced": it is raster.
  • Never render before
    validate.mjs
    prints
    ok
    .
  • 绝不编辑
    runtime/
    ,也绝不交付修改过的
    specimen.js
    副本。
  • 绝不在 piece 中创建材质或选择颜色;所有表面都必须来自
    mat.*
    。
  • 绝不调用两次
    texture()
    ,也绝不在 piece 中导入任何内容。
  • 绝不运行
    npm install
    、添加依赖,或在对方项目中创建自己的文件夹。
  • 绝不能将 live 称为 path traced;它是 raster。
  • 在
    validate.mjs
    输出
    ok
    之前,绝不进行渲染。