本文正在参加「金石计划」
前言,在看了 antfu 的博客之后,对他的博客里的梅花片段久久不能忘怀,就想着翻一下源码做一个简单版本的梅花图来看看。
可以直接在这里看代码,以及其详细展示。
我理解这个梅花的逻辑并不复杂,主要是对 canvas 以及数学的的应用。
首先同步一个点,我们的技术栈是 Vue3 + TS
大致思路:我们的梅花是一个一个的 canvas 线段构成的,我们需要从四个方向开始绘制,并且一帧一帧的渲染上去。
- 首先我们定义一个 Canvas
<canvas width="600" height="600" ref="el" class="plum-border"></canvas>
class 是用来加黑色边框的
- 其次我们需要获取到 Canvas
function init() {
const canvas = el.value!
const ctx = canvas.getContext('2d')!
return { ctx }
}
我们需要把获取到的 canvas 暴露出去做展示
- 画一条线
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;}
- 获取终点的位置
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 是角度
- 写绘制函数
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()
})
}
- 那么以下是我们的全部代码
<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>