水印算法
一、引论
texts.forEach((text, index) => {
const yOffset = (index - (texts.length - 1) / 2) * lineHeight;
ctx.fillText(text, i, j + yOffset);
});
这段代码通常出现在使用 HTML5 Canvas (ctx 即 CanvasRenderingContext2D) 进行绘图的开发场景中。它的核心目的是:将一组文本字符串(texts 数组)以当前坐标 (i, j) 为中心,垂直居中地绘制在画布上。
下面我将逐行拆解并详细解释其逻辑:
1. 代码整体结构
texts.forEach((text, index) => {
// ... 计算逻辑 ...
ctx.fillText(text, i, j + yOffset);
});
texts.forEach(...): 遍历一个包含多个字符串的数组texts。(text, index): 回调函数的参数,text是当前要绘制的字符串,index是它在数组中的索引(从 0 开始)。ctx.fillText(...): Canvas API,用于在指定坐标填充绘制文本。
2. 核心算法解析:yOffset 的计算
这是这段代码最精妙的部分:
const yOffset = (index - (texts.length - 1) / 2) * lineHeight;
目的
为了让多行文本的整体垂直中心点对齐到基准坐标 j 上。如果不做这个偏移,默认情况下文本会从 j 点开始向下排列,导致整体视觉重心偏下。
数学推导
假设 lineHeight (行高) 为 H,数组长度 N = texts.length。
-
(texts.length - 1) / 2:
这是为了找到数组索引的中间值。- 如果
N=1(索引0): 中间值 = 0。 - 如果
N=3(索引0, 1, 2): 中间值 = 1 (即索引1是中心)。 - 如果
N=4(索引0, 1, 2, 3): 中间值 = 1.5 (中心在索引1和2之间)。
- 如果
-
index - ...:
计算当前行相对于“中心行”的相对行数。- 中心行的结果为
0。 - 中心行上方的行结果为负数(例如 -1, -2)。
- 中心行下方的行结果为正数(例如 1, 2)。
- 中心行的结果为
-
* lineHeight:
将“相对行数”转换为实际的“像素距离”。
举例说明
假设 texts = ["第一行", "第二行", "第三行"],lineHeight = 20,基准点 j = 100。
数组长度 N = 3,中心索引值 = (3-1)/2 = 1。
-
当 index = 0 ("第一行"):
yOffset = (0 - 1) * 20 = -20
绘制 Y 坐标 =100 + (-20) = 80(在中心点上方) -
当 index = 1 ("第二行", 中间行):
yOffset = (1 - 1) * 20 = 0
绘制 Y 坐标 =100 + 0 = 100(正好在中心点) -
当 index = 2 ("第三行"):
yOffset = (2 - 1) * 20 = 20
绘制 Y 坐标 =100 + 20 = 120(在中心点下方)
结果:这三行文本将以 y=100 为中心上下对称分布。
3. 绘制执行
ctx.fillText(text, i, j + yOffset);
text: 当前循环到的字符串内容。i: X 轴坐标。注意这里所有行的 X 坐标都是相同的i,意味着这些文本是左对齐(取决于textAlign设置,默认通常是左对齐)且垂直堆叠的。j + yOffset: Y 轴坐标。j是设定的垂直中心基准线。yOffset是根据上述公式计算出的偏移量,确保文本块整体居中。
4. 潜在注意事项 (开发细节)
虽然这段代码逻辑很清晰,但在实际 Canvas 开发中,要达到完美的视觉居中,还需要考虑以下两点:
-
ctx.textBaseline的设置:
Canvas 的fillText绘制位置取决于textBaseline属性。- 如果
textBaseline是默认的'alphabetic',那么j点对齐的是文字的基线(如字母 'x' 的底部),而不是文字的几何中心。这可能导致视觉上略微偏上或偏下。 - 建议: 为了实现严格的几何居中,通常在运行此代码前设置:
如果是ctx.textBaseline = 'middle'; // 或者 'ideographic' / 'hanging' 视具体字体和需求而定,但 'middle' 最常用于此类计算'middle',那么j点就会精确对齐每一行文字高度的中点,配合上面的yOffset算法,整体效果会非常完美。
- 如果
-
lineHeight的定义:
Canvas 原生 API 没有 直接支持lineHeight属性。这里的lineHeight必须是开发者手动定义的一个数字变量(例如20或fontSize * 1.2)。如果这个值设置得小于字体的实际高度,文字会发生重叠。
总结
这段代码是一个标准的多行文本垂直居中绘制算法。它通过数学计算抵消了索引带来的自然向下偏移,使得无论数组里有几行文字,整个文本块的垂直中心都会牢牢固定在坐标 (i, j) 上。
二、推导
这个算法的推导过程其实是一个经典的“将索引映射到对称区间”的数学问题。
我们的目标是:让一组按顺序排列的元素(索引 \(0, 1, 2, ..., N-1\)),在视觉上围绕中心点 \(0\) 对称分布。
以下是详细的推导步骤:
1. 明确目标
假设我们有 \(N\) 行文本,行高为 \(H\) (lineHeight)。
我们希望:
- 中间那行的偏移量
yOffset为 0。 - 中间行上面一行的偏移量为 \(-H\)。
- 中间行下面一行的偏移量为 \(+H\)。
- 以此类推,形成等差数列:\(..., -2H, -H, 0, H, 2H, ...\)
2. 寻找“中心索引”
首先,我们需要知道这组数据的“中心”在哪个索引位置。
-
情况 A:奇数个元素 (\(N=3\))
索引:\(0, 1, 2\)
显然,索引 1 是中心。
计算公式:\((3 - 1) / 2 = 1\)。 -
情况 B:偶数个元素 (\(N=4\))
索引:\(0, 1, 2, 3\)
没有单个整数索引是中心,中心位于索引 1 和 2 之间。
数学上的中心点是 1.5。
计算公式:\((4 - 1) / 2 = 1.5\)。
结论:无论 \(N\) 是奇数还是偶数,中心点的索引值都可以统一表示为:
3. 计算相对位移(行数)
既然知道了中心点的索引是 \(\frac{N - 1}{2}\),那么对于任意一个当前索引 index,它距离中心有多少“行”?
只需做减法:
让我们验证一下这个公式:
-
如果 \(N=3\) (中心是 1):
- index=0: \(0 - 1 = -1\) (中心上方 1 行) ✅
- index=1: \(1 - 1 = 0\) (中心) ✅
- index=2: \(2 - 1 = +1\) (中心下方 1 行) ✅
-
如果 \(N=4\) (中心是 1.5):
- index=0: \(0 - 1.5 = -1.5\)
- index=1: \(1 - 1.5 = -0.5\)
- index=2: \(2 - 1.5 = +0.5\)
- index=3: \(3 - 1.5 = +1.5\)
注:偶数行时,没有哪一行正好在 0 点,而是两行平分中心点(-0.5 和 +0.5),这在视觉上是完美的居中对齐。
4. 转换为像素距离 (yOffset)
现在我们有了“相对行数”(例如 -1, 0, 1),要把它变成像素坐标,只需要乘以行高 lineHeight (\(H\))。
5. 对应回代码
将上述数学公式直接翻译成 JavaScript 代码:
| 数学符号 | 代码变量 |
|---|---|
| \(\text{index}\) | index |
| \(N\) | texts.length |
| \(H\) | lineHeight |
| \(\frac{N - 1}{2}\) | (texts.length - 1) / 2 |
| \(\times\) | * |
最终代码:
const yOffset = (index - (texts.length - 1) / 2) * lineHeight;
直观图解
假设 texts.length = 5, lineHeight = 20。
中心索引 = \((5-1)/2 = 2\)。
| 索引 (index) | 计算过程 (index - 2) |
相对行数 | 最终 yOffset (* 20) |
绘制位置相对于 j |
|---|---|---|---|---|
| 0 | \(0 - 2\) | -2 | -40 | j - 40 (最上) |
| 1 | \(1 - 2\) | -1 | -20 | j - 20 |
| 2 | \(2 - 2\) | 0 | 0 | j (中心) |
| 3 | \(3 - 2\) | +1 | +20 | j + 20 |
| 4 | \(4 - 2\) | +2 | +40 | j + 40 (最下) |
为什么要减 1? (length - 1)
这是很多初学者容易困惑的地方。
- 数组长度是 \(N\),但最大索引是 \(N-1\)(因为索引从 0 开始)。
- 我们是在对索引值进行运算,而不是对数量进行运算。
- 如果我们用
index - length / 2:- 当 \(N=3\) 时,中心是 \(1.5\)。索引 \(0, 1, 2\) 会变成 \(-1.5, -0.5, 0.5\)。这意味着没有一行是在正中心 \(0\) 的,整体画面会向下偏移半行。
- 只有用
index - (length - 1) / 2,才能保证当 \(N\) 为奇数时,正中间那一行的偏移量严格为 0。
这就是这个公式的完整推导逻辑。

浙公网安备 33010602011771号