在 canvas 里长一棵梅

761 字 · 2 分钟#canvas#生成艺术

antfu 的个人网站背景有一棵会慢慢生长的梅,我盯着看了很久,之后照着这个思路自己种了一版,加了些自己的笔墨。算法本体小得惊人——一个会分叉的随机游走——但每个参数都在影响这棵树的「性格」,调参的时间比写代码长得多。

先长一棵:

每一棵都不重样。点击画布,再长一棵

核心:会分叉的随机游走

一根枝就是一个状态:位置加方向。每一步向前画一小段,然后掷骰子决定要不要分出左右两叉,子枝的方向在父枝基础上随机偏最多 15 度:

const R15 = Math.PI / 12

function step(branch, counter) {
  const len = Math.random() * 6
  const nx = branch.x + Math.cos(branch.angle) * len
  const ny = branch.y + Math.sin(branch.angle) * len
  drawLine(branch.x, branch.y, nx, ny)
  counter.value++

  const rate = counter.value < 30 ? 0.8 : 0.5
  if (Math.random() < rate) {
    queue.push(() => step({ x: nx, y: ny, angle: branch.angle + Math.random() * R15 }, counter))
  }
  if (Math.random() < rate) {
    queue.push(() => step({ x: nx, y: ny, angle: branch.angle - Math.random() * R15 }, counter))
  }
}

几个数字都不是随便写的:

每段 0 到 6px。 这是「像不像枝」的分水岭。段长放大到 20px,画出来是折线图;短碎的笔触叠起来才有干笔皴出来的毛糙感。随机到 0 也没关系,那一步等于原地掂了一下笔。

分叉率先 0.8 后 0.5。 前 30 段用高分叉率把骨架铺开,之后降到一半让枝条慢慢死绝。全程 0.8 会长成一团绒球,全程 0.5 则大概率还没成形就断了——先繁后疏,树冠才有疏密。0.5 附近还有个微妙的临界性质:每根枝平均只有一个后代,整棵树随时可能灭绝,也可能突然爆发,每次生长都悬着一口气。

越界淘汰。 长出画布外一圈的枝直接掐掉。这不只是省性能——边界本身参与了构图,树的轮廓一半是算法长的,一半是画框裁的。

迟疑感

一开始我把任务队列每帧全部执行,树长得像喷的。后来改成每帧只随机执行一半,剩下的顺延到之后的帧:

const batch = queue
queue = []
for (const task of batch) {
  if (Math.random() < 0.5) task()
  else queue.push(task)
}

生长速度立刻活了:有的枝一口气蹿出去,有的顿在那里两三帧才继续。这个「迟疑」是整个动画里我最喜欢的细节——匀速生长是机器,走走停停才像活物。

笔墨

线条不能一样粗。起笔的主干压重些,越到梢头笔锋越细,用累计段数线性衰减:

ctx.lineWidth = Math.max(0.5, 1.7 - counter.value * 0.012)

再给走到头的梢一成左右的几率点两三个墨点当花。这个概率试过 0.4,满树是花,腻;降到 0.12,大多数枝头留白,偶尔一簇,才有点「疏影横斜」的意思。少数花换成一点蓝,整幅画就有了一处提色。

参数即性格

这套代码没有一行在「画树」,它只描述了一种生长的倾向:往前走、偶尔分叉、越长越细、出界就死。所谓的树,是这些倾向叠了几千步之后自己冒出来的样子。角度换成 45 度,长出来的就是闪电;分叉率随深度递增,长出来的就是珊瑚。

调参数的那几个晚上比写代码久得多。概率挪一点,整棵树的脾气就变了。