还不快来用 Canvas 写一个展示梅花?

1,273 阅读1分钟

本文正在参加「金石计划」

前言,在看了 antfu 的博客之后,对他的博客里的梅花片段久久不能忘怀,就想着翻一下源码做一个简单版本的梅花图来看看。

可以直接在这里看代码,以及其详细展示。

image.png

我理解这个梅花的逻辑并不复杂,主要是对 canvas 以及数学的的应用。

首先同步一个点,我们的技术栈是 Vue3 + TS

大致思路:我们的梅花是一个一个的 canvas 线段构成的,我们需要从四个方向开始绘制,并且一帧一帧的渲染上去。

  1. 首先我们定义一个 Canvas
<canvas width="600" height="600" ref="el" class="plum-border"></canvas>

class 是用来加黑色边框的

  1. 其次我们需要获取到 Canvas
function init() {
  const canvas = el.value!
  const ctx = canvas.getContext('2d')!
  return { ctx }
}

我们需要把获取到的 canvas 暴露出去做展示

  1. 画一条线
function line(ctx: CanvasRenderingContext2D, p1: Point, p2: Point) {
  ctx.beginPath();
  ctx.strokeStyle! = 'black'
  ctx.moveTo(p1.x, p1.y);
  ctx.lineTo(p2.x, p2.y);
  ctx.stroke();
}

Point 的数据结构如下: {x: number;y: number;}

  1. 获取终点的位置
function getP2(x = 0, y = 0, theta = 0) {
  const dx = x + len * Math.cos(theta)
  const dy = y + len * Math.sin(theta)
  return { x: dx, y: dy }
}

theta 是角度

  1. 写绘制函数
  const step = function (x: number, y: number, rad: number) {
    const p1 = { x, y }
    const p2 = getP2(x, y, rad)
    line(ctx, p1, p2)
    if (x > 600 || x < 0 || y > 600 || y < 0) return
    if (random() < 0.5 || interable < 4) {
      pendingCallback.push(() => step(p2.x, p2.y, rad + random() * r15))
    }
    if (random() < 0.5 || interable < 4) {
      pendingCallback.push(() => step(p2.x, p2.y, rad - random() * r15))
    }
  }
  
  pendingCallback.push(() => step(random() * 600, 0, r90))
  pendingCallback.push(() => step(random() * 600, 600, -r90))
  pendingCallback.push(() => step(0, random() * 600, 0))
  pendingCallback.push(() => step(600, random() * 600, r180))
  1. 一帧一帧的开始绘制
  function frame() {
    const task = [...pendingCallback]
    pendingCallback.length = 0
    interable += 1
    task.forEach(fn => fn())
  }

  function startFrame() {
    requestAnimationFrame(() => {
      frame()
      startFrame()
    })
  }
  • 那么以下是我们的全部代码
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const el = ref<HTMLCanvasElement>()

/* 角度 */
const r180 = Math.PI
const r90 = Math.PI / 2
const r15 = Math.PI / 12
/* random 函数 */
const { random } = Math
/* len 也就是延展出去的长度 */
const len: number = 6
/* 匿名函数 */
const pendingCallback: any[] = []

interface Point {
  x: number;
  y: number;
}
/* initialize */
function init() {
  const canvas = el.value!
  const ctx = canvas.getContext('2d')!
  return { ctx }
}
/* paintLine */
function line(ctx: CanvasRenderingContext2D, p1: Point, p2: Point) {
  ctx.beginPath();
  ctx.strokeStyle! = 'black'
  ctx.moveTo(p1.x, p1.y);
  ctx.lineTo(p2.x, p2.y);
  ctx.stroke();
}
/* getP2 */
function getP2(x = 0, y = 0, theta = 0) {
  const dx = x + len * Math.cos(theta)
  const dy = y + len * Math.sin(theta)
  return { x: dx, y: dy }
}

onMounted(() => {
  const { ctx } = init()
  let interable = 0

  const step = function (x: number, y: number, rad: number) {
    const p1 = { x, y }
    const p2 = getP2(x, y, rad)
    line(ctx, p1, p2)
    if (x > 600 || x < 0 || y > 600 || y < 0) return
    if (random() < 0.5 || interable < 4) {
      pendingCallback.push(() => step(p2.x, p2.y, rad + random() * r15))
    }
    if (random() < 0.5 || interable < 4) {
      pendingCallback.push(() => step(p2.x, p2.y, rad - random() * r15))
    }
  }

  pendingCallback.push(() => step(random() * 600, 0, r90))
  pendingCallback.push(() => step(random() * 600, 600, -r90))
  pendingCallback.push(() => step(0, random() * 600, 0))
  pendingCallback.push(() => step(600, random() * 600, r180))

  function frame() {
    const task = [...pendingCallback]
    pendingCallback.length = 0
    interable += 1
    task.forEach(fn => fn())
  }

  function startFrame() {
    requestAnimationFrame(() => {
      frame()
      startFrame()
    })
  }

  startFrame()
})
</script>

<template>
  <canvas width="600" height="600" ref="el" class="plum-border"></canvas>
</template>

<style scoped>
.plum-border {
  height: 600px;
  width: 600px;
  border: 1px black solid;
}
</style>