I am participating in the Mid-Autumn Festival Creative Submission contest. Please see:Mid-Autumn Festival Creative Submission Contest

preface

We wish each other a long life so as to share the beauty of this graceful moonlight, even though miles apart.

– su shi

introduce

As the Mid-Autumn Festival is approaching, this issue takes this opportunity to create a canvas animation to bless moon cakes and send it to all of you. Wish you a family reunion and all your wishes come true

Next, we will teach you to use Canvas API to make this blessing moon cake without any engine or plug-in. We’ll start with infrastructure, moon cake drawing, and typewriter animation. Buckle up and we’re off

open

Chapter 1: Infrastructure

<canvas id="canvas"></canvas>
<script type="module" src="./app.js"></script>
Copy the code

We put HTML first, javasript uses module mode, convenient for the following JS file introduction.

* {
    padding: 0;
    margin: 0;
}

html.body {
    width: 100%;
    height: 100vh;
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

#canvas {
    width: 100%;
    height: 100%;
    cursor: pointer;
    background-image: repeating-radial-gradient(circle at center center,
        transparent 0px,
        transparent 8px.rgba(255.255.255.0.05) 8px.rgba(255.255.255.0.05) 11px,
        transparent 11px,
        transparent 17px.rgba(255.255.255.0.05) 17px.rgba(255.255.255.0.05) 25px,
        transparent 25px,
        transparent 38px.rgba(255.255.255.0.05) 38px.rgba(255.255.255.0.05) 42px),
        repeating-radial-gradient(circle at center center,
            rgb(170.0.0) 0px.rgb(170.0.0) 11px.rgb(170.0.0) 11px.rgb(170.0.0) 19px.rgb(170.0.0) 19px.rgb(170.0.0) 24px.rgb(170.0.0) 24px.rgb(170.0.0) 33px.rgb(170.0.0) 33px.rgb(170.0.0) 44px.rgb(170.0.0) 44px.rgb(170.0.0) 46px);
    background-size: 60px 60px;
}
Copy the code

Then use CSS3 to draw a festive point background ~

Next, we are going to write the main logic, but let’s plan what it will be:

  1. We definitely want to get the canvas and then get the width and height of the entire screen attached.
  2. The expectation is definitely to have a moon cake drawing, moon cake has the name of the filling, there are all kinds of lines drawing, so take a class, and then instantiate a to achieve the main logic.
  3. Because the blessing message is simple, we draw it directly in the main logic. Generally, each blessing message only has two sentences, so we use commas to separate it, and type it word by word so as to record the current position and cycle.
/*app.js*/
import MoonCake from "./js/MoonCake.js";

class Application {
  constructor() {
    this.canvas = null;
    this.ctx = null;
    this.w = 0;
    this.h = 0;
    this.types = ["Five ren"."Egg yolk"."With lotus-paste"."Red bean"."Sesame"];
    this.words = [
      "Long may we share the beauty of this graceful moonlight, even miles apart."."Spend a happy holiday night, miss thousands of miles dream sweet."."Bright moon in the sky, Mid-Autumn night read zhi heart",]this.moonCake = null;
    this.text = "";
    this.textIndex = 0;
    this.dt = 0;
    this.init();
  }
  init() {
    / / initialization
    this.canvas = document.getElementById("canvas");
    this.ctx = this.canvas.getContext("2d");
    window.addEventListener("resize".this.reset.bind(this));
    this.reset();
    this.render();
    this.step();
  }
  reset() {
    / / reset
    this.dt = 0;
    this.w = this.canvas.width = this.ctx.width = window.innerWidth;
    this.h = this.canvas.height = this.ctx.height = window.innerHeight;
    this.render()
  }
  render() {
    / / the main rendering
    const {w, h, ctx, types, words} = this;
    this.text = words[~~(words.length * Math.random())].split(",")
    this.textIndex = 0;
    // Instantiate mooncakes
    this.moonCake = new MoonCake({
      x: w / 2.y: h / 2.scale: 85..name: types[~~(types.length * Math.random())]
    }).render(ctx); 
  }
  drawText() {
    // Typewriter drawing
  }
  step() {
    / / redraw
    requestAnimationFrame(this.step.bind(this));
    const {ctx, w, h} = this;
    ctx.clearRect(0.0, w, h);
    this.moonCake && this.moonCake.draw();
    this.drawText();
    this.dt++; }}window.onload = new Application();
Copy the code

We expect to render is the instance of the moon cake, we should pass is its coordinates and scale size and filling name, next we will focus on to, is how to draw a moon cake to ~

Chapter 2: Moon cake drawing

To draw a moon cake, it is necessary to pile canvas foundation. We observed that a moon cake can be divided into two lace, two rounded rectangles, six horizontal lines, and zigzagging lines, and the filling name.

Our difficulty is how to draw the lace pattern, can actually imagine a round 360 degrees so a total of how much we want to split into side divided by how much, so you can get point of view, using the origin, can calculate the corresponding x and y coordinates, then connected into a triangle, then the problem comes, how to make lace, We can use bezier curves to do this, we bisect the current triangle to get the Angle, add a little extension to the radius of the circle, make it find the center point, connect it with a curve and complete the drawing of the lace, because here we use 12 edges to calculate the Angle and then draw a moon cake skin

And rounded rectangles, we’re going to use the universal arcTo for edges because there are only four angles that can be easily divided into math.pi.

Draw the filling name, because there may be three words, we also want to deal with separately, reduce the adjustment position.

And the other thing to say is that we want to show the drawing process step by step so we’re going to store the drawing process in an array, keep track of the current time, and append new drawing tasks every 200 milliseconds. Until the end, we want to give a status notification that it has ended, and the aspect main logic does something else.

The rest is some basic drawing, I won’t go over it here, see the complete code of the moon cake class below:

/*MoonCake.js*/
class MoonCake {
  constructor(options) {
    this.x = 0;                              // X coordinates
    this.y = 0;                              // Y coordinates
    this.name = "Five ren"                        / / paste
    this.strokeStyle = "rgb(180,110,48)";    / / line color
    this.fillStyle = "RGB (250201,)";      / / fill color
    this.fontSize = 36;                      // Font size
    this.scale = 1;                          // Scale
    Object.assign(this, options)
    this.ctx = null;
    this.progress = 0;                       // Draw progress
    this.stepFn = []                         // Draw steps
    this.isComplete = false;                 // Whether to finish drawing
    this.nowDate = new Date(a);// The current time
    this.lastDate = new Date(a);// End time
    return this;
  }
  render(ctx) {
     / / rendering
    if(! ctx)throw new Error("context is undefined.");
    this.ctx = ctx;
    this.stepFn.length = 0;
    this.stepFn.push(() = > this.drawEdge(180.20))
    this.stepFn.push(() = > this.drawEdge(140.12))
    this.stepFn.push(() = > this.drawRoundRectPath(140.220.40))
    this.stepFn.push(() = > this.drawRoundRectPath(220.140.40))
    this.stepFn.push(() = > this.drawLine(30, -110.30.110))
    this.stepFn.push(() = > this.drawLine(0, -110.0.110))
    this.stepFn.push(() = > this.drawLine(-30, -110, -30.110))
    this.stepFn.push(() = > this.drawLine(-110, -30.110, -30))
    this.stepFn.push(() = > this.drawLine(-110.0.110.0))
    this.stepFn.push(() = > this.drawLine(-110.30.110.30))
    this.stepFn.push(() = > this.drawRect(140.140))
    this.stepFn.push(() = > this.drawBox(140))
    this.stepFn.push(() = > this.drawText())
    return this;
  }
  draw() {
    / / to draw
    for (let i = 0; i < this.progress; i++) {
      this.stepFn[i] && this.stepFn[i]()
    }
    if (this.progress > this.stepFn.length) return this.isComplete = true;
    this.nowDate = new Date(a);if(this.nowDate-this.lastDate>200) {this.progress++;
      this.lastDate = this.nowDate; }}drawText(n) {
    // Draw text
    const {ctx, x, y, name, fontSize, strokeStyle, scale} = this;
    let size = fontSize;
    ctx.save()
    ctx.translate(x, y);
    ctx.scale(scale, scale)
    ctx.fillStyle = strokeStyle;
    ctx.textAlign = "center";
    ctx.font = `bolder ${size}px fangsong,self`
    ctx.shadowColor = strokeStyle;
    ctx.shadowBlur = 1;
    if (name.length == 2) {
      ctx.fillText(name.charAt(0), 0, -size * 0.5 + 5);
      ctx.fillText(name.charAt(1), 0, size * 0.5 + 5);
    }
    if (name.length >= 3) {
      size *= 0.7;
      ctx.font = `bolder ${size}px fangsong,self`
      ctx.fillText(name.charAt(0), 0, -size * 1 + 2);
      ctx.fillText(name.charAt(1), 0, size * 0 + 2);
      ctx.fillText(name.charAt(2), 0, size * 1 + 2);
    }
    ctx.restore();
  }
  drawBox(size) {
    // Draw a polyline box
    const {ctx, x, y, strokeStyle, scale} = this;
    let v = 17,
      n = -size / 2;
    ctx.save()
    ctx.translate(x, y);
    ctx.scale(scale, scale)
    ctx.beginPath();
    ctx.lineCap = "round";
    ctx.lineWidth = 4;
    ctx.strokeStyle = strokeStyle;
    ctx.moveTo(v + n, n)
    ctx.lineTo(v + n, size - v + n)
    ctx.lineTo(size - v + n, size - v + n)
    ctx.lineTo(size - v + n, v + n)
    ctx.lineTo(v * 2 + n, v + n)
    ctx.lineTo(v * 2 + n, size - v * 2 + n)
    ctx.lineTo(size - v * 2 + n, size - v * 2 + n)
    ctx.lineTo(size - v * 2 + n, 45 + n)
    ctx.stroke()
    ctx.restore();

  }
  drawLine(x1, y1, x2, y2) {
    // Draw lines
    const {ctx, x, y, strokeStyle, scale} = this;
    ctx.save()
    ctx.translate(x, y);
    ctx.scale(scale, scale)
    ctx.beginPath();
    ctx.lineCap = "round";
    ctx.lineWidth = 4;
    ctx.strokeStyle = strokeStyle;
    ctx.moveTo(x1, y1)
    ctx.lineTo(x2, y2)
    ctx.stroke()
    ctx.restore();
  }
  drawRect(width, height) {
    // Draw a rectangle
    const {ctx, x, y, strokeStyle, fillStyle, scale} = this;
    ctx.save()
    ctx.translate(x, y);
    ctx.scale(scale, scale)
    ctx.beginPath();
    ctx.lineCap = "round";
    ctx.lineWidth = 4;
    ctx.strokeStyle = strokeStyle;
    ctx.fillStyle = fillStyle;
    ctx.rect(-width / 2, -height / 2, width, width)
    ctx.fill();
    ctx.stroke()
    ctx.restore();
  }
  drawRoundRectPath(width, height, radius) {
    // Draw rounded rectangles
    const {ctx, x, y, strokeStyle, fillStyle, scale} = this;
    let w = -width / 2,
      h = -height / 2
    ctx.save()
    ctx.translate(x, y);
    ctx.scale(scale, scale)
    ctx.lineCap = "round";
    ctx.strokeStyle = strokeStyle;
    ctx.fillStyle = fillStyle;
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.arc(width - radius + w, height - radius + h, radius, 0.Math.PI / 2);
    ctx.lineTo(radius + w, height + h);
    ctx.arc(radius + w, height - radius + h, radius, Math.PI / 2.Math.PI);
    ctx.lineTo(w, radius + h);
    ctx.arc(radius + w, radius + h, radius, Math.PI, Math.PI * 3 / 2);
    ctx.lineTo(width - radius + w, h);
    ctx.arc(width - radius + w, radius + h, radius, Math.PI * 3 / 2.Math.PI * 2);
    ctx.lineTo(width + w, height - radius + h);
    ctx.closePath();
    ctx.stroke()
    ctx.restore();
  }
  drawEdge(radius, lineWidth) {
    // Draw lace
    const {ctx, x, y, strokeStyle, fillStyle, scale} = this;
    let n = 12,
      v = 360 / n,
      m = 30;
    ctx.save()
    ctx.translate(x, y);
    ctx.scale(scale, scale)
    ctx.beginPath();
    ctx.lineCap = "round";
    for (let i = 0; i < n; i++) {
      let angle1 = i * v * Math.PI / 180;
      let angle2 = (i + 1) * v * Math.PI / 180;
      let angle3 = (i + 0.5) * v * Math.PI / 180;
      ctx.lineWidth = lineWidth;
      ctx.strokeStyle = strokeStyle;
      ctx.fillStyle = fillStyle;
      let _sx = radius * Math.cos(angle1),
        _sy = radius * Math.sin(angle1);
      ctx.lineTo(_sx, _sy);
      let _ex = radius * Math.cos(angle2),
        _ey = radius * Math.sin(angle2);

      let _mx = (radius + m) * Math.cos(angle3),
        _my = (radius + m) * Math.sin(angle3); ctx.bezierCurveTo(_mx, _my, _ex, _ey, _ex, _ey) } ctx.closePath(); ctx.stroke() ctx.fill(); ctx.restore(); }}export default MoonCake;
Copy the code

Chapter three: Typewriter animation

/*api.js*/
drawText() {
    const {ctx, w, h, text} = this;
    ctx.save();
    ctx.fillStyle = "RGB (253190, 0)";
    ctx.textAlign = "center";
    ctx.font = `bolder 32px fangsong,self`
    ctx.shadowColor = "RGB (253190, 0)";
    ctx.shadowBlur = 10;
    ctx.fillText(text[0].substr(0.this.textIndex), w / 2, h * 0.36 + 240);
    if (text[0].length < this.textIndex) {
        ctx.fillText(text[1].substr(0.this.textIndex - text[0].length), w / 2, h * 0.36 + 240 + 52);
    }
    ctx.restore()
}
step() {
    requestAnimationFrame(this.step.bind(this));
    const {ctx, w, h} = this;
    ctx.clearRect(0.0, w, h);
    this.moonCake && this.moonCake.draw();
    if (this.moonCake.isComplete) {
        this.moonCake.y -= 1.2;
        this.moonCake.y = Math.max(h * 0.36.this.moonCake.y)
        if (this.moonCake.y == h * 0.36) {
            this.drawText();
            if (this.dt % 20= =0 && this.textIndex < this.text.join("").length) {
                this.textIndex++; }}}this.dt++;
}
Copy the code

We want the typewriter effect to happen again when the moon cake is finished so I’m going to use isComplete and once I’ve finished moving up a certain distance, I’m going to do the text.

Because the message we just wrote is like this:

this.words = [
      "Long may we share the beauty of this graceful moonlight, even miles apart."."Spend a happy holiday night, miss thousands of miles dream sweet."."Bright moon in the sky, Mid-Autumn night read zhi heart",]Copy the code

So we’re going to print out two lines, and we’re going to get rid of both numbers, so we’re going to generate a comma separated array of two dimensions, representing the first and second lines, and changing the position of the drawing as dt increases, and that’s it.


So now that we’re done, do you want to do one? Online demo

expand

In addition, in fact, there are many flaws. For example, the font used in this issue is imitated song, many computers can carry this font, but there are few mobile phones, so we have time to use a set of imitated Song fonts according to our own writing. After all, Chinese fonts are too big to be used directly.

We have used a lot of graphics in this issue. It is difficult to say that it is not easy to say that it is not easy. In fact, it is better to use SVG to animate paths. However, canvas drawing is very flexible when properly used, and amazing works can be completed with the combination of simple graphics. I hope you can be inspired by this and send your own Mid-Autumn Festival wishes to your friends and relatives.


If it is helpful, friends don’t forget, like, comment, favorites, one key three even yo ~~