canvas 绘制 L-system 图形进阶

211 阅读2分钟

树图形规则

const Tree = {
  // 基态 --> 最初的形态
  start: "F",
  // 裂变规则
  rules: { F: "F[+F]F[-F][F]" },
}
  • 第 0 次裂变 F
  • 第 1 次裂变 F 按照裂变规则为 F[+F]F[-F][F]
  • 第 2 次裂变 F[+F]F[-F][F] --> F[+F]F[-F][F][+F[+F]F[-F][F]]F[+F]F[-F][F][-F[+F]F[-F][F]][F[+F]F[-F][F]]

树图形绘制规则

const drawRules = {
  angle: -Math.PI / 2, // 旋转角度记录,起始角度
  len: 12, // 一格距离
  x: windows.innerWidth / 2, // 起点 X 位置
  y: windows.innerHeight + 10, // 起点 Y 位置
  rotate: (20 * Math.PI) / 180, // 旋转叠加角度
  actions: {
    "-": "left",
    "+": "right",
    F: "forward",
    "[": "push",
    "]": "pop",
  }, // 行动规则
}
符号意义
-左转
+右转
F前进一格
[入栈
]出栈

裂变获得对应的字符串,根据绘制规则 —— 设定起点(x,y)和方向(angle),从左到右扫描字符串,遇到 F 就前进一格,遇到 - 向左旋转 20°,遇到 + 向右旋转 20°,这样就得到一棵树。

裂变代码实现

裂变的规则没有变化。

/**
 * 裂变
 *
 * @param graph 图形
 * @param times 裂变次数
 * @return
 */
function replacement(graph, times) {
  let { start, rules } = graph
  for (let i = 0; i < times; i++) {
    let temp = ""
    for (let j = 0; j < start.length; j++) {
      const s = start[j]
      if (Object.keys(rules).includes(s)) {
        temp += rules[s]
      } else {
        temp += s
      }
    }
    start = temp
  }
  return start
}

图形绘制代码实现

图形绘制需处理枝叶的入栈和出栈。

/**
 * 图形绘制规则
 *
 * @param str 裂变后字符串
 * @return 图形点[[[x],[y]], [[x],[y]]]三维数组
 */

function interpretation(str, drawRules) {
  let { actions, rotate, angle, len, x, y } = drawRules,
    point_x = [x],
    point_y = [y],
    coordinates = [[point_x, point_y]],
    stack = []
  for (let i = 0; i < str.length; i++) {
    const s = str[i]
    if (!Object.keys(actions).includes(s)) {
      continue
    } else if (actions[s] === "left") {
      angle -= rotate
    } else if (actions[s] === "right") {
      angle += rotate
    } else if (actions[s] === "forward") {
      const next_x = point_x[point_x.length - 1] + len * Math.cos(angle),
        next_y = point_y[point_y.length - 1] + len * Math.sin(angle)
      point_x.push(next_x)
      point_y.push(next_y)
    } else if (actions[s] === "push") {
      stack.push([
        angle,
        point_x[point_x.length - 1],
        point_y[point_y.length - 1],
      ])
    } else if (actions[s] === "pop") {
      const cur = stack.pop(),
        stackAngle = cur[0],
        _x = cur[1],
        _y = cur[2]
      point_x = [_x]
      point_y = [_y]
      angle = stackAngle
      coordinates.push([point_x, point_y])
    }
  }
  return coordinates
}

绘制代码实现

/**
 * 绘制
 *
 * @param points 图形点[[[x],[y]], [[x],[y]]]三维数组
 * @param canvas = "canvas" 元素选择器
 */
function draw(points, canvas = "canvas") {
  const ctx = document.querySelector(canvas).getContext("2d")
  ctx.strokeStyle = "rgba(100, 125, 100, 0.2)"
  ctx.beginPath()
  let count = 0
  for (let i = 0; i < points.length; i++) {
    const point = points[i]
    for (let j = 0; j < point[0].length; j++) {
      ctx.lineTo(point[0][j], point[1][j])
    }
  }
  ctx.stroke()
}

春见 - 码上掘金 (juejin.cn)