Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

画一朵樱花 #48

Open
yinguangyao opened this issue Dec 1, 2020 · 0 comments
Open

画一朵樱花 #48

yinguangyao opened this issue Dec 1, 2020 · 0 comments

Comments

@yinguangyao
Copy link
Owner

yinguangyao commented Dec 1, 2020

樱花

这是基于 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();

总结

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant