样式-径向渐变

4 阅读3分钟

一、径向渐变的核心原理

1. 基本定义

径向渐变通过两个圆(起始圆和结束圆)定义渐变范围:

  • 起始圆:圆心 (x0, y0),半径 r0(通常较小,甚至为0)
  • 结束圆:圆心 (x1, y1),半径 r1(通常较大)
  • 颜色过渡发生在两个圆之间的所有中间圆上,而非简单的线性投影。

2. 颜色计算的数学本质

这个公式是**径向渐变(Radial Gradient)的核心数学定义。它的由来基于“中间圆族”**的几何插值原理。

2.1. 核心思想:圆与圆的插值

径向渐变由两个圆定义:

  • 起始圆(Circle 0):圆心 (x0,y0)(x_0, y_0),半径 r0r_0
  • 结束圆(Circle 1):圆心 (x1,y1)(x_1, y_1),半径 r1r_1

当渐变参数 tt 从 0 变化到 1 时,图形系统会在两个圆之间生成无数个**“中间圆”。对于任意一个 tt 值(0t10 \le t \le 1),这个中间圆的圆心和半径是通过线性插值**计算出来的:

  • 中间圆的圆心 (Cx,Cy)(C_x, C_y)Cx=(1t)x0+tx1C_x = (1-t)x_0 + t x_1 Cy=(1t)y0+ty1C_y = (1-t)y_0 + t y_1 (几何意义:圆心从起点圆心沿直线移动到终点圆心)

  • 中间圆的半径 RRR=(1t)r0+tr1R = (1-t)r_0 + t r_1 (几何意义:半径从起始半径线性变化到终点半径)

公式

Cx=(1t)x0+tx1C_x = (1-t)x_0 + t x_1

确实可以直接从向量方程推导而来

这种写法在数学上被称为仿射组合(Affine Combination)线性插值(Linear Interpolation / Lerp),它是向量加法与数乘运算的直接结果。

1. 向量推导过程

假设我们有两个点(或向量):

  • 起点 P0P_0,坐标为 (x0,y0)(x_0, y_0)
  • 终点 P1P_1,坐标为 (x1,y1)(x_1, y_1)

我们要找这两点连线上的任意一点 MM,其位置由参数 tt 控制。

步骤 1:定义方向向量P0P_0 指向 P1P_1 的向量 d\vec{d} 为: d=P1P0=(x1x0,y1y0)\vec{d} = P_1 - P_0 = (x_1 - x_0, y_1 - y_0)

步骤 2:向量位移方程MM 可以看作是从起点 P0P_0 出发,沿着方向 d\vec{d} 移动了一段距离。这段距离是总长度的 tt 倍(其中 0t10 \le t \le 1)。 向量方程为: M=P0+tdM = P_0 + t \cdot \vec{d} 或者写作: OM=OP0+t(OP1OP0)\vec{OM} = \vec{OP_0} + t (\vec{OP_1} - \vec{OP_0}) (这里 OO 是原点,OP\vec{OP} 表示位置向量)

步骤 3:展开并合并同类项d\vec{d} 代入方程: M=P0+t(P1P0)M = P_0 + t(P_1 - P_0) M=P0+tP1tP0M = P_0 + t P_1 - t P_0

提取公因式 P0P_0M=(1t)P0+tP1M = (1 - t)P_0 + t P_1

步骤 4:分解到 X 轴 如果我们只看 X 分量: Cx=(1t)x0+tx1C_x = (1 - t)x_0 + t x_1

同理,Y 分量为: Cy=(1t)y0+ty1C_y = (1 - t)y_0 + t y_1


2. 为什么系数是 (1t)(1-t)tt

这体现了**权重(Weight)**的概念:

  • t=0t=0Cx=1x0+0x1=x0C_x = 1 \cdot x_0 + 0 \cdot x_1 = x_0 起点的权重是 100%,终点的权重是 0%。结果是起点。

  • t=1t=1Cx=0x0+1x1=x1C_x = 0 \cdot x_0 + 1 \cdot x_1 = x_1 起点的权重是 0%,终点的权重是 100%。结果是终点。

  • t=0.5t=0.5Cx=0.5x0+0.5x1C_x = 0.5 x_0 + 0.5 x_1 两者权重各占 50%。结果是中点。

关键性质:两个系数之和恒为 1。 (1t)+t=1(1-t) + t = 1 这在几何上保证了结果点 MM 始终落在 P0P_0P1P_1 的连线上(如果是凸组合,则在线段上)。

3. 总结

你看到的 Cx=(1t)x0+tx1C_x = (1-t)x_0 + t x_1 就是向量方程 M=P0+t(P1P0)\vec{M} = \vec{P_0} + t(\vec{P_1} - \vec{P_0})代数展开形式

  • 向量形式更直观地表达了“从起点沿方向移动”的物理意义。
  • 标量形式(即你问的公式)更便于计算机直接进行坐标计算,因为它把 X 和 Y 解耦了,且不需要显式构建向量对象。

所以在径向渐变中,圆心 (Cx,Cy)(C_x, C_y) 的运动轨迹,本质上就是一个匀速直线运动的向量插值过程

2.2. 圆的标准方程

在平面几何中,一个圆心为 (h,k)(h, k),半径为 rr 的圆,其标准方程为: (xh)2+(yk)2=r2(x - h)^2 + (y - k)^2 = r^2

2.3. 公式的组装

为了找到点 P(x,y)P(x, y) 对应的颜色,我们需要知道该点落在哪个 tt 值的中间圆上

我们将第1步中推导出的中间圆圆心中间圆半径代入第2步的圆方程中:

  1. 代入圆心(x[(1t)x0+tx1])2+(y[(1t)y0+ty1])2=(x - [(1-t)x_0 + t x_1])^2 + (y - [(1-t)y_0 + t y_1])^2 = \dots

  2. 代入半径=[(1t)r0+tr1]2\dots = [(1-t)r_0 + t r_1]^2

合并后,就得到了你看到的公式: (x[(1t)x0+tx1])2+(y[(1t)y0+ty1])2=[(1t)r0+tr1]2(x - [(1-t)x_0 + t x_1])^2 + (y - [(1-t)y_0 + t y_1])^2 = [(1-t)r_0 + t r_1]^2

2.4. 这个公式意味着什么?

这个方程建立了一个关于 tt二次方程关系。

  • 左边:表示点 PP 到“当前时刻 tt 的圆心”的距离的平方。
  • 右边:表示“当前时刻 tt 的圆”的半径的平方。

求解过程: 当渲染引擎处理像素 P(x,y)P(x,y) 时,它会解这个方程求出 tt

  • 如果解出的 t=0.5t=0.5,说明点 PP 刚好落在渐变过程 50% 位置的那个圆环上,因此取颜色渐变条 50% 处的颜色。
  • 如果方程无解(例如点 PP 在渐变范围之外),则根据 spreadMethod(如 pad, reflect, repeat)来决定颜色。

2.5 总结

这个公式的来源就是**“圆的定义”** + “线性插值”。它描述了径向渐变本质上是一个圆心在移动、半径在缩放的动态圆族覆盖过程。

简单来说,径向渐变并不是简单的颜色混合,而是形状的过渡。系统通过计算一系列“中间圆”,来确定任意像素点 P(x,y)P(x, y) 处于渐变的哪个阶段(即 tt 值)。

对于任意点 P(x, y),其颜色由参数 t 决定,t 需满足以下条件:
P 位于由起始圆到结束圆线性插值得到的中间圆上,即:

(x - [(1-t)·x0 + t·x1])² + (y - [(1-t)·y0 + t·y1])² = [(1-t)·r0 + t·r1]²

这是一个关于 t二次方程,解出 t 后才能确定颜色位置。
关键点

  • 当两圆同心x0=x1, y0=y1)时,公式简化为 t = (d - r0)/(r1 - r0)d 为点P到圆心的距离)。
  • 当两圆不同心时,必须解二次方程,无法简化为线性投影计算

二、用户结论的错误分析

以下依据 Canvas 2D API 规范(W3C 标准)及主流浏览器实现进行纠正。

❌ 错误的核心公式分析

原公式:

proj = (点P相对向量) · (归一化轴向)
t = (proj - r1) / (r2 - r1)

错误根源

  1. 物理模型错误:径向渐变的颜色由“位于两个圆之间的同心圆族”决定,而非“垂直于渐变轴的一条直线”。
  2. 数学计算错误:当两圆不同心时,t 是二次方程的解,无法通过一次线性投影计算得出

✅ 径向渐变的正确原理与公式

径向渐变由两个圆定义:

  • 起点圆:C0 = (x0, y0),半径 r0
  • 终点圆:C1 = (x1, y1),半径 r1

对于画布上的任意一点 P(x, y),其对应的渐变插值参数 t 需满足: 点 P 必须位于以 (1-t)C0 + t·C1 为圆心、半径为 (1-t)r0 + t·r1 的圆周上

代入方程得:

[ x - (1-t)x0 - t·x1 ]² + [ y - (1-t)y0 - t·y1 ]² = [ (1-t)r0 + t·r1 ]²

展开后得到标准二次方程:

A·t² + B·t + C = 0

其中:

  • dx = x1 - x0, dy = y1 - y0, dr = r1 - r0
  • A = dx² + dy² - dr²
  • B = 2·[ (x - x0)·(-dx) + (y - y0)·(-dy) + r0·dr ]
  • C = (x - x0)² + (y - y0)² - r0²

t 的有效取值

  • 解二次方程,取满足 0 ≤ t ≤ 1较大实根(规范要求:当点位于重叠区域时,取外侧圆对应的 t)。
  • t < 0 → 使用 0% 颜色。
  • t > 1 → 使用 100% 颜色。

⚡ 关键区别对照表

特征原错误结论(线性逻辑)实际径向渐变逻辑
两圆同心碰巧正确(t = (dist - r0)/(r1 - r0)公式退化为线性比例,特例
两圆偏心错误(投影值产生非线性误差)必须解二次方程,结果与投影值无直接关系
相交情况无法处理双解情况通过取最大实根正确选择外侧圆颜色
内含情况可能导致 t 超出 [0,1] 时颜色映射错误按规范处理扩展模式

🔍 典型反例验证

设:

  • 起点圆:(0, 0, r0=10)
  • 终点圆:(100, 0, r1=50)
  • 待测点 P:(50, 0) (位于圆心连线上)

错误投影法proj = 50t = (50 - 10)/(50 - 10) = 1.0 → 显示终点颜色。

实际二次方程计算: 代入方程 (50 - 100t)² = (10 + 40t)² → 解得 t ≈ 0.36t ≈ -1.14(舍去负值)。 → 实际显示过渡色,而非终点色。误差极大。

📝 总结纠正

  1. 颜色分布原理:由两个圆定义的圆锥曲面族决定,必须解二次方程
  2. 与位置关系强烈相关。同心是唯一可简化为线性距离计算的特例;偏心、相交、相离均需完整解方程。
  3. 开发实践:浏览器底层图形库(Skia、CoreGraphics、Direct2D)均已实现正确的二次方程求解逻辑。开发者无需手写公式,但理解原理有助于处理复杂阴影、光照模拟等效果。

三、径向渐变的正确计算逻辑

1. 标准流程

  1. 定义中间圆族
    对每个 t ∈ ,中间圆的圆心为 ( (1-t)·x0 + t·x1, (1-t)·y0 + t·y1 ),半径为 (1-t)·r0 + t·r1
  2. 求解点P对应的 t
    代入点 P(x,y) 到中间圆方程,解二次方程:
    A·t² + B·t + C = 0
    
    其中:
    • A = (x1-x0)² + (y1-y0)² - (r1-r0)²
    • B = 2[(x-x0)(x0-x1) + (y-y0)(y0-y1) + r0(r1-r0)]
    • C = (x-x0)² + (y-y0)² - r0²
  3. 确定有效 t
    • 若方程有解,取 满足 t ∈ 的最大实根(因渐变从内向外扩散)。
    • 若无解,则点P在渐变区域外,按 spreadMethod 规则处理(默认取最近边界色)。

2. 颜色映射规则

  • t ≤ 0 → 显示 colorStop(0) 的颜色(起始圆内侧)。
  • 0 < t < 1 → 在相邻色标间线性插值颜色。
  • t ≥ 1 → 显示 colorStop(1) 的颜色(结束圆外侧)。

这个公式本质上是在描述**“点与圆的几何位置关系”**。

它之所以能用来计算颜色,是因为它将**“寻找颜色”的问题转化为了“求解方程”**的数学问题。

以下是该公式的来源推导,以及它与“方程有解”和“颜色映射”之间的逻辑关联:

1. 公式是怎么来的?(几何插值原理)

径向渐变的本质是两个圆之间的形状过渡

  • 起始圆(Circle 0):圆心 (x0,y0)(x_0, y_0),半径 r0r_0
  • 结束圆(Circle 1):圆心 (x1,y1)(x_1, y_1),半径 r1r_1

当渐变从 0 进行到 1 时,中间会生成无数个**“过渡圆”**。对于任意一个进度 tt0t10 \le t \le 1),这个过渡圆的属性是两个圆的线性插值:

  • 过渡圆的圆心(1t)x0+tx1(1-t)x_0 + t x_1
  • 过渡圆的半径(1t)r0+tr1(1-t)r_0 + t r_1

我们知道,圆的标准方程是 (xa)2+(yb)2=R2(x-a)^2 + (y-b)^2 = R^2(其中 (a,b)(a,b) 是圆心,RR 是半径)。 将上面的过渡圆属性代入标准方程,就得到了你看到的公式:

(x[(1t)x0+tx1]过渡圆圆心)2+(y[(1t)y0+ty1]过渡圆圆心)2=([(1t)r0+tr1]过渡圆半径)2(x - \underbrace{[(1-t)x_0 + t x_1]}_{\text{过渡圆圆心}})^2 + (y - \underbrace{[(1-t)y_0 + t y_1]}_{\text{过渡圆圆心}})^2 = (\underbrace{[(1-t)r_0 + t r_1]}_{\text{过渡圆半径}})^2

一句话总结来源: 这个公式就是**“时刻 tt 的那个圆的方程”**。


2. 和“方程有解”是怎么关联的?

在渲染图像时,屏幕上的每一个像素点 P(x,y)P(x,y) 都是已知的,而 tt 是未知的。我们需要知道这个点到底落在哪个过渡圆上。

我们将公式展开,它实际上是一个关于 tt一元二次方程At2+Bt+C=0A \cdot t^2 + B \cdot t + C = 0

这里的“方程有解”对应着以下物理意义:

情况 A:方程有解(tt 存在)

这意味着点 P(x,y)P(x,y) 确实位于从起始圆到结束圆的过渡轨迹上。

  • 我们解出 tt(通常取 0t10 \le t \le 1 之间的实数解)。
  • 这个 tt 值直接告诉我们要取渐变色条上的哪个位置的颜色。
    • 例如:解出 t=0.5t=0.5,说明该点位于渐变的正中间,颜色就是渐变色的 50% 处。
情况 B:方程无解(或无实数解)

这意味着点 P(x,y)P(x,y) 不在任何过渡圆上。这通常发生在渐变范围之外。

  • 例如:如果两个圆是同心圆,且 r1>r0r_1 > r_0,那么在 r1r_1 之外的区域,或者 r0r_0 之内的区域(取决于具体定义),可能无法通过 0t10 \le t \le 1 的插值圆覆盖。
  • 这时就需要用到你提到的颜色映射规则(即 spreadMethod):
    • Pad(填充):无解时,取最近的边界颜色(t=0t=0t=1t=1 的颜色)。
    • Reflect(反射):无解时,模拟镜像延伸。
    • Repeat(重复):无解时,循环渐变。

3. 关联总结表
步骤数学表达物理/视觉含义
1. 定义(xxt)2+(yyt)2=rt2(x - x_t)^2 + (y - y_t)^2 = r_t^2定义了一个随时间 tt 变化的圆环。
2. 求解把像素坐标 (x,y)(x,y) 代入,解出 tt询问系统:“这个像素点是在渐变过程的哪一刻出现的?”
3. 判定有解 (0t10 \le t \le 1)在渐变内。该点的颜色 = 渐变色条在 tt 处的颜色。
4. 映射无解 (或 tt 超出范围)在渐变外。根据规则(Pad/Reflect/Repeat)决定颜色。

结论: 这个公式是判定器。它通过计算点 PP 是否满足某个 tt 时刻的圆方程,来反推该点应该显示什么颜色。方程的解 tt,就是颜色查找的索引值


四、与线性渐变的本质区别

特性线性渐变径向渐变
定义方式起点 (x0,y0) 和终点 (x1,y1)起始圆 (x0,y0,r0) 和结束圆 (x1,y1,r1)
计算基础点在方向向量上的线性投影点与双圆的几何关系(二次方程)
位置关系影响仅由方向向量决定,与位置无关直接依赖两圆圆心/半径关系
典型场景水平/垂直/对角渐变光晕、球体、焦点效果

总结

径向渐变的颜色分布必须通过双圆几何关系解二次方程确定,其计算逻辑高度依赖两圆的圆心位置和半径,无法简化为线性投影。用户提出的公式仅适用于线性渐变,不适用于径向渐变。实际开发中,浏览器会自动处理复杂的数学计算,开发者只需通过 createRadialGradient(x0,y0,r0,x1,y1,r1) 定义两个圆即可。