✨Effect Lab
Colour着色

The one canvas setting that makes everything glow一个 canvas 设置,让所有东西都发光

Most of the "expensive looking" effects here are not expensive. They are circles with a radial gradient, drawn with additive blending. Understanding that one mode explains fire, auroras, kaleidoscopes, metaballs and neon text.这里大部分「看起来很贵」的效果其实都不贵。它们就是一圈带径向渐变的圆,用加色混合画出来。理解那一个模式,火焰、极光、万花筒、融合光球和霓虹字就全解释了。

Additive blending: light adds up, paint does not

By default Canvas 2D composites with source-over: whatever you draw replaces what was underneath, modulated by alpha. That is how paint works. Light does something else — two overlapping sources are brighter than either alone, and the sum is clamped at white.

That is one line:

Switch it on and everything changes. Overlapping glow circles stop looking like stacked discs and start looking like light. The centre of a cluster saturates to white because many small contributions add past 1.0, which is exactly what a real bright source does to a camera sensor.

The failure mode is equally immediate: draw twenty blobs with lighter and the whole centre goes flat white, losing all structure. Additive blending has no negative contribution, so it can only accumulate. If you need contrast back, you need some particles drawn darker — which means a second pass, or a different mode.

additive draw
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(255,120,40,0.08)';   // low alpha!
ctx.beginPath();
ctx.arc(x, y, r, 0, 7);
ctx.fill();

Alpha per draw must be low. With lighter, ten draws at 0.08 sum to roughly 0.57 — visible but not blown out. Ten draws at 0.5 are pure white.

Fire is a gradient plus a compositing mode

The flame looks like a simulation and is not. It is a number of radial gradients, each with a warm core and a transparent edge, drawn additively and drifting upward with a random horizontal jitter:

The trick that makes it read as flame rather than as a blur: the gradient stops. A linear fade from orange to transparent looks like a spotlight. A fade that goes white → yellow → orange → red → transparent at the edges is a temperature ramp, and the eye has strong priors about that ordering.

The randomness is doing the flicker. Without the horizontal jitter each frame, the shape is stable and reads as a static blob. With it, the edges boil — and boiling edges are what a fire has that a gradient does not.

Why not particles? The fire on this site is not particle-based, because thousands of upward-moving dots read as "sparks" rather than "flame". Flame is a continuous field with turbulent boundaries. Gradients with per-frame jitter give you the continuity; a particle spray does not.

one flame blob
function flame(x, y, h) {
  const g = ctx.createRadialGradient(x, y, 0, x, y, h);
  g.addColorStop(0,    'rgba(255,255,220,0.55)');   // core
  g.addColorStop(0.25, 'rgba(255,200,60,0.40)');
  g.addColorStop(0.55, 'rgba(255,110,20,0.22)');
  g.addColorStop(1,    'rgba(160,20,0,0)');          // edges
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(x + (Math.random() - 0.5) * 12, y, h, 0, 7);
  ctx.fill();
}

Aurora, holo and kaleido are the same blob at different scales

Once you have a soft additive blob, the effects are arrangement problems:

Aurora is three or four very large blobs (radius in the hundreds of pixels) moving on slow sine paths. The blobs are so large that you never see their edges — you only see the colour fields overlapping, which is why it reads as sky rather than as shapes.

Holo is the same blobs at a much lower alpha, moving faster, with the hue derived from position so the colour shifts continuously instead of being fixed per blob. That hue-from-position detail is what separates "ambient gradient" from "liquid colour".

Kaleidoscope is the only one that needs more than blending: draw everything into the centre, then mirror it. In Canvas that is a series of ctx.scale(-1, 1) transforms around the centre, redrawing the same geometry into each wedge:

The reason a kaleidoscope is convincing is that the symmetry is exact. Any per-particle randomness is mirrored too, so the whole image is consistent — which is why it must be drawn as a mirrored frame, not as particles that happen to be placed symmetrically.

mirrored wedges
// draw the scene once into the centre quadrant, then mirror it
for (let i = 0; i < 6; i++) {
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(i * Math.PI / 3);
  ctx.scale(i % 2 ? -1 : 1, 1);
  drawWedge();          // same geometry every time
  ctx.restore();
}

drawWedge must be deterministic within a frame — randomise positions outside the loop, or each wedge gets a different pattern and the symmetry breaks.

Metaballs: blur, then threshold

The merging blobs look like they need a distance field or marching squares. There is a far cheaper approximation: draw soft blobs, then apply a hard threshold on alpha so everything above the line becomes solid and everything below disappears.

In CSS, filter: blur(Npx) contrast(K) does both steps in one declaration. The blur softens the edges; the contrast pushes everything above the midpoint to fully opaque and everything below to transparent. Circles that were merely near each other become one merged shape, with a smoothly curved bridge — exactly the liquid look.

In Canvas the equivalent is two offscreen passes: draw blobs to buffer A, then read the pixels and write a hard-alpha version to buffer B. More code, more control, but the CSS version is one line and is composited on the GPU — which is why every metaball demo on the web uses it.

Note the tuning relationship: blur radius and contrast are not independent. Bigger blur means a wider bridge between blobs, and you need more contrast to keep the edge hard. Change one and you will have to re-tune the other.

metaballs in one filter
.metaball-stage { filter: blur(14px) contrast(28); background: #000; }
.metaball {
  position: absolute;
  width: 90px; height: 90px;
  border-radius: 50%;
  background: #7cf;
}

Both numbers matter: blur 14 with contrast 28 gives a firm edge and a visible bridge. Blur 30 with contrast 28 turns everything into one blob.

Neon text is stacked shadows, and the stack order is the design

The neon tube effect is text-shadow repeated at increasing radii. The reason it looks like a tube rather than a glow: the innermost shadow is tight and almost white, and each subsequent one is wider, dimmer and more saturated.

Three to five layers is the useful range. Two is not enough to read as a light source; six or more costs real paint time and the outer layers are invisible anyway. The animation then pulses the whole stack together, which is why it reads as breathing rather than flickering.

The related sine wave demo uses no blending at all and is worth mentioning here because it makes the same point differently: three lines, each a sine with a different amplitude, frequency and phase, drawn with a small shadowBlur. The visual richness comes from phase offset, not from shaders — the three curves are never in the same place at the same time, so the composition keeps changing without any of them being complicated.

layered neon
.neon {
  color: #fff;
  text-shadow:
    0 0 4px  #fff,
    0 0 10px #ff6b9d,
    0 0 22px #ff6b9d,
    0 0 44px #ff2d6f;
  animation: breathe 3s ease-in-out infinite;
}
@keyframes breathe {
  50% { text-shadow: 0 0 3px #fff, 0 0 7px #ff6b9d, 0 0 14px #ff6b9d, 0 0 26px #ff2d6f; }
}

Where to take this

Reach for lighter before you reach for a shader. A soft radial gradient drawn additively is the entire visual engine of this section — fire, aurora, holographic colour and neon are all that one primitive arranged differently. The only thing to watch is saturation: low alpha per draw, and accept that additive blending can never darken.

加色混合:光会相加,颜料不会

Canvas 2D 默认用 source-over 合成:画上去的东西替换掉下面的,按 alpha 调制。颜料的规则。光的规则是另一套——两个重叠的光源比任何一个都亮,而且总和会被截断在白。

就是一行:

打开它,一切都变了。重叠的光圈不再像叠在一起的圆盘,开始像光。一团圆的中心会饱和成白色,因为许多小贡献加起来超过了 1.0——真实世界的强光源照在相机传感器上就是这个结果。

失效方式也立刻出现:用 lighter 画二十个光团,整个中心糊成一片死白,结构全丢。加色混合没有负贡献,所以它只会累加。想把对比度找回来,就得让某些粒子画得更暗——那意味着第二趟绘制,或者换一种混合模式。

加色绘制
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(255,120,40,0.08)';   // alpha 必须低!
ctx.beginPath();
ctx.arc(x, y, r, 0, 7);
ctx.fill();

每次绘制的 alpha 必须低。用 lighter 时,十次 0.08 大约累加到 0.57——看得见但没烧穿;十次 0.5 就是纯白。

火焰是一个渐变加一个混合模式

火苗看着像模拟,其实不是。它是若干个径向渐变,每个有一个暖色核心和透明边缘,加色叠加,一边上升一边水平随机抖动:

让它读作火焰而不是一团模糊的关键,是渐变的色标。从橙色线性淡出到透明,看起来像聚光灯。而白 → 黄 → 橙 → 红 → 边缘透明的这条路,是一条温度曲线,眼睛对这个顺序有很强的先验。

随机性负责闪烁。去掉每帧的水平抖动,形状就固定了,读起来是一团静止的色块。加上它,边缘就开始「沸腾」——而沸腾的边缘正是火有、渐变没有的东西。

为什么不用粒子?本站的火不是粒子做的,因为几千个向上移动的点读起来是「火星」,不是「火焰」。火焰是一个边界不断扰动的连续场。带逐帧抖动的渐变给你连续性,粒子喷射给不了。

一团火苗
function flame(x, y, h) {
  const g = ctx.createRadialGradient(x, y, 0, x, y, h);
  g.addColorStop(0,    'rgba(255,255,220,0.55)');   // 核心
  g.addColorStop(0.25, 'rgba(255,200,60,0.40)');
  g.addColorStop(0.55, 'rgba(255,110,20,0.22)');
  g.addColorStop(1,    'rgba(160,20,0,0)');          // 边缘
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(x + (Math.random() - 0.5) * 12, y, h, 0, 7);
  ctx.fill();
}

极光、全息和万花筒,是同一个光团换了尺度

一旦你有了一个柔和的光团,剩下的都是排列问题:

极光是三四个非常大的光团(半径几百像素),沿缓慢的正弦轨迹移动。它们大到你看不见边缘——你只看到色场互相叠加,所以它读作天空,而不是几个圆。

全息是同样的光团,alpha 更低、动得更快,色相由位置推出来,于是颜色连续流动,而不是每个光团固定一个色。这个「色相来自位置」的细节,就是「环境渐变」和「液态色彩」的分界。

万花筒是唯一需要比混合更多东西的:把内容画到中心,然后镜像。在 Canvas 里就是绕中心做一串 ctx.scale(-1, 1),把同一份几何重画进每个扇区:

万花筒之所以能骗过人,是因为对称是精确的。任何逐粒子的随机性也会被一起镜像,所以整幅图是自洽的——这就是为什么它必须被画成「镜像的一帧」,而不是「碰巧摆得对称的粒子」。

镜像扇区
// 先把内容画进中心一个扇区,再镜像
for (let i = 0; i < 6; i++) {
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(i * Math.PI / 3);
  ctx.scale(i % 2 ? -1 : 1, 1);
  drawWedge();          // 每次都是同一份几何
  ctx.restore();
}

drawWedge 在单帧内必须是确定的——随机位置要放在循环外面算好,否则每个扇区图案都不一样,对称就破了。

融合光球:先模糊,再取阈值

融合的光球看起来需要距离场或者 marching squares。有个便宜得多的近似:画柔和的光团,然后对 alpha 取一个硬阈值,高于线的变成实心,低于线的消失。

在 CSS 里,filter: blur(Npx) contrast(K) 一句就把两步都做了。模糊软化边缘,对比度把中点以上的推到完全不透明、以下的推到完全透明。仅仅靠得近的两个圆变成一个融合的形状,中间有一条平滑弯曲的桥——正是那种液体的感觉。

Canvas 里的等价做法是两趟离屏:先把光团画到缓冲 A,再读像素、把硬 alpha 版本写到缓冲 B。代码更多、控制更细,但 CSS 版本只有一行,而且走 GPU 合成——所以网上所有 metaball 示例都用它。

注意这两个参数不是独立的:模糊半径越大,两个球之间的桥越宽,你就需要更高的对比度才能保住硬边。改一个,另一个必须重调。

一句 filter 做融合光球
.metaball-stage { filter: blur(14px) contrast(28); background: #000; }
.metaball {
  position: absolute;
  width: 90px; height: 90px;
  border-radius: 50%;
  background: #7cf;
}

两个数都要紧:模糊 14 配对比度 28,边缘硬、桥看得见;模糊 30 配对比度 28,全糊成一团。

霓虹字是叠起来的 shadow,而叠的顺序就是设计

霓虹灯管是 text-shadow 在递增半径上重复几次。它之所以像灯管而不像光晕:最里面那层很紧、几乎是白色,之后每一层更宽、更暗、更饱和。

三到五层是有用的区间。两层读不出「光源」;六层以上真的会增加绘制开销,而外面几层反正看不见。动画让整叠一起脉动,所以它读作呼吸,而不是闪烁。

隔壁的正弦波完全不用混合,但值得在这里提一句,因为它用另一种方式说明同一件事:三条线,各自是不同振幅、频率、相位的正弦,配一点 shadowBlur。视觉上的丰富来自相位差,不是来自着色器——三条曲线从不同时出现在同一个位置,所以构图一直在变,而其中任何一条都不复杂。

叠层霓虹
.neon {
  color: #fff;
  text-shadow:
    0 0 4px  #fff,
    0 0 10px #ff6b9d,
    0 0 22px #ff6b9d,
    0 0 44px #ff2d6f;
  animation: breathe 3s ease-in-out infinite;
}
@keyframes breathe {
  50% { text-shadow: 0 0 3px #fff, 0 0 7px #ff6b9d, 0 0 14px #ff6b9d, 0 0 26px #ff2d6f; }
}

接下来可以怎么用

在想上着色器之前,先想 lighter。一个柔和的径向渐变用加色画出来,就是这一整节的视觉引擎——火焰、极光、全息色彩、霓虹,全是同一个原语的不同排法。唯一要盯的是饱和度:每次绘制的 alpha 要低,并接受加色混合永远无法变暗。

Related demos相关示例

Each one runs on its own page, with the source shown below the stage.每个都在独立页面实时运行,舞台下方附完整源码。

Every effect on this site is a single self-contained file. Copy it, change one number, see what happens.本站每个特效都是单个自包含文件。复制走,改一个数字,看会发生什么。

Browse all 131 effects浏览全部 131 个特效