Canvas 实现樱花特效

·  阅读 3620

樱花

搬运一下老文。这是基于 React + Canvas 画的一朵樱花。

canvas

首先需要了解一些 canvas 的概念。使用 <canvas></canvas> 会创建一块画布,我们可以在这个上面绘制内容。

var canvas = document.getElementById('tutorial');
//获得 2d 上下文对象
var ctx = canvas.getContext('2d');
复制代码

绘制路径

一般来说,canvas 创建的画布以左上角作为原点(0, 0)。

我们使用 beginPath 来创建一条路径。然后用 moveTo 移动到起始点坐标,用 closePath 闭合路径。

可以使用 stroke 来绘制图形轮廓,用 fill 来绘制填充内容。

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath(); //新建一条path
    ctx.moveTo(50, 50); //把画笔移动到指定的坐标
    ctx.lineTo(200, 50);  //绘制一条从当前位置到指定坐标(200, 50)的直线.
    //闭合路径。会拉一条从当前点到path起始点的直线。如果当前点与起始点重合,则什么都不做
    ctx.closePath();
    ctx.stroke(); //绘制路径。
}
draw();
复制代码

绘制圆形

可以通过 arc 来绘制一个圆形,它接受四个参数,分别是圆形坐标、半径、开始弧度、结束弧度、顺逆时针。 Math.PI 就是数学上的圆周率π,一般是 3.1415926...

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.arc(50, 50, 40, 0, Math.PI * 2, false);
    ctx.stroke();
}
draw();
复制代码

弧度

弧度(rad)是数学上面的概念,一般是指从圆心拉了两条半径,这俩半径中间的圆弧,如果它的长度和半径相等,那么这个角度就是一弧度。

一般来说,一个圆有 2 * π 个弧度,也是因为圆周长是 2πR

const rad = 180 / π
复制代码

cos 和 sin

以前初中就学过这俩知识,对于一个直角三角形来说,cos 就是较长的直角边除以斜边,sin 则是较短的直角边除以斜边。

在 JavaScript 里面会接收弧度作为参数,所以需要手动转换度数为弧度。

const cos = Math.cos(2 * rad)
const sin = Math.sin(2 * rad)
复制代码

贝塞尔曲线

一般我们绘制贝塞尔曲线都是用的二次贝塞尔曲线,它有一个起始点、控制点、结束点三个坐标来决定的。

感兴趣的可以看一下这篇文章:怎么理解贝塞尔曲线?

在 canvas 里面也提供了 quadraticCurveTo(cp1x, cp1y, x, y) 方法来绘制曲线。

开始绘制

了解完上面的知识后,开始绘制我们的樱花。首先要知道,樱花包含花瓣和花蕊两部分,花蕊在花瓣正中间。

我们考虑用粉红色来绘制花瓣,用白色绘制花蕊。

image

樱花有五瓣,所以一瓣的夹角是 75°,也就是 75 / rad 弧度。

首先我们需要声明一个樱花类,它有半径、圆心坐标、颜色等属性。接着开始绘制。

class Flower {
  r = r;
  color = color;
  cx = 800;
  cy = 500;
}
复制代码

花瓣

绘制最麻烦的一步就是花瓣的弧度,这是个贝塞尔曲线。观察图片,我们可以以花瓣凹进去的三角形(剪刀形状)到圆心距离作为半径,以剪刀两边的作为一个贝塞尔曲线的控制点。

image

那么这个控制点的坐标是什么呢?如上图所示,其实我们的控制点p1在分割线上,和原点距离是半径的长度,而终点在p2上面,长度大概是半径的1.2-1.4倍。p0p1 和 p0p2 大概构成了 25 °的角。

所以这里也很容易进行计算。首先计算出控制点 p1 的位置,肯定是 cx + R * Math.cos(a * part / rad),这里的 a 就是循环生成的,a * part / rad 就是指的是第几瓣的角度。

 const x0 = cx + R * Math.cos((a * part) / rad);
 const y0 = cy + R * Math.sin((a * part) / rad);
复制代码

然后我们找到 1/3 (25°)的坐标。设置 R1 为 1.3 倍半径。

const x1 = cx + R1 * Math.cos((a * part + 2 * part / 6) / rad);
 const y1 = cy + R1 * Math.sin((a * part + 2 * part / 6) / rad);
复制代码

这样关键的两个点就画了出来,接着生成贝塞尔曲线。

ctx.moveTo(cx, cy);
ctx.quadraticCurveTo(x0, y0, x1, y1);
复制代码

然后我们绘制出剩下的一半花瓣。最终代码如下:

const x0 = cx + R * Math.cos((a * part) / rad);
    const y0 = cy + R * Math.sin((a * part) / rad);
  
    const x1 = cx + R1 * Math.cos((a * part + 2 * part / 6) / rad);
    const y1 = cy + R1 * Math.sin((a * part + 2 * part / 6) / rad);
    
    // 这个点其实在中点,也就是 37.5°的地方
    const x2 = cx + R * Math.cos((a * part + 3 * part / 6) / rad); 
    const y2 = cy + R * Math.sin((a * part + 3 * part / 6) / rad);
  
    const x3 = cx + R1 * Math.cos((a * part + 4 * part / 6) / rad);
    const y3 = cy + R1 * Math.sin((a * part + 4 * part / 6) / rad);
  
    const x4 = cx + R * Math.cos((a * part + part) / rad);
    const y4 = cy + R * Math.sin((a * part + part) / rad);
  
    // petal
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.quadraticCurveTo(x0, y0, x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x3, y3);
    ctx.quadraticCurveTo(x4, y4, cx, cy);
    ctx.fill();
    ctx.stroke();
复制代码

花蕊

接着绘制花蕊,其实花蕊很容易绘制,因为它们分别处于 1/3、1/2、2/3 处。

const ax0 = cx + R / 3 * Math.cos((a * part + 2 * part / 6) / rad);
    const ay0 = cy + R / 3 * Math.sin((a * part + 2 * part / 6) / rad);
    const ax1 = cx + R / 2 * Math.cos((a * part + 3 * part / 6) / rad);
    const ay1 = cy + R / 2 * Math.sin((a * part + 3 * part / 6) / rad);
    const ax2 = cx + R / 3 * Math.cos((a * part + 4 * part / 6) / rad);
    const ay2 = cy + R / 3 * Math.sin((a * part + 4 * part / 6) / rad);
复制代码

这几个坐标点都找好了,但是不要忘了在终点绘制一个小圆点,这个更像花蕊上面的蕊头。

 ctx.arc(ax0, ay0, 2, 0, 2 * Math.PI)
复制代码

最终的代码如下:

const { ctx, cx, cy, r: R } = this
    ctx.save();
    ctx.strokeStyle = "#fff";

    const ax0 = cx + R / 3 * Math.cos((a * part + 2 * part / 6) / rad);
    const ay0 = cy + R / 3 * Math.sin((a * part + 2 * part / 6) / rad);
    const ax1 = cx + R / 2 * Math.cos((a * part + 3 * part / 6) / rad);
    const ay1 = cy + R / 2 * Math.sin((a * part + 3 * part / 6) / rad);
    const ax2 = cx + R / 3 * Math.cos((a * part + 4 * part / 6) / rad);
    const ay2 = cy + R / 3 * Math.sin((a * part + 4 * part / 6) / rad);
    let ary = []
    // 如果半径大于40
    if (R > 40) {
      ary = [{
        x: ax0,
        y: ay0
      }, {
        x: ax1,
        y: ay1
      }, {
        x: ax2,
        y: ay2
      }];
    } else {
      ary = [{
        x: ax1,
        y: ay1
      }];
    }

    ctx.beginPath();
    for (let i = 0; i < ary.length; i++) {
      ctx.moveTo(cx, cy);
      ctx.lineTo(ary[i].x, ary[i].y);
      ctx.arc(ary[i].x, ary[i].y, 2, 0, 2 * Math.PI)
    }
    ctx.stroke();
    ctx.restore();
复制代码

总结

最后,我把这个项目部署到了线上,可以访问 sakura.gyyin.top 来访问到。

欢迎关注我的 Github:github.com/yinguangyao…

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改