Small knowledge, big challenge! This paper is participating in theEssentials for programmers”Creative activities

This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money

preface

Movement and change are constantly renewing the world,

As the unbroken time is always renewing the continuity of the endless years.

— Marcus Aurelius

introduce

In this issue, I’m going to introduce a new work — kaleidoscope brushes. I’m sure many of us played with kaleidoscopes when we were young. Through a long tube, through a lens, we can see the colorful world inside. As we turn it, it presents strange images in front of our eyes. Therefore, today I wanted to make this time appear in the computer again through code drawing, and I came up with this brush.

As you can see in the image above, it changes colors and shapes as you drag and drag your input device. Besides, it does not rely on any third party library and only uses Canvas API to complete drawing. Next, we will explain it from three aspects, such as infrastructure, event binding and drawing and rendering.

The body of the

1. Infrastructure

We still use Vite to build, first look at the HTML structure

<head>
    <! -... -->
	<style>
        body{
            width: 100%;
            height: 100vh;
        }
        canvas{
            width: 100%;
            height: 100%;         
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script type="module" src="./app.js"></script>
</body>
Copy the code

It’s easy, we just put a canvas tag and introduce app.js as the main logic. Because this time is also brief, I will write the logic of this time in app.js, not separate split.

/*app.js*/
class Application {
  constructor() {
    this.canvas = null;                         / / the canvas
    this.ctx = null;                            / / environment
    this.w = 0;                                 / / canvas width
    this.h = 0;                                 / / the canvas
    this.n = 18;                                // The number of lines drawn in a kaleidoscope at one time
    this.bgColor = "rgba(255,255,255,1)";       // Background color
    this.penColor = 0;                          // Line color
    this.lineCap = "round"                      // Line cap type
    this.state = false;                         // The current pressed state, false is not pressed, true is pressed
    this.point = null;                          // The currently pressed coordinate point
    this.rotate = 0;                            // The offset Angle of each line in the kaleidoscope
    this.init();
  }
  init() {
    this.canvas = document.getElementById("canvas");
    this.ctx = this.canvas.getContext("2d");
    window.addEventListener("resize".this.reset.bind(this));
    this.penColor = ~~(Math.random() * 360)
    this.rotate = 360 / this.n * Math.PI / 180;
    this.reset();
    this.bindEvent();
    this.step();
  }
  reset() {
    / / reset
    this.w = this.canvas.width = this.ctx.width = window.innerWidth;
    this.h = this.canvas.height = this.ctx.height = window.innerHeight;
    this.penColor = ~~(Math.random() * 360)
    this.clear();
  }
  clear() {
    // Empty the canvas
    this.ctx.clearRect(0.0.this.w, this.h);
  }
  _mouseDown(e) {
     // Input device press down
  }
  _mouseUp() {
     // The input device is lifted
  }
  _mouseMove(e) {
      // Input device move
  }
  bindEvent() {
     // Bind events
  }
  drawLine(x, y) {
     // Draw lines
  }
  step() {
     / / frame change}}window.onload = new Application();
Copy the code

The infrastructure is basically the same as in the previous case, so the comments should make sense.

We mainly explain these points here:

  • PenColor: This is our current color pen, he is a value type, because every time to generate a random value to give it, and behind in step changes in real time, will make it gain change constantly, to achieve the effect of changing color, so, at the time of drawing, will to do through an HSL color control, while the penColor ACTS as the hue value in this column, HSL (Hue, Saturation, brightness).
  • Rotate: Initialization computes the basic Angle of each line segment offset by the following strokes.
  • Reset: Expect to reset the entire environment every time the screen width and height changes.

2. Bind data

Let’s analyze. My scene could be on a computer or on a phone, so do two sets of events, a tap and a mouse.

bindEvent() {
    const {canvas} = this;
    if (navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)) {
        canvas.addEventListener("touchstart".this._mouseDown.bind(this), false);
        canvas.addEventListener("touchmove".this._mouseMove.bind(this), false);
        canvas.addEventListener("touchend".this._mouseUp.bind(this), false);
    } else {
        canvas.addEventListener("mousedown".this._mouseDown.bind(this), false);
        canvas.addEventListener("mousemove".this._mouseMove.bind(this), false)
        canvas.addEventListener("mouseup".this._mouseUp.bind(this), false)
        canvas.addEventListener("mouseout".this._mouseUp.bind(this), false)}}Copy the code

They all fall into press, move, lift, and then we’ll write the corresponding events

_mouseDown(e) {
    let {clientX, clientY, touches} = e;
    let x = clientX,
        y = clientY;
    if (touches) {
        x = touches[0].clientX;
        y = touches[0].clientY;
    }
    this.state = true
    this.point = {x,y}
}
_mouseMove(e) {
    if (!this.state) return;
    let {clientX, clientY, touches} = e;
    let x = clientX,
        y = clientY;
    if (touches) {
        x = touches[0].clientX;
        y = touches[0].clientY;
    }
    this.drawLine(x, y)
    this.point = {x,y}
}
_mouseUp() {
    this.state = false
}
Copy the code
  • Press: to change the state to true and record the initial point
  • Move: Only if the state is true is executed downward. Draw a line at drawLine and then use point to save the coordinates so that the next drawing can start with the finished coordinates.
  • Lift: Changes the state to false

3. Draw and render

In the input device move event, we can get the values to move to the X and y positions, and then use drawLine to do the drawing first.

drawLine(x, y) {
    const {w, h, ctx, penColor, point, rotate, n, lineCap} = this;
    ctx.lineWidth = 10;
    ctx.strokeStyle = `hsl(${~~penColor}`, 100%, 50%);
    ctx.lineCap = lineCap;
    ctx.lineJoin = "round";
    ctx.shadowColor = "Rgba (255255255, 1)";
    ctx.shadowBlur = 1;
    for (let i = 0; i < n; i++) {
        ctx.save();
        ctx.translate(w / 2, h / 2);
        ctx.rotate(rotate * i);
        if ((n % 2= = =0) && i % 2! = =0) {
            ctx.scale(1, -1);
        }
        ctx.beginPath();
        ctx.moveTo(point.x - w / 2, point.y - h / 2);
        ctx.lineTo(x - w / 2, y - h / 2); ctx.stroke(); ctx.restore(); }}Copy the code

In fact, it is easy to say, the most important thing is to find the origin of coordinates, a origin as the center to draw the line you want to draw. And how does it split? If you look at the code, I’m using the ctx.rotate method, because originally we had a rotate in initialization that calculates its offset Angle, and each one is based on the origin and the current subscript I and should be offset to that Angle, and when the total number of lines is even, Every other line gives it a symmetry around the circumference. What remains is the most basic drawing operation of the Canvas API, which is too basic to explain too much.

Look, we can draw things on canvas now, but the kaleidoscope changes, and we don’t expect the line segments to stay and fade away, and then we have to keep redrawing it.

step() {
    window.requestAnimationFrame(this.step.bind(this))
    const {ctx, w, h} = this;
    ctx.fillStyle = "rgba(255,255,255,.008)"
    ctx.fillRect(0.0, w, h);
    this.penColor += 0.05;
    this.penColor %= 360;
}
Copy the code

We use fillRect to cover the white transparent layer layer by layer, so as to achieve the effect of fading away. In addition, here we can make the color of the stroke gradually change.

However, the problem came, we found that although the color was light after drawing, it could not disappear for a long time, and its path track still made people look very ugly.

So, we need to improve the step method.

step() {
    window.requestAnimationFrame(this.step.bind(this))
    const {ctx, w, h} = this;
    ctx.globalCompositeOperation = "lighter";
    ctx.fillStyle = "rgba(255,255,255,.008)"
    ctx.fillRect(0.0, w, h);
    ctx.globalCompositeOperation = "source-over";
    this.penColor += 0.05;
    this.penColor %= 360;
}
Copy the code

As you can see, the globalCompositeOperation is used to do the blending and overlap contrast directly to eliminate the color value of the color track.

Now we have the desired result, a kaleidoscope paintbrush — online demo

conclusion

Through this case, some drawing offset skills, color value changes, the use of globalCompositeOperation, I believe you have your own new ideas, now just a kaleidoscope brush, we can also generate triangles and circles and other shapes to make a real kaleidoscope can also oh, Or multiple fu character door curtain can also be realized on this basis, what also hurriedly play their own creativity ~