水印算法

一、引论

texts.forEach((text, index) => {
	const yOffset = (index - (texts.length - 1) / 2) * lineHeight;
	ctx.fillText(text, i, j + yOffset);
});

这段代码通常出现在使用 HTML5 Canvas (ctxCanvasRenderingContext2D) 进行绘图的开发场景中。它的核心目的是:将一组文本字符串(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

  1. (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之间)。
  2. index - ...:
    计算当前行相对于“中心行”的相对行数

    • 中心行的结果为 0
    • 中心行上方的行结果为负数(例如 -1, -2)。
    • 中心行下方的行结果为正数(例如 1, 2)。
  3. * 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 开发中,要达到完美的视觉居中,还需要考虑以下两点:

  1. ctx.textBaseline 的设置:
    Canvas 的 fillText 绘制位置取决于 textBaseline 属性。

    • 如果 textBaseline 是默认的 'alphabetic',那么 j 点对齐的是文字的基线(如字母 'x' 的底部),而不是文字的几何中心。这可能导致视觉上略微偏上或偏下。
    • 建议: 为了实现严格的几何居中,通常在运行此代码前设置:
      ctx.textBaseline = 'middle'; 
      // 或者 'ideographic' / 'hanging' 视具体字体和需求而定,但 'middle' 最常用于此类计算
      
      如果是 'middle',那么 j 点就会精确对齐每一行文字高度的中点,配合上面的 yOffset 算法,整体效果会非常完美。
  2. lineHeight 的定义:
    Canvas 原生 API 没有 直接支持 lineHeight 属性。这里的 lineHeight 必须是开发者手动定义的一个数字变量(例如 20fontSize * 1.2)。如果这个值设置得小于字体的实际高度,文字会发生重叠。

总结

这段代码是一个标准的多行文本垂直居中绘制算法。它通过数学计算抵消了索引带来的自然向下偏移,使得无论数组里有几行文字,整个文本块的垂直中心都会牢牢固定在坐标 (i, j) 上。


二、推导

这个算法的推导过程其实是一个经典的“将索引映射到对称区间”的数学问题。

我们的目标是:让一组按顺序排列的元素(索引 \(0, 1, 2, ..., N-1\)),在视觉上围绕中心点 \(0\) 对称分布。

以下是详细的推导步骤:

1. 明确目标

假设我们有 \(N\) 行文本,行高为 \(H\) (lineHeight)。
我们希望:

  • 中间那行的偏移量 yOffset0
  • 中间行上面一行的偏移量为 \(-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\)
    没有单个整数索引是中心,中心位于索引 12 之间。
    数学上的中心点是 1.5
    计算公式:\((4 - 1) / 2 = 1.5\)

结论:无论 \(N\) 是奇数还是偶数,中心点的索引值都可以统一表示为:

\[\text{centerIndex} = \frac{N - 1}{2} \]

3. 计算相对位移(行数)

既然知道了中心点的索引是 \(\frac{N - 1}{2}\),那么对于任意一个当前索引 index,它距离中心有多少“行”?

只需做减法:

\[\text{relativeRows} = \text{index} - \text{centerIndex} \]

\[\text{relativeRows} = \text{index} - \frac{N - 1}{2} \]

让我们验证一下这个公式:

  • 如果 \(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\))。

\[\text{yOffset} = \text{relativeRows} \times H \]

\[\text{yOffset} = \left( \text{index} - \frac{N - 1}{2} \right) \times \text{lineHeight} \]

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

这就是这个公式的完整推导逻辑。

posted @ 2026-03-12 16:01  庶旁  阅读(3)  评论(0)    收藏  举报