[Tencent Bugly Tips] Canvas Particle Engine Hands-on Tutorial to Impress Leaders and Users

[Tencent Bugly Tips] Canvas Particle Engine Hands-on Tutorial to Impress Leaders and Users

[[165460]]

Preface

Well, calling it a "particle engine" is a bit of a headline, and it's still a bit far from a real particle engine. Let's stop talking nonsense and watch [demo] first. After scanning, click on the screen for a surprise...

This article will teach you how to make a simple canvas particle maker (hereinafter referred to as the engine).

Worldview

This simple engine requires three elements: world, launcher, and grain. In general, the launcher exists in the world, creates particles, and the world and the launcher affect the state of the particles. After being affected by the world and the launcher, each particle calculates its position at the next moment and draws itself.

World

The so-called "world" is the environment that globally affects the particles that exist in this "world". If a particle chooses to exist in this "world", then this particle will be affected by this "world".

Launcher

Units used to emit particles. They can control various properties of particles generated by particles. As the parents of particles, emitters can control the birth properties of particles: birth position, birth size, lifespan, whether it is affected by the "World", whether it is affected by the "Launcher" itself, etc...

In addition, the emitter itself must clean up the dead particles it produces.

Grain

The smallest basic unit is each individual in the commotion. Each individual has its own location, size, lifespan, whether it is affected by the same name, and other properties, so that their form can be accurately depicted on the canvas at all times.

Particle drawing main logic

The above is the main logic of particle drawing.

Let’s first look at what the world needs.

Creating a World

I don't know why I naturally thought that the world should have gravity acceleration. But gravity acceleration alone can't show many tricks, so here I added two other influencing factors: heat and wind. Gravity acceleration and heat are in vertical directions, and wind affects the direction horizontally. With these three things, we can make particles move very coquettishly.

The maintenance of some states (such as the existence of particles) requires a time mark, so let's add time to the world, so that it will be convenient to make time pause and reverse flow effects later.

  1. define(function(require, exports, module) {
  2. var Util = require ('./Util');
  3. var Launcher = require ('./Launcher');
  4.  
  5. /**
  6. * World constructor
  7. * @param config
  8. * backgroundImage background image
  9. * canvas canvas reference
  10. * context canvas context
  11. *
  12. * time world time
  13. *
  14. * gravity
  15. *
  16. * heat
  17. * heatEnable thermal switch
  18. * minHeat random minimum heat
  19. * maxHeat Random maximum heat
  20. *
  21. * wind
  22. * windEnable wind switch
  23. * minWind Random minimum wind speed
  24. * maxWind Random maximum wind speed
  25. *
  26. * timeProgress time progress unit, used to control time speed
  27. * launchers The launcher queue belonging to this world
  28. * @constructor
  29. */
  30. function World(config){
  31. //Too long, details omitted
  32. }
  33. World.prototype.updateStatus = function (){};
  34. World.prototype.timeTick = function (){};
  35. World.prototype.createLauncher = function (config){};
  36. World.prototype.drawBackground = function (){};
  37. module.exports = World ;
  38. });

As we all know, animation means continuous redrawing, so we need to expose a method for external loop calls:

  1. /**
  2. * Loop trigger function
  3. * Triggered when the conditions are met
  4. * For example, RequestAnimationFrame callback, or loop triggering after setTimeout callback
  5. * Used to maintain the life of the World
  6. */
  7.  
  8. World.prototype.timeTick = function (){
  9.  
  10. //Update various world states
  11. this.updateStatus();
  12.  
  13. this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
  14. this.drawBackground();
  15.  
  16. //Trigger the loop call function for all emitters
  17. for(var i = 0 ;i < this.launchers.length ;i++){
  18. this.launchers[i].updateLauncherStatus();
  19. this.launchers[i].createGrain(1);
  20. this.launchers[i].paintGrain();
  21. }
  22. };

This timeTick method does the following each time it is called in the outer loop:

  1. Update the world state
  2. Clear the canvas and redraw the background
  3. Poll all emitters in the world and update their status, create new particles, and draw particles

So, what exactly needs to be updated in the state of the world?

Obviously, it is easy to think that we need to add a little time forward each time. Secondly, in order to make the particles move as much as possible, we keep the wind and heat states unstable - every gust of wind and every heat wave are not noticeable to you~

  1. World.prototype.updateStatus = function (){
  2. this.time+=this.timeProgress;
  3. this.wind = Util .randomFloat(this.minWind,this.maxWind);
  4. this.heat = Util .randomFloat(this.minHeat,this.maxHeat);
  5. };

Now that the world is created, we have to make it possible for the world to create particle emitters. Otherwise, how can we create particles?

  1. World.prototype.createLauncher = function(config){
  2. var _launcher = new Launcher(config);
  3. this .launchers.push(_launcher);
  4. };

Well, as God, we have almost created the world, and the next step is to create all kinds of creatures.

Making the First Creature: The Launcher

The emitter is the first organism in the world, and it is through the emitter that all kinds of strange particles can be reproduced. So what characteristics does the emitter need to have?

First of all, we have to figure out which world it belongs to (because there may be more than one world).

Secondly, it is the state of the transmitter itself: position, wind force and heat within its own system. It can be said that the transmitter is a small world within a world.

Finally, let's describe his "genes". The genes of the emitter will affect their offspring (particles). The more "genes" we give to the emitter, the more biological characteristics their offspring will have. Please see the conscientious comment code below for details~

  1. define(function (require, exports, module) {
  2. var Util = require ('./Util');
  3. var Grain = require ('./Grain');
  4.  
  5. /**
  6. * Emitter constructor
  7. * @param config
  8. * id identity is used for subsequent maintenance of the visual editor
  9. * world The host of this launcher
  10. *
  11. * grainImage particle image
  12. * grainList particle queue
  13. * grainLife The life of the particles produced
  14. * grainLifeRange particle life fluctuation range
  15. * maxAliveCount Maximum number of surviving particles
  16. *
  17. * x Transmitter position x
  18. * y emitter position y
  19. * rangeX transmitter position x fluctuation range
  20. * rangeY The y fluctuation range of the transmitter position
  21. *
  22. * sizeX: particle horizontal size
  23. * sizeY: particle vertical size
  24. * sizeRange Particle size fluctuation range
  25. *
  26. * mass particle mass (not useful for now)
  27. * massRange particle mass fluctuation range
  28. *
  29. * heat The heat of the transmitter's own system
  30. * heatEnable The heat enable switch of the transmitter's own system
  31. * minHeat Minimum value of random heat
  32. * maxHeat Minimum value of random heat
  33. *
  34. * wind The wind force of the transmitter itself
  35. * windEnable Transmitter's own system wind power enable switch
  36. * minWind Minimum random wind speed
  37. * maxWind Minimum random wind speed
  38. *
  39. * grainInfluencedByWorldWind Particles are affected by world wind force switch
  40. * grainInfluencedByWorldHeat Particles are affected by world heat.
  41. * grainInfluencedByWorldGravity Particles are affected by world gravity switch
  42. *
  43. * grainInfluencedByLauncherWind Particles are affected by the wind of the launcher
  44. * grainInfluencedByLauncherHeat Particles are affected by the heat of the emitter
  45. *
  46. * @constructor
  47. */
  48.  
  49. function Launcher(config) {
  50. //Too long, details omitted
  51. }
  52.  
  53. Launcher.prototype.updateLauncherStatus = function () {};
  54. Launcher.prototype.swipeDeadGrain = function (grain_id) {};
  55. Launcher.prototype.createGrain = function (count) {};
  56. Launcher.prototype.paintGrain = function () {};
  57.  
  58. module.exports = Launcher ;
  59.  
  60. });

The transmitter is responsible for giving birth to the baby. How does it do that?

  1. Launcher.prototype.createGrain = function (count) {
  2. if (count + this.grainList.length < = this.maxAliveCount) {
  3. //The count of new ones added together with the old ones have not reached the maximum limit
  4. } else if (this.grainList.length > = this.maxAliveCount &&
  5. count + this.grainList.length > this.maxAliveCount) {
  6. //The number of old particles alone has not reached the maximum limit
  7. //The number of newly created count items plus the old ones exceeds the maximum limit
  8. count = this .maxAliveCount - this.grainList.length;
  9. } else {
  10. count = 0 ;
  11. }
  12. for (var i = 0 ; i <   count ; i++) {
  13. var _rd = Util .randomFloat(0, Math.PI * 2);
  14. var _grain = new Grain({/*particle configuration*/});
  15. this.grainList.push(_grain);
  16. }
  17. };

After giving birth, I still have to clean up after the baby dies... (So sad, I blame it on insufficient memory)

  1. Launcher.prototype.swipeDeadGrain = function (grain_id) {
  2. for (var i = 0 ; i <   this.grainList.length ; i++) {
  3. if ( grain_id == this.grainList[i].id) {
  4. this this.grainList = this.grainList.remove(i); //remove is a self-defined Array method
  5. this.createGrain(1);
  6. break;
  7. }
  8. }
  9. };

After giving birth, you have to let the child out to play:

  1. Launcher.prototype.paintGrain = function () {
  2. for (var i = 0 ; i <   this.grainList.length ; i++) {
  3. this.grainList[i].paint();
  4. }
  5. };

Don’t forget to maintain your own little inner world~ (similar to the big world outside)

  1. Launcher.prototype.updateLauncherStatus = function () {
  2. if (this.grainInfluencedByLauncherWind) {
  3. this.wind = Util .randomFloat(this.minWind, this.maxWind);
  4. }
  5. if(this.grainInfluencedByLauncherHeat){
  6. this.heat = Util .randomFloat(this.minHeat, this.maxHeat);
  7. }
  8. };

Well, at this point, we have completed the creation of the world's first creature, and the next step is their descendants (whoosh, God is so tired)

Descendants and grandchildren, endless

Come out, little ones, you are the protagonists of the world!

As the protagonists of the world, particles have various states of their own: position, speed, size, life span, and birth time are of course indispensable.

  1. define(function (require, exports, module) {
  2. var Util = require ('./Util');
  3.  
  4. /**
  5. * Particle constructor
  6. * @param config
  7. * id unique identifier
  8. * world world host
  9. * launcher launcher host
  10. *
  11. * x position x
  12. * y position y
  13. * vx horizontal speed
  14. * vy vertical speed
  15. *
  16. * sizeX horizontal size
  17. * sizeY vertical size
  18. *
  19. * mass
  20. * life life span
  21. * birthTime birth time
  22. *
  23. * color_r
  24. * color_g
  25. * color_b
  26. * alpha transparency
  27. * initAlpha initialization transparency
  28. *
  29. * influencedByWorldWind
  30. * influencedByWorldHeat
  31. * influencedByWorldGravity
  32. * influencedByLauncherWind
  33. * influencedByLauncherHeat
  34. *
  35. * @constructor
  36. */
  37. function Grain(config) {
  38. //Too long, details omitted
  39. }
  40.  
  41. Grain.prototype.isDead = function () {};
  42. Grain.prototype.calculate = function () {};
  43. Grain.prototype.paint = function () {};
  44. module.exports = Grain ;
  45. });

Particles need to know what they will be like in the next moment, so that they can show themselves in the world. As for the state of motion, of course, it is all knowledge from junior high school physics:-)

  1. Grain.prototype.calculate = function () {
  2. //Calculate the position
  3. if (this.influencedByWorldGravity) {
  4. this.vy += this.world.gravity+Util.randomFloat(0,0.3*this.world.gravity);
  5. }
  6. if (this.influencedByWorldHeat && this.world.heatEnable) {
  7. this.vy - = this .world.heat+Util.randomFloat(0,0.3*this.world.heat);
  8. }
  9. if (this.influencedByLauncherHeat && this.launcher.heatEnable) {
  10. this.vy - = this .launcher.heat+Util.randomFloat(0,0.3*this.launcher.heat);
  11. }
  12. if (this.influencedByWorldWind && this.world.windEnable) {
  13. this.vx += this.world.wind+Util.randomFloat(0,0.3*this.world.wind);
  14. }
  15. if (this.influencedByLauncherWind && this.launcher.windEnable) {
  16. this.vx += this.launcher.wind+Util.randomFloat(0,0.3*this.launcher.wind);
  17. }
  18. this.y += this.vy;
  19. this.x += this.vx;
  20. this this.alpha = this.initAlpha * (1 - (this.world.time - this.birthTime) / this.life);
  21.  
  22. //TODO calculate color and other
  23.  
  24. };

How do particles know if they are dead?

  1. Grain.prototype.isDead = function () {
  2. return Math.abs(this.world.time - this.birthTime) > this.life;
  3. };

How should the particles present themselves?

  1. Grain.prototype.paint = function () {
  2. if (this.isDead()) {
  3. this.launcher.swipeDeadGrain(this.id);
  4. } else {
  5. this.calculate();
  6. this.world.context.save();
  7. this.world.context.globalCompositeOperation = 'lighter' ;
  8. this this.world.context.globalAlpha = this.alpha;
  9. this.world.context.drawImage(this.launcher.grainImage, this.x-(this.sizeX)/2, this.y-(this.sizeY)/2, this.sizeX, this.sizeY);
  10. this.world.context.restore();
  11. }
  12. };

Alas.

Follow-up

In the future, we hope to expand on this prototype and create a visual editor for everyone to use.

By the way, the code is here: https://github.com/jation/CanvasGrain

<<:  VR Chronicles: What did VR devices look like 50 years ago?

>>:  【Tencent Bugly Tips】WeChat Files

Recommend

Can alcohol be used to clean mobile phone screens?

During the epidemic, various disinfectants and cl...

The latest ranking of 50 information flow advertising media platforms!

Friends who are involved in advertising know that...

Analysis of Xiaohongshu’s competitive products!

Competitive product analysis is almost a compulso...

Traditional home appliances innovation cuts off the "insulation layer"

Whether it was voluntary or induced, under He Xia...

How to create a good word-of-mouth effect for your App?

App promotion is constantly innovating, and the e...

Your farts contain information about your health!

Farting is generally a good thing, as the accumul...

Analysis of relationship marketing structure and case interpretation

During the development process of an enterprise, ...

How painful is it to be overly sensitive in this part of the body?

appendix: 1. ——I really want to chop off my nose!...

Mosquito warning! I love people wearing these four colors the most

The picture material comes from the Internet Mosq...