Skip to content

梅花生长

Published: at 06:16 PM

下面这个文档,教你如何实现一个基于 Vue 3 @vueuse/core 和 Canvas绘制的梅花生长动画。

Table of contents

Open Table of contents

基础知识概述

在深入代码之前,请确保你对以下知识点有基本的了解:

准备工作

在开始之前,确保你的项目已经安装了Vue 3 和 @vueuse/core。如果没有,你可以通过以下命令进行安装:

npm install vue@next
npm install @vueuse/core

设置Vue组件

首先,你需要在你的项目中创建一个新的Vue组件。可以命名为Plum.vue,然后按照以下步骤:

<template>
  <div
    class="fixed top-0 bottom-0 left-0 right-0 pointer-events-none print:hidden"
    style="z-index: -99; position: fixed; top: 0; left: 0; bottom: 0; right: 0;"
    :style="`mask-image: ${mask}; --webkit-mask-image: ${mask};`"
  >
    <canvas ref="el" width="400" height="400"></canvas>
  </div>
</template>

创建canvas元素,并设置其宽高为400x400,以便于绘制动画。 同时,设置其样式为position: fixed; top: 0; left: 0; bottom: 0; right: 0;,使其覆盖整个视口。

import { ref, reactive, onMounted, computed } from 'vue';
import { useWindowSize, useRafFn } from '@vueuse/core';

基本属性和全局变量初始化

在这里我们需要对Canvas元素设置基本的属性以及全局变量的初始化

const r180 = Math.PI;
const r90 = Math.PI / 2;
const r15 = Math.PI / 12;
const color = '#88888825';

const el = ref(null);

const { random } = Math;
const size = reactive(useWindowSize());

const start = ref(() => {});
const MIN_BRANCH = 30;
const len = ref(6);
const stopped = ref(false);

这一部分初始化了几个重要的全局变量:一些基础的数学常量用于后续计算,el用于引用canvas元素,size用于存储窗口大小,其它的变量如start, MIN_BRANCH, len, stopped为绘画逻辑所使用。

初始化Canvas函数initCanvas

// 初始化Canvas函数
function initCanvas(canvas, width = 400, height = 400, _dpi) {
  const ctx = canvas.getContext("2d");
  // 设备像素比,解决高DPI设备画面模糊的问题
  const dpr = window.devicePixelRatio || 1;
  const bsr =
    ctx.webkitBackingStorePixelRatio ||
    ctx.mozBackingStorePixelRatio ||
    ctx.msBackingStorePixelRatio ||
    ctx.oBackingStorePixelRatio ||
    ctx.backingStorePixelRatio ||
    1;
  const dpi = _dpi || dpr / bsr;
  // 调整大小以适配高DPI屏幕
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  canvas.width = dpi * width;
  canvas.height = dpi * height;
  ctx.scale(dpi, dpi);
  return { ctx, dpi };
}

initCanvas函数接收三个参数:canvasCanvas元素,widthheightCanvas元素的宽度和高度,_dpi表示一个设备每英寸可以显示或打印的点的数量,这里用于调整Canvas的分辨率。

极坐标转笛卡尔坐标函数polar2cart

极坐标系是数学中的一种坐标系统,需要通过一些数学转换来转换为直角坐标系(笛卡尔坐标系),用于画布上的绘图。

function polar2cart(x = 0, y = 0, r = 0, theta = 0) {
  const dx = r * Math.cos(theta);
  const dy = r * Math.sin(theta);
  return [x + dx, y + dy];
}

polar2cart函数接收一个点以及一个角度,并转换为笛卡尔坐标系下的点。这对于绘制基于角度和长度的直线非常有用。

组件挂载与动画逻辑

// 组件挂载后初始化
onMounted(async () => {
  const canvas = el.value;
  const { ctx } = initCanvas(canvas, size.width, size.height);
  const { width, height } = canvas;

  let steps = [];
  let prevSteps = [];

  // 绘制步骤逻辑
  const step = (x, y, rad, counter = { value: 0 }) => {
    const length = random() * len.value;
    counter.value += 1;
    const [nx, ny] = polar2cart(x, y, length, rad);
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(nx, ny);
    ctx.stroke();
    const rad1 = rad + random() * r15;
    const rad2 = rad - random() * r15;
    if (nx < -100 || nx > size.width + 100 || ny < -100 || ny > size.height + 100) return;
    const rate = counter.value <= MIN_BRANCH ? 0.8 : 0.5;
    if (random() < rate) steps.push(() => step(nx, ny, rad1, counter));
    if (random() < rate) steps.push(() => step(nx, ny, rad2, counter));
  };

帧动画控制函数

// 帧动画控制函数
const frame = () => {
  if (performance.now() - lastTime < interval) return;
  prevSteps = steps;
  steps = [];
  lastTime = performance.now();
  if (!prevSteps.length) {
    controls.pause();
    stopped.value = true;
  }
  prevSteps.forEach((i) => {
    if (random() < 0.5) steps.push(i);
    else i();
  });
}; 

这段代码主要是控制画布上的动画效果。具体逻辑如下:

启动动画函数

start.value = () => {
   controls.pause();
   ctx.clearRect(0, 0, width, height);
   ctx.lineWidth = 1;
   ctx.strokeStyle = color;
   prevSteps = [];
   steps = [
     () => step(randomMiddle() * size.width, -5, r90),
     () => step(randomMiddle() * size.width, size.height + 5, -r90),
     () => step(-5, randomMiddle() * size.height, 0),
     () => step(size.width + 5, randomMiddle() * size.height, r180),
   ];
   if (size.width < 500) steps = steps.slice(0, 2);
   controls.resume();
   stopped.value = false;
 };

这段代码是初始化画布并开始动画的绘制过程。逻辑如下:

完整代码

<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { useWindowSize, useRafFn } from "@vueuse/core";

const r180 = Math.PI;
const r90 = Math.PI / 2;
const r15 = Math.PI / 12;
const color = "#88888825";

const el = ref(null);

const { random } = Math;
const size = reactive(useWindowSize());

const start = ref(() => {});
const MIN_BRANCH = 30;
const len = ref(6);
const stopped = ref(false);

function initCanvas(canvas, width = 400, height = 400, _dpi) {
  const ctx = canvas.getContext("2d");

  const dpr = window.devicePixelRatio || 1;
  const bsr =
    ctx.webkitBackingStorePixelRatio ||
    ctx.mozBackingStorePixelRatio ||
    ctx.msBackingStorePixelRatio ||
    ctx.oBackingStorePixelRatio ||
    ctx.backingStorePixelRatio ||
    1;

  const dpi = _dpi || dpr / bsr;

  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  canvas.width = dpi * width;
  canvas.height = dpi * height;
  ctx.scale(dpi, dpi);

  return { ctx, dpi };
}

function polar2cart(x = 0, y = 0, r = 0, theta = 0) {
  const dx = r * Math.cos(theta);
  const dy = r * Math.sin(theta);
  return [x + dx, y + dy];
}

onMounted(async () => {
  const canvas = el.value;
  const { ctx } = initCanvas(canvas, size.width, size.height);
  const { width, height } = canvas;

  let steps = [];
  let prevSteps = [];

  const step = (x, y, rad, counter = { value: 0 }) => {
    const length = random() * len.value;
    counter.value += 1;

    const [nx, ny] = polar2cart(x, y, length, rad);

    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(nx, ny);
    ctx.stroke();

    const rad1 = rad + random() * r15;
    const rad2 = rad - random() * r15;

    if (
      nx < -100 ||
      nx > size.width + 100 ||
      ny < -100 ||
      ny > size.height + 100
    )
      return;

    const rate = counter.value <= MIN_BRANCH ? 0.8 : 0.5;

    if (random() < rate) steps.push(() => step(nx, ny, rad1, counter));

    if (random() < rate) steps.push(() => step(nx, ny, rad2, counter));
  };

  let lastTime = performance.now();
  const interval = 1000 / 40;

  let controls;

  const frame = () => {
    if (performance.now() - lastTime < interval) return;

    prevSteps = steps;
    steps = [];
    lastTime = performance.now();

    if (!prevSteps.length) {
      controls.pause();
      stopped.value = true;
    }

    prevSteps.forEach((i) => {
      if (random() < 0.5) steps.push(i);
      else i();
    });
  };

  controls = useRafFn(frame, { immediate: false });

  const randomMiddle = () => random() * 0.6 + 0.2;

  start.value = () => {
    controls.pause();
    ctx.clearRect(0, 0, width, height);
    ctx.lineWidth = 1;
    ctx.strokeStyle = color;
    prevSteps = [];
    steps = [
      () => step(randomMiddle() * size.width, -5, r90),
      () => step(randomMiddle() * size.width, size.height + 5, -r90),
      () => step(-5, randomMiddle() * size.height, 0),
      () => step(size.width + 5, randomMiddle() * size.height, r180),
    ];
    if (size.width < 500) steps = steps.slice(0, 2);
    controls.resume();
    stopped.value = false;
  };

  start.value();
});
const mask = computed(() => "radial-gradient(circle, transparent, black);");
</script>

<template>
  <div
    class="fixed top-0 bottom-0 left-0 right-0 pointer-events-none print:hidden"
    style="z-index: -99; position: fixed;top: 0;left: 0;bottom: 0;right: 0;"
    :style="`mask-image: ${mask};--webkit-mask-image: ${mask};`"
  >
    <canvas ref="el" width="400" height="400" />
  </div>
</template>

总结

这篇文档主要实现了一个梅花生长动画的 Vue 组件,利用 Vue 3@vueuse/core 库的功能,结合 Canvas 绘图基础知识和动态绘制算法。它通过初始化和监听窗口变化,实现了在 Canvas 上动态绘制线条来模拟梅花生长效果,同时应用了遮罩效果增强视觉效果。
通过这个组件,我们可以轻松地在 Vue 项目中实现梅花生长动画效果,并自定义其样式和行为。