Double eleven Singles Day is coming again, this time of year is my most depressed moment. The thinner purse, the drier hands, the stronger, but the cooler head. Do something this year!

✧(≖ theus ≖) : one item for one item for a smooth item.

The first thing that comes to mind as a visualized front end siege lion is the particles they play with. So let’s put this particle system to good use. Demo address

Make text out of particles

First, let’s think about how a series of particles can be made into a confession.

In fact, the implementation principle is very simple, Canvas has a method getImageData, can get a rectangle range of all the pixel data. So let’s try to get the shape of a text.

First, calculate the appropriate size and position of the text using the measureText method.

// Create a canvas with the same scale as the canvas
const width = 100;
const height = ~~(width * this.height / this.width); // this.width, this.height specifies the size of the entire canvas
const offscreenCanvas = document.createElement('canvas');
const offscreenCanvasCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.setAttribute('width', width);
offscreenCanvas.setAttribute('height', height);

// After drawing the desired textAll on the off-screen canvas, calculate its appropriate size
offscreenCanvasCtx.fillStyle = '# 000';
offscreenCanvasCtx.font = 'bold 10px Arial';
const measure = offscreenCanvasCtx.measureText(textAll); // Measure text to get width
const size = 0.8;
// The width and height of the screen are 0.8 respectively
const fSize = Math.min(height * size * 10 / lineHeight, width * size * 10 / measure.width);  // lineHeight=7 magic
offscreenCanvasCtx.font = `bold ${fSize}px Arial`;

// Put the text in the appropriate position according to the calculated font size. The starting position of the text coordinate is at the lower left
const measureResize = offscreenCanvasCtx.measureText(textAll);
// The starting position of the text is in the lower left
let left = (width - measureResize.width) / 2;
const bottom = (height + fSize / 10 * lineHeight) / 2;
offscreenCanvasCtx.fillText(textAll, left, bottom);
Copy the code

We can appendChild into the body to see

Ok. Attention, class, I’m starting to transform.

The pixel data obtained by getImageData is A Uint8ClampedArray (an array of values ranging from 0 to 255) with 4 numbers in A set corresponding to the R, G, B, and A values of each pixel. We just need to determine that I * 4 + 3 is not 0 to get the font shape data we need.

// Texts are used in texts, and textAll is used in texts
Object.values(texts).forEach(item= > {
    offscreenCanvasCtx.clearRect(0.0, width, height);
    offscreenCanvasCtx.fillText(item.text, left, bottom);
    left += offscreenCanvasCtx.measureText(item.text).width;
    const data = offscreenCanvasCtx.getImageData(0.0, width, height);
    const points = [];
	// Determine whether the I * 4 + 3 bit is 0, and get the relative x and y coordinates (multiply by the actual length and width of the canvas, and take the y coordinate in reverse)
    for (let i = 0, max = data.width * data.height; i < max; i++) {
        if (data.data[i * 4 + 3]) {
            points.push({
                x: (i % data.width) / data.width,
                y: (i / data.width) / data.height }); }}// Save to an object for later drawing
    geometry.push({
        color: item.hsla,
        points
    });
})
Copy the code

Make the scene, draw the graphics

Text and graphics to get the way and done, so we can output the content as a whole. Let’s define a simple script format.

// HSLA format is easy to do later color change extension
const color1 = {h:197.s:'100%'.l:'50%'.a:'80%'};
const color2 = {h:197.s:'100%'.l:'50%'.a:'80%'};
/ / lifeTime shot number
const Actions = [
    {lifeTime:60.text: [{text:3.hsla:color1}]},
    {lifeTime:60.text: [{text:2.hsla:color1}]},
    {lifeTime:60.text: [{text:1.hsla:color1}]},
    {lifeTime:120.text:[
        {text:'I'.hsla:color1},
        {text:'❤ ️'.hsla:color2},
        {text:'Y'.hsla:color1},
        {text:'O'.hsla:color1},
        {text:'U'.hsla:color1}
    ]},
];
Copy the code

Parse out the graphics of each scene according to the preset script, and add a tick to determine whether lifeTime is the time to switch to the next graph to redraw the graph.

function draw() {
    this.tick++;
    if (this.tick >= this.actions[this.actionIndex].lifeTime) {
        this.nextAction();
    }
    this.clear();
    this.renderParticles(); / / draw point
    this.raf = requestAnimationFrame(this.draw);
}

function nextAction() {...// Switch scene balabala..
    this.setParticle(); // Set the points randomly to the previously obtained action.geometry. Points
}
Copy the code

So our basic function is done.

Can you give me a little more power

The particle system, now just context. Arc simply draws a point. So let’s add a particle system.

class PARTICLE {
    X,y, and z are the current coordinates, and vx,vy, and vz are the velocities in the three directions
    constructor(center) {
        this.center = center;
        this.x = 0;
        this.y = 0;
        this.z = 0;
        this.vx = 0;
        this.vy = 0;
        this.vz = 0;
    }
    // Set the destination to which the particles need to move (next position)
    setAxis(axis) {
        this.nextX = axis.x;
        this.nextY = axis.y;
        this.nextZ = axis.z;
        this.color = axis.color;
    }
    step() {
        // The further the elastic model is from the target, the faster it will be
        this.vx += (this.nextX - this.x) * SPRING;
        this.vy += (this.nextY - this.y) * SPRING;
        this.vz += (this.nextZ - this.z) * SPRING;
		// The coefficient of friction allows the particles to stabilize
        this.vx *= FRICTION;
        this.vy *= FRICTION;
        this.vz *= FRICTION;
		
        this.x += this.vx;
        this.y += this.vy;
        this.z += this.vz;
    }
    getAxis2D() {
        this.step();
        // 2D offset in 3D coordinates. For the time being, consider only the position, not the size change
        const scale = FOCUS_POSITION / (FOCUS_POSITION + this.z);
        return {
            x: this.center.x + (this.x * scale),
            y: this.center.y - (this.y * scale), }; }}Copy the code

And we’re done!

Since it is a 3D particle, in fact, there are not articles to do, students can use their imagination to something more cool.

What else is there to see

Above, the particles are laid out as text. Well, we could just write the formula and pose for it.

// In texts, use func instead of Actions
{
    lifeTime: 100.func: (radius) = > {
        const i = Math.random() * 1200;
        let x = (i - 1200 / 2) / 300;
        let y = Math.sqrt(Math.abs(x)) - Math.sqrt(Math.cos(x)) * Math.cos(30 * x);
        return {
            x: x * radius / 2.y: y * radius / 2.z: ~ ~ (Math.random() * 30),
            color: color3 }; }}Copy the code

Let’s use the text conversion method again

{
    lifeTime: Infinity.func: (width, height) = > {
            if(! points.length){const img = document.getElementById("tulip");
                const offscreenCanvas = document.createElement('canvas');
                const offscreenCanvasCtx = offscreenCanvas.getContext('2d');
                const imgWidth = 200;
                const imgHeight = 200;
                offscreenCanvas.setAttribute('width', imgWidth);
                offscreenCanvas.setAttribute('height', imgHeight);
                offscreenCanvasCtx.drawImage(img, 0.0, imgWidth, imgHeight);
                let imgData = offscreenCanvasCtx.getImageData(0.0, imgWidth, imgHeight);
                for (let i = 0, max = imgData.width * imgData.height; i < max; i++) {
                    if (imgData.data[i * 4 + 3]) {
                        points.push({
                            x: (i % imgData.width) / imgData.width,
                            y: (i / imgData.width) / imgData.height }); }}}const p = points[~~(Math.random() * points.length)]
            const radius = Math.min(width * 0.8, height * 0.8);
            return {
                x: p.x * radius - radius / 2.y: (1 - p.y) * radius - radius / 2.z: ~ ~ (Math.random() * 30),
                color: color3 }; }}Copy the code

Perfect 😝.

Then we can draw an image using drawImage instead of arc.

Wait!! In front of the effect always feel something wrong, as if some card.

Optimization tips

  1. Layered. If you need to add some other content to the Canvas, consider splitting multiple Canvas to do it.
  2. Reduce property Settings. Include lineWidth, fillStyle, and so on. Canvas context is a very complex object, and it takes a lot of performance when you set some of its properties. Arc and other drawing methods should also be reduced.
  3. Draw off screen. I don’t need arc to draw points. The delay above is that fillStyle and ARC are called every time a point is drawn. But when we draw a drawImage, the performance is much better. DrawImage can draw another Canvas in addition to the image directly, so we can draw the dots on a Canvas that is not on the screen in advance.
  4. To reduce js computations and avoid clogging processes, use web workers. Of course, our current calculations don’t use this at all.

I used 3000 particles here to compare the frame rate before and after I used off-screen drawing.

conclusion

The only limit you have now is your imagination. Let’s conquer the boss, conquer the goddess!

This Double eleven is out of poverty, no hair loss!

Well don’t say, the goddess asked me to fix the computer.

reference

  • deformable particles

  • heart.png

  • Canvas Reference Manual

  • y=sqrt(abs(x))-sqrt(cos(x))*cos(40x)

The article can be reproduced at will, but please keep the original link. If you are passionate enough to join ES2049 Studio, please send your resume to caijun.hcj(at)alibaba-inc.com.