MarkStageProvider
让长得一样的角色共享同一份动画。
上面四十四个角色。三个正在运行的引擎。
它怎么工作
Provider 为每一种不同的外观保留一个隐藏角色,每个可见实例都变成指向它的 <use href="#…">。浏览器复用源这一帧渲染出的内容,所以一个镜像只花一个元素,没有动画工作。
外观是形状、状态、脸型和笔
key 由形状、状态、脸型、鼻子、线宽、强调与暂停组成。墨色和尺寸被刻意排除。带 hud 的角色会整个离开舞台、自己跑一个引擎:表盘是实时读数,按它的值做 key 会每一帧都铸出一个新源。
脸型和鼻子包含在内,因为镜像显示的就是源画出的东西:线条脸的角色不能借用别的脸的源。两个鼻子不同的线条脸 blob 是两个源。默认脸上的鼻子没有意义,所以从 key 里丢掉,不会拆分舞台。
墨色是 CSS 自定义属性,可继承的自定义属性在使用处解析而不是定义处——所以每个镜像都可以是不同墨色,却共享同一份动画。尺寸设在外层元素上,因此也可以自由变化。
这是整套设计,也很容易搞错。按 agent 身份给舞台建索引读起来很自然,因为应用手里拿着的就是这么一份列表。但它什么都共享不了:每个 agent 按定义都是唯一的,五十个 agent 就是五十个引擎。按外观建索引,五十个都是 idle blob 的 agent 恰好只要一个。
上限
maxSources 限制同时运行的引擎数,默认 36。超出上限后认领仍会被记录,但不再托管新的源,因此页面的退化方式是让一部分角色静止,而不是让所有角色都变慢。
认领会合并
滚动时 IntersectionObserver 可能在一帧里产生几十次认领。分别提交每一次会在主线程最忙的时刻把 Provider 重渲染几十次,所以认领被批成每帧一次更新。
源的尺寸
隐藏源渲染在 96 像素,而不是收成没有。任何测量自身元素的东西读到的是源的盒子,而不是镜像的,所以缩到几个像素的源会让每个镜像都表现得像自己很小。
在 Provider 外面
StagedMark 会回退到自己的 CharacterMark,所以使用它的组件可以单独工作——在测试里,或在文档示例里——树上不必有 Provider。
style 与 ref
style 最后应用,所以在那里设置的 --aora-fg 会覆盖由 color 推出的墨色。这是把一个可动画的自定义属性——而不是固定墨色——交给角色的唯一办法,群组活动的墨色过渡就靠它。
ref 暴露和 CharacterMark 相同的 CharacterHandle,但只在这个角色自己拥有引擎时才有效——对 StagedMark 来说就是在 provider 之外。在 provider 之内,每个角色都是舞台隐藏源的镜像,与所有同外观的角色共用动画,无法单独动作;那种情况下句柄干脆不挂载,usePokeCycle 会报告没有什么可戳。
第一个角色通过 style 改了墨色;第二个是镜像,句柄保持 null,于是按钮落到第三个——它在 provider 之外,跑自己的引擎。要有意让整组角色一起动,走共享的角色指令,而不是逐个 ref。
API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children* | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | null | — | — |
maxSources | number | 36 | Cap on simultaneously running engines. Claims past the cap fall back to a static render rather than degrading every animation on the page. |
StagedMark
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
className | string | — | — |
color | "black" | "brown" | "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "violet" | "magenta" | "pink" | "gray" | — | — |
emotionId | string | — | Protocol id, for hosts that speak the wire format. Ignored when `state` is given. |
emphasis | boolean | false | Enlarge and converge the eyes. Part of the shared appearance. |
eyeColor | string | — | — |
face | "cute" | "eyes" | "line" | — | Face style. Part of the shared appearance, so line-face marks share a line-face source. |
hud | HudSpec | null | null | A dial in the ring outside the body. `null` removes it. A HUD spec is a live readout whose value moves every tick, so unlike the other geometry props it cannot be folded into the appearance key — each new value would mint a source and orphan the last one. A mark with a HUD therefore leaves the stage and runs its own engine, which is what a per-instance readout needs anyway. |
lineWeight | number | — | Multiplier on the shared line weight. Part of the shared appearance, so a heavier-pen mark gets its own source rather than mirroring a light one. |
nose | "slant" | "angular" | "hook" | "long" | "curve" | "bow" | null | — | Nose for the line face. Unlike `CharacterMark`, the default here is `null` rather than seed-derived: mirrors of one source must agree on a nose, and "the seed's nose" differs per mark. |
paused | boolean | false | Hold the current frame. Part of the shared appearance. |
preset | "mochi" | "sprout" | "boba" | "dot" | "pip" | "byte" | "momo" | "bao" | "teal" | "tato" | "hexa" | "tri" | "gemma" | "nimbo" | "dew" | "leafy" | "bloom" | "lucky" | "shroom" | "oak" | "pokey" | "nashi" | "pomme" | "berry" | "cheri" | "twinkle" | "luv" | "ember" | "chime" | "jelly" | "beep" | "luna" | "finn" | "inky" | "miso" | "hop" | "bruno" | "chirp" | "waddle" | "oink" | "rusty" | "kiki" | "maru" | "zip" | "fuzz" | "lulu" | — | A named character from the cast. Supplies `shape`, `color`, `face` and `nose` when they are not given. |
ref | Ref<CharacterHandle> | — | Flourishes, when this mark owns an engine. A mirror shares its animation with every other mark of the same appearance and so cannot move alone; the handle is simply never attached in that case, and `usePokeCycle` reports back that there was nothing to poke. |
shape | "mochi" | "circle" | "blob" | "pebble" | "egg" | "bun" | "tofu" | "pill" | "hex" | "onigiri" | "gem" | "cloud" | "drop" | "leaf" | "flower" | "clover" | "sprout" | "mushroom" | "acorn" | "cactus" | "pear" | "apple" | "strawberry" | "cherry" | "star" | "heart" | "flame" | "bell" | "ghost" | "bot" | "crescent" | "boba" | "fish" | "octopus" | "cat" | "bunny" | "bear" | "chick" | "penguin" | "pig" | "fox" | "bob" | "topknot" | "spiky" | "curly" | "twinbuns" | — | — |
sizePx | number | 28 | — |
state | "idle" | "sleeping" | "drowsy" | "bored" | "waking" | "powering-down" | "listening" | "thinking" | "searching" | "reading" | "writing" | "working" | "loading" | "progress" | "radar" | "orbit" | "dictating" | "uploading" | "sending" | "receiving" | "messaging" | "waiting" | "spawning" | "composing" | "dragging" | "happy" | "excited" | "laughing" | "playful" | "proud" | "celebrate" | "bouncing" | "shy" | "sad" | "angry" | "confused" | "suspicious" | "surprised" | "scared" | "curious" | "humming" | "alerting" | "notifying" | "refusing" | — | — |
style | CSSProperties | — | Applied last, so `--aora-fg` here overrides the ink derived from `color`. That is the only way to hand a mark an animatable custom property instead of a fixed colour, which is what the group activity ink transition needs. |
styleId | StyleId | — | Style series. Part of the shared appearance, so a pixel mark gets a pixel source. Named `styleId` because `style` is the inline CSS. Falls back to the surrounding `AppearanceProvider`. |
styleOptions | Record<string, unknown> | — | Parameters of the series. Part of the shared appearance, compared by value after the engine's own clamping, so `{}` and the defaults share a source. |
variant | StyleVariant | — | Paint variant within the series. Part of the shared appearance. |
接下来
- 性能——怎样渲染上百个角色
- useMarkCommands——hold / release / poke / spin