paper-cinder-transition
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePaper Cinder Transition
纸张余烬转场
A page does not fade into the next one, it is eaten by a line of fire. Reach for
when the burn is a ring on a loading screen that must survive a
blocked main thread; reach for this when the burn is the change between two scenes
you own. For text that is written in by a mask rather than burned, use ;
for a scroll-scrubbed sequence with no burn, .
fire-paper-loadermasked-revealscroll-scrubbed-visual-sequence页面不是淡入下一页面,而是被一道火线吞噬。当燃烧效果是加载屏上的圆环,并且需要
在主线程被阻塞时仍能正常工作时,请使用 ;当燃烧效果用于切换
你掌控的两个场景时,请使用本方案。对于通过蒙版写入而非燃烧呈现的文本,使用
;对于没有燃烧效果、由滚动进度驱动的序列,使用
。
fire-paper-loadermasked-revealscroll-scrubbed-visual-sequenceThe mechanism
机制
One full-screen pass decides, per pixel, whether it belongs to the frozen old frame
(ahead of the line), to the burning zone (the line), or to the live new scene (behind
it), from a single signed distance to a noisy front. Everything else is staging.
Remove any of the three layers and it stops reading as fire: no frozen frame and the old
page keeps moving under the flame, no live scene and the reveal is a still, no
pixel-true distance and the edge is a soft blotch.
xc = signed distance to the front in CSS px (+ ahead, - burnt)
reveal = clamp(0.5 - xc/aa, 0, 1) // new frame from the front back, cut to the pixel
col = mix(fromFrozen, toLive, reveal)
col = mix(col, charcoal, bed * pores) // 26 px band of torn, porous char (alpha-masked)
col += flame(xc) + charGlow(xc) // additive: thread, ember zone, tight glow全屏 pass 根据到含噪前沿的单一有符号距离,逐像素判断该像素属于冻结的旧帧
(位于线条前方)、燃烧区(线条本身),还是实时的新场景(位于线条后方)。其余部分都只是布景。
移除三个图层中的任意一个,它就不再像火焰:没有冻结帧,旧页面会在火焰下继续运动;
没有实时场景,显露出来的就是静止画面;没有像素级精确的距离,边缘就会变成柔软的斑块。
xc = signed distance to the front in CSS px (+ ahead, - burnt)
reveal = clamp(0.5 - xc/aa, 0, 1) // new frame from the front back, cut to the pixel
col = mix(fromFrozen, toLive, reveal)
col = mix(col, charcoal, bed * pores) // 26 px band of torn, porous char (alpha-masked)
col += flame(xc) + charGlow(xc) // additive: thread, ember zone, tight glowStaging split
场景布置拆分
| goes in the skill | staging (yours to change) |
|---|---|
| the composite order, the distance field, the ember ramp, char rules, the noise bake | what the two scenes are, palette of the scenes, typography, page copy |
| the two scene buffers (frozen from, live to) and the scissor | which side the line enters from (see below) |
| timing, easing, DPR, reduced-motion still | control chrome |
| 放入 skill 的内容 | 场景布置(由你调整) |
|---|---|
| 合成顺序、距离场、余烬渐变、炭化规则、噪声烘焙 | 两个场景的内容、场景配色、排版、页面文案 |
| 两个场景的 buffer(冻结的 from、实时的 to)以及 scissor | 线条从哪一侧进入(见下文) |
| 时序、缓动、DPR、reduced-motion 静帧 | 控制组件 |
Rules, each tied to the failure it prevents
每项规则都对应它所防止的失败
Draw the frozen old frame and the live new scene, never one fading into the other.
Snapshot the outgoing scene once into a full-resolution target when the transition starts
(clock frozen) and keep rendering the incoming scene live. A cross-fade, or a burn over a
still-animating old scene, shows two chapters in one patch of screen and the fire stops
being the thing that changes the page.
Measure the front in pixels, not in field units. .
A level set alone runs a hair-thin seam wherever the noise leans with the sweep and a fat
one where it leans against it, so the flame's width is uneven for no reason.
xc = (field - T)/grad * pxHeight / pxScaleTake the slope by finite differences a few px apart, never of the baked texture.
The baked noise's slope is constant across each texel; per pixel it stripes the band
across the line. Sample the broad field at in x and y () and
clamp the gradient to .
dFdx+4 pxhStep = 4*uPxS/uPxH0.35/aspect .. 3/aspectThe ember zone is a smooth luminous gradient, not steps. Symptom: it reads as a
non-realistic poster. Grade it continuously by a heat variable: yellow at the front, then
orange, then deep red at a crisp torn lip. Hot stretches carry yellow further back. Ramp
constants (linear, before tone map):
| stop | rgb | where |
|---|---|---|
| deep | | lip, tau 0.05 |
| orange | | tau 0.42 |
| gold | | tau 0.76 |
| hot gold | | tau above 0.86 |
| thread | | the front line |
| glow | | 2 to 5 px round the thread |
Keep hot colours gold, never cream. A pale yellow reads cream at full heat and khaki
when dimmed. Nothing on the ramp is grey or white; a dim stretch goes down the ramp, it is
not just darker.
The front is a bright thread that beads and pinches, 0.7 to 5 px, not an even stroke.
An even stroke reads as neon. Width with 0.5 to 1.5 from
a noise along the edge. Give it a tight glow only (), no wide halo.
(0.9 + 2.6*heat) * beadbeadexp(-|xc+0.5wc| / (2 + 2.8*heat))It must visibly move, at rest of the sweep too. Symptom: "too stale/static". Drive heat
with pulses that run along the edge in both directions (,
), flicker at , a roll back through the zone at , drifting
beads (), and licking tongues on the front (, , at most 0.022 and 0.010
of the screen). Stills hide all of this, so judge the burn in a real-time held clip.
sA*0.018 - t*1.30sA*0.027 + t*1.70t*7-t*2.6-t*0.8t*1.1t*2.8Alpha-mask the black char; it is not a flat slab. The char is a band 26 px wide
(plus 12 px of front slack) that crumbles off the live scene along a torn far edge, with
pores opening in it as it ages, down to whatever lies beneath. A solid black strip reads as
a wipe. Cover = , edge torn by px, which is
gentler than the front's own slope so it frays without cutting loose an island.
bed * pores8*noise(0.08) + 3*noise(0.25)Never ring the pores with a glowing rim. Rims joined into one drawn contour along the
char edge. Pores are just holes.
Take the char's edge from the broad front, not the fine one. A 2-D noise cut loose
islands of char that floated on the new frame. Use a noise along the edge only
(), and let with at most 12.
kbEdgeNLineedge = zoneW + fs + bedW*(0.35 + 1.10*n)fsScorch the old frame ahead of the flame in one continuous darkening, 20 to 40 px reach,
from tan to dark brown by , reach varied by
noise. Three separate steps drew outlines that copied the flame.
0.80, 0.62, 0.420.20, 0.10, 0.045tk^2Bake the noise; do not stack fbm per pixel. Cost at 2880x1800: per-pixel fbm
1.5 to 3 ms a pass; one tileable texture and a hash table, 0.3 to 0.6 ms with the same look.
Bake once: a 1024^2 RGBA16F texture on a 16-cell period (r fbm 3 octaves, g ridged 3, b
gradient, a ridged 2, all sampled at ) and the integer hash lattice in a 2048^2
R16F table, filtered at the smoothstep of the fraction so it equals the value noise exactly.
x*0.0625Scissor the live scene to the revealed side, and render it small. Behind the line only,
plus of the screen for bays and haze. During a burn draw it at scale (the
composite stays full size, because the edge is what the eye reads); once the transition has
rested 150 ms, cross-fade to the full-size scene over 0.3 s, both drawn meanwhile, so the
sharpness step is never seen and never costs a moving frame. Put text on the live side at
full size in the composite; drawn in the 0.75 buffer it goes soft.
0.160.75Skip the noise where nothing can reach. By : more than ahead of the
front return the frozen frame, and behind return the live one. Inside the
band, more than 62 px ahead (plus slack) or 125 px behind does the same. That is what pays
for full device resolution.
hx0.215 + 0.150.25 - 0.215Sweep from the side the incoming words sit. ; a chapter
whose text is on the right is burned in from the right. Going backward uses the target's side
too. A line that always runs left to right crosses its own new title last.
hx = dir>0 ? uv.x : 1-uv.xSeed every burn. picks 2.0 to 3.6 broad bays up the frame,
their depth (0.055 to 0.115) and lean (), so two burns are never the same silhouette.
seed = (chapter+1)*17.3(sv.x-0.5)*0.07绘制冻结的旧帧和实时的新场景,绝不能让一个场景淡入另一个场景。
转场开始时,将退场场景一次性快照到全分辨率 target 中(冻结时钟),并继续实时渲染入场场景。
交叉淡入,或者在仍会运动的旧场景上燃烧,会让同一块屏幕区域同时显示两个章节,
火焰便不再是负责换页的元素。
以像素而非场单位测量前沿。 。
仅靠 level set,会在噪声顺着扫掠方向倾斜的位置形成细如发丝的接缝,在逆着扫掠方向的位置形成粗接缝,
导致火焰宽度无缘无故地不均匀。
xc = (field - T)/grad * pxHeight / pxScale通过间隔数像素的有限差分求斜率,绝不要对烘焙纹理使用 。
烘焙噪声的斜率在每个 texel 内恒定;逐像素计算会让接缝沿整条线产生条纹。
以间隔数像素的方式在 x 和 y 方向采样宽范围场(),
并将梯度限制在 。
dFdxhStep = 4*uPxS/uPxH0.35/aspect .. 3/aspect余烬区应使用平滑的发光渐变,而不是色阶。 症状:看起来像不真实的海报。
用 heat 变量连续分级:前沿为黄色,然后是橙色,最后在清晰撕裂的唇边处转为深红色。
高温段会让黄色向后延伸更远。渐变常量(线性,位于 tone map 之前):
| 停止点 | rgb | 位置 |
|---|---|---|
| 深色(deep) | | 唇边,tau 0.05 |
| 橙色(orange) | | tau 0.42 |
| 金色(gold) | | tau 0.76 |
| 高热金色(hot gold) | | tau 高于 0.86 |
| 细线(thread) | | 前沿线 |
| 辉光(glow) | | 细线周围 2 到 5 px |
高热颜色始终保持金色,绝不能变成奶油色。
浅黄色在满热时会呈现奶油色,变暗后则呈卡其色。渐变中不能出现灰色或白色;
变暗的区段应沿渐变向低处移动,而不是仅仅降低亮度。
前沿是聚珠、收束的明亮细线,宽度为 0.7 到 5 px,不能是均匀笔画。
均匀笔画会像霓虹灯。宽度为 ,其中 为 0.5 到 1.5,
由沿边缘的噪声决定。仅添加紧凑辉光(),不要宽泛光晕。
(0.9 + 2.6*heat) * beadbeadexp(-|xc+0.5wc| / (2 + 2.8*heat))即使在扫掠停驻时,它也必须明显运动。 症状:“too stale/static”(太陈旧或静止)。
用沿边缘双向移动的脉冲驱动 heat(、
),在 处闪烁,以 穿过燃烧区向后滚动,
加入漂移的串珠(),以及前沿上舔舐的火舌(、,
最多占屏幕的 0.022 和 0.010)。静帧会隐藏所有这些效果,因此应在实时录制的片段中判断燃烧效果。
sA*0.018 - t*1.30sA*0.027 + t*1.70t*7-t*2.6-t*0.8t*1.1t*2.8对黑色炭层使用 alpha mask;它不是一块平板。
炭层宽 26 px(另加前沿的 12 px 余量),沿撕裂的远端边缘从实时场景上碎落;
随着老化,孔隙会在炭层中张开,透出下方内容。一条纯黑带会看起来像简单擦除。
覆盖率 = ,边缘通过 px 撕裂,
这比前沿自身的斜率更缓和,因此只会磨损起毛,不会切下漂浮的孤立碎片。
bed * pores8*noise(0.08) + 3*noise(0.25)绝不要让孔隙周围出现发光轮廓。
发光边缘连接后会沿炭层边缘形成一条画出来的轮廓线。孔隙只是洞。
炭层边缘取自宽范围前沿,而不是精细前沿。
二维噪声会切下漂浮在新帧上的孤立炭块。只使用沿边缘的噪声(),
并令 ,其中 最大为 12。
kbEdgeNLineedge = zoneW + fs + bedW*(0.35 + 1.10*n)fs以一次连续的加深处理来烧灼火焰前方的旧帧, 覆盖距离为 20 到 40 px,
从棕褐色 按 过渡到深棕色 ,
并使用噪声改变覆盖范围。分成三个独立色阶会画出复制火焰形状的轮廓。
0.80, 0.62, 0.42tk^20.20, 0.10, 0.045烘焙噪声;不要逐像素堆叠 fbm。
在 2880x1800 下,成本为:逐像素 fbm 每个 pass 需要 1.5 到 3 ms;
一张可平铺纹理加一张 hash 表,在保持相同效果的情况下只需 0.3 到 0.6 ms。
只烘焙一次:在 16 单元格周期上创建一张 1024^2 RGBA16F 纹理
(r 为 3 个八度的 fbm,g 为 3 个八度的 ridged,b 为 gradient,a 为 2 个八度的 ridged,
全部按 采样),并在 2048^2 R16F 表中创建整数 hash lattice;
按小数部分的 smoothstep 过滤,使其与 value noise 完全一致。
x*0.0625用 scissor 将实时场景裁剪到显露侧,并缩小渲染。
只在线条后方,并额外包含屏幕的 以呈现 bays 和 haze。
燃烧过程中按 比例绘制(composite 保持全尺寸,因为眼睛感知的是边缘);
转场停驻 150 ms 后,在 0.3 s 内交叉淡入全尺寸场景,期间同时绘制两者,
这样既看不到清晰度突变,也不会给运动中的帧增加成本。
文字应在 composite 中以全尺寸绘制在实时侧;画在 buffer 中会变模糊。
0.160.750.75跳过噪声影响不到的区域。
按 判断:前沿前方超过 时返回冻结帧,
后方超过 时返回实时场景。在 band 内部,前方超过 62 px(另加余量)
或后方超过 125 px 时同样如此。这正是全设备分辨率性能得以维持的原因。
hx0.215 + 0.150.25 - 0.215从入场文字所在的一侧扫掠。
;文字位于右侧的章节应从右侧烧入。
向后导航时也使用目标场景所在的一侧。若线条始终从左向右移动,它最后会越过自己的新标题。
hx = dir>0 ? uv.x : 1-uv.x为每次燃烧设置随机种子。
用于选取沿画面向上分布的 2.0 到 3.6 个宽阔 bays,
以及它们的深度(0.055 到 0.115)和倾斜(),
使两次燃烧永远不会具有相同轮廓。
seed = (chapter+1)*17.3(sv.x-0.5)*0.07Constants
常量
| item | value |
|---|---|
| band half width | |
| front, linear | |
| duration | 3.0 s default; 1.5 to 5 s all read |
| line noise scales | broad |
| ember zone width | 12 to 66 px, |
| char | bed 26 px, crust 3 to 6 px, sumi |
| heat shimmer | about 1.8 px of |
| bloom | bright pass threshold 1.15, half-res and quarter-res blurs, weights |
| tone | vignette |
| live scene | 0.75 scale in transit, 150 ms rest, 0.3 s cross-fade |
| budgets | |
| 项目 | 值 |
|---|---|
| band 半宽 | |
| 前沿,线性 | |
| 持续时间 | 默认 3.0 s;1.5 到 5 s 均可读 |
| 线噪声尺度 | 宽范围 |
| 余烬区宽度 | 12 到 66 px, |
| 炭化层 | bed 26 px,crust 3 到 6 px,sumi |
| 热浪扰动 | 约 1.8 px 的 |
| bloom | 亮部提取阈值 1.15,半分辨率和四分之一分辨率模糊,权重 |
| 色调 | 暗角 |
| 实时场景 | 过渡期间 0.75 缩放,停驻 150 ms,0.3 s 交叉淡入 |
| 性能预算 | |
Gotchas, symptom first
常见问题:先看症状
- The flame is a thin red line on black char, no ember zone. The baked noise and hash
samplers are unbound, so reads as the frame texture's value. Bind
lifeanduKbN.uKbH - Bare garbage or a hard cut behind the line. The live scene is only drawn inside the
scissor (); a narrower margin lets the composite read undrawn texels.
front + 0.215 + 0.16 - Stair-steps or 2x2 blocks along the char edge. A derivative taken past a skipped branch.
Compute the pixel change from the field's own scale ().
kbAA(v, w) - Islands of char floating on the new frame. The far edge was cut from fine noise; take it from the broad front.
- A milky band inside the ember edge. Lifting the blacks with a curve washed it white over paper; light the dark areas by a gain instead.
- The scene behind the line looks soft after the burn ends. The 0.75 buffer stayed; run the full-size cross-fade after the rest interval.
- Timings look slow. A hidden Browser pane keeps rendering and steals the GPU; measure
in a headless browser on the GPU flags () with the pane parked. Kibori's transitions held p50 8.3 ms (120 Hz cap) once the noise was baked and the scene scissored; this demo's burn measured p50 8.3 ms, p90 8.8 ms at 1440x900 the same way.
--use-angle=metal --enable-gpu - The grain and embers swim as the front passes. Keep them on the paper (in paper coordinates plus a per-burn offset), never in screen space; only the front moves.
g
- 火焰是在黑色炭层上的细红线,没有余烬区。 烘焙噪声和 hash 采样器未绑定,导致 读取到 frame texture 的值。绑定
life和uKbN。uKbH - 线条后方出现未绘制内容或硬切。 实时场景只在 scissor 内绘制();余量过窄会让 composite 读取到未绘制的 texel。
front + 0.215 + 0.16 - 炭层边缘出现阶梯或 2x2 色块。 导数取自被跳过分支之后的位置。应根据场自身的尺度()计算像素变化。
kbAA(v, w) - 新帧上漂浮着孤立炭块。 远端边缘使用了精细噪声切割;应从宽范围前沿取得边缘。
- 余烬边缘内部出现乳白色带。 抬黑曲线会在纸张上洗成白色;应使用 gain 提亮暗部。
- 燃烧结束后,线条后方的场景仍然模糊。 buffer 一直保留;应在停驻时间结束后执行全尺寸交叉淡入。
0.75 - 时序看起来太慢。 隐藏的 Browser 面板仍在渲染并抢占 GPU;应将面板停放,在无头浏览器中通过 GPU 标志()进行测量。采用相同方式,Kibori 的转场在烘焙噪声并裁剪场景后保持 p50 8.3 ms(120 Hz 上限);此 demo 的燃烧效果在 1440x900 下测得 p50 8.3 ms、p90 8.8 ms。
--use-angle=metal --enable-gpu - 前沿经过时,grain 和余烬会游动。 将它们固定在纸张坐标上(使用纸张坐标中的 ,并加上每次燃烧的偏移),绝不能使用屏幕坐标;只有前沿需要移动。
g
Accessibility and lifecycle
无障碍与生命周期
- : no burn. Render the designed still of each scene at a fixed time and cross-fade the frozen and live frames over 0.35 s (
prefers-reduced-motion: reduce). Controls stay live.smoothstep - Pause on and when the canvas leaves the viewport; reset the time base on resume so the first frame does not integrate the pause.
document.hidden - Clamp to 1/30 s, cap DPR at 2, size from a
dtand build nothing at a zero viewport.ResizeObserver - A press during a burn queues the target; the last one wins after the line leaves.
- Real buttons, arrow keys and 1 to 3 change chapters, and a live region names the arrival.
- :不燃烧。在固定时间渲染每个场景的设计静帧,并在 0.35 s 内使用
prefers-reduced-motion: reduce交叉淡化冻结帧和实时帧。控件保持可用。smoothstep - 在 以及 canvas 离开视口时暂停;恢复时重置时间基准,避免首帧积分暂停期间的时间。
document.hidden - 将 限制为 1/30 s,DPR 上限为 2,根据
dt设置尺寸,视口尺寸为零时不构建任何内容。ResizeObserver - 燃烧过程中按下按钮会排队目标场景;线条离开后,最后一次操作生效。
- 真实按钮、方向键以及 1 到 3 可切换章节,并由 live region 宣告到达的章节。
Cost
成本
Measured in the source page: per-pixel noise was the cost, not the flame. Baking it, and
returning the plain scene away from the line, is the lever. The 0.75 scale and the scissor
come second. Do not chase MSAA on the composite: the edge is drawn to the pixel by .
kbAA在源页面中测得:成本来自逐像素噪声,而不是火焰。烘焙噪声,并在线条影响范围之外
直接返回普通场景,是首要优化手段。其次是 缩放和 scissor。
不要在 composite 上追逐 MSAA:边缘由 逐像素绘制。
0.75kbAAProvenance
来源
Extracted from KIBORI, a WebGL timber-workshop page whose chapters burn into each other and
whose footer paper burn set the look. The GLSL (flame, char, pores, crust, scorch)
and the burn pass in are that page's own, ported, with the scenes replaced by
three shader-drawn rooms.
EDGEdemo.html提取自 KIBORI,这是一个 WebGL 木工工坊页面,其各章节会相互燃烧过渡,
页脚的纸张燃烧效果定义了整体视觉风格。 中的 GLSL
(火焰、炭层、孔隙、结壳、焦痕)和燃烧 pass 均移植自该页面,
并已将场景替换为三个由 shader 绘制的房间。
demo.htmlEDGE