canvas 上的 11 个混合模式: 一套照片审美的技术骨架

2026-05-22 · 约 10 分钟 · 标签: PixMojo , canvas , 技术 , blend mode

canvas 上的 11 个混合模式

PixMojo 有 30 多个 sub-look(Wong Kar-Wai 风格、Golden Hour、Neon Tokyo、Loro Piana 风格 等等)。看起来是 30 个独立算法。实际不是。

底层只有 11 个 canvas 原语,每个 sub-look 是这 11 个原语的不同组合和参数。像做菜 — 30 道菜可能用同样 5 个基础调料,只是配比不同。

这篇讲清楚这 11 个是什么,以及它们组合成一套照片审美的思考路径。

底层 API:getImageData / putImageData

所有 pixel-level 操作的起点都是这个 API。canvas 2D context 的 getImageData(0, 0, w, h) 返回一个 Uint8ClampedArray,长度是 w × h × 4(RGBA)。你直接读写这个数组,然后 putImageData 写回去。

原生速度上,一张 4K 图 (3840 × 2160) 大约 33 million pixels × 4 bytes = 130MB 的数组。遍历一次在 iPhone 12 上 30-50ms。这个数字决定了所有 pixel-level 操作的性能预算。

我把常用的模式抽象成一个 pixelOp(ctx, w, h, op) 函数:

function pixelOp(
  ctx: CanvasRenderingContext2D,
  w: number,
  h: number,
  op: (r: number, g: number, b: number) => [number, number, number]
) {
  const img = ctx.getImageData(0, 0, w, h)
  const px = img.data
  for (let i = 0; i < px.length; i += 4) {
    const [r, g, b] = op(px[i], px[i + 1], px[i + 2])
    px[i] = Math.max(0, Math.min(255, r))
    px[i + 1] = Math.max(0, Math.min(255, g))
    px[i + 2] = Math.max(0, Math.min(255, b))
  }
  ctx.putImageData(img, 0, 0)
}

有了它,几乎所有色彩调整都是 6-8 行代码。

11 个原语

1. Cast color (color cast) — multiply/overlay blend

ctx.globalCompositeOperation = "multiply" 加一个纯色矩形。用于整体色调偏移:暖化 (加暖橙 multiply)、冷化 (加青蓝 multiply)、复古 (加暖黄 multiply)。

2. Vignette (暗角)

createRadialGradient 从画面中心到外圈的黑色渐变。用于中心聚焦:告诉眼睛”看中间”。

3. Letterbox (电影黑边)

2.35:1 上下加黑条。用于电影感:这一个动作就把一张手机照片变成”电影截图”。

4. Shadow crush (暗部下压)

pixel-level 操作,检测 luminance < 阈值的像素,进一步压暗。用于电影级对比:让暗部真的黑,不是灰。

5. Highlight lift (高光提升)

反过来,luminance > 阈值的像素往白色 push。用于Apple 广告风格:明亮、通透、洁净。

6. Desaturate/saturate

pixel-level:gray + (color - gray) * factor。factor < 1 降饱和,> 1 增饱和。大部分好看的照片都是稍微降饱和的,因为 iPhone 拍出来饱和度已经过头。

7. Contrast

pixel-level:(color - 128) * factor + 128。factor > 1 增对比,< 1 减对比。审美意义:增对比 = 戏剧感,减对比 = 柔和梦幻。

8. Bloom (辉光)

createRadialGradient 白色高斯,screen blend。用于梦幻感 / Soft cinema 感:Greta Gerwig 的电影里到处是这个东西。

9. Grain (胶片颗粒)

生成一张 noise 小图然后 overlay blend 上去。用于胶片质感:数码照片过于干净,加一点噪点反而看起来”真实”。

10. Lens flare (镜头光晕)

createRadialGradient + 旋转 triangular gradients 做 streaks。用于Sun Flare 效果:夏天海边、Golden Hour 都靠它。

11. Two-tone lighting (双色照明)

两个不同色的 createRadialGradient 从不同角落 screen 上去。用于 Neon Tokyo 风:粉色右上 + 蓝色左下,赛博朋克瞬间成立。

组合就是审美

有了这 11 个原语,30 个 look 是这样组合的:

  • Wong Kar-Wai 风 = Cast color (暖红) + Cast color (magenta overlay) + Contrast + Vignette + Letterbox
  • Golden Hour = Cast color (暖橙) + Highlight lift + Bloom + Vignette
  • Neon Tokyo = Two-tone lighting (粉+蓝) + Shadow crush + Contrast
  • Loro Piana 风 = Cast color (奶油) + Shadow crush (轻) + Desaturate + Grain (细)
  • Apple 广告风 = Highlight lift + Desaturate (轻) + Contrast (低)

每个 look 就是 4-5 个原语 + 参数组合。整个 canvas-mojo-extras.ts 一共 700 行左右,覆盖 30 个 look。

为什么这样做而不是 preset library

传统 photo app 的做法是”每个 look 一个独立算法 / 一套 LUT”。我最初也是这么想的,但两个理由让我改了:

第一,可组合性。原语化之后,用户可以叠加:Golden Hour + Sun Flare = 夏日海报感。Wong Kar-Wai + Grain = 更胶片。传统 LUT 无法组合。

第二,可调性。每个原语都有参数。之后要加 Pro tier,直接暴露 warmth / bloom / vignette 强度滑块就行,不需要重新做 UI。

第三,可移植性。这 11 个原语在 GLSL 里也就是各 5-10 行 shader。未来要迁 WebGL 或 Metal / OpenGL ES(做 mobile 时),JS 逻辑直接 map。保持了 abstraction 的迁移路径。

到底难在哪

技术上说,写 11 个原语不难。真正难的是参数 tuning。

每个 look 用 4-6 个原语组合,每个原语 3-5 个参数。一个 look 的 preset 大概 15-25 个数字。这些数字不能算出来,只能眼睛调 — 拿真实照片测,一点点改,让”看起来像那个 vibe”。

比如”Wong Kar-Wai In the Mood for Love” preset:

{ warmth: 0.38, magenta: 0.22, contrast: 1.18, vignette: 0.32, letterbox: true }

这 5 个数字调了大概两天。改到最后一个数字改动 0.02 就明显了。这个过程不是编程,是设计。

这也是我说 taste 是护城河的原因。11 个原语随便谁都能抄,但 30 个 look × 每 look 20 个参数 = 600 个手调数字。这套数字组成的 taste 无法在几天内复制。


写在最后:这篇讲的是技术骨架。审美骨架下一篇会写 — 为什么每个 Mojo Pack 是这五个而不是别的。那是产品设计问题,比技术问题更难。

如果好奇 canvas 实际效果,pixmojo.app 可以试。所有渲染都在你浏览器里,照片不上传。

评论
评论功能尚未配置:在 GitHub 仓库开启 Discussions,并按项目 README 里的步骤填好 src/consts.ts 即可启用(基于 Giscus,评论需要 GitHub 账号)。