[[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. - define(function(require, exports, module) {
- var Util = require ('./Util');
- var Launcher = require ('./Launcher');
-
- /**
- * World constructor
- * @param config
- * backgroundImage background image
- * canvas canvas reference
- * context canvas context
- *
- * time world time
- *
- * gravity
- *
- * heat
- * heatEnable thermal switch
- * minHeat random minimum heat
- * maxHeat Random maximum heat
- *
- * wind
- * windEnable wind switch
- * minWind Random minimum wind speed
- * maxWind Random maximum wind speed
- *
- * timeProgress time progress unit, used to control time speed
- * launchers The launcher queue belonging to this world
- * @constructor
- */
- function World(config){
- //Too long, details omitted
- }
- World.prototype.updateStatus = function (){};
- World.prototype.timeTick = function (){};
- World.prototype.createLauncher = function (config){};
- World.prototype.drawBackground = function (){};
- module.exports = World ;
- });
As we all know, animation means continuous redrawing, so we need to expose a method for external loop calls: - /**
- * Loop trigger function
- * Triggered when the conditions are met
- * For example, RequestAnimationFrame callback, or loop triggering after setTimeout callback
- * Used to maintain the life of the World
- */
-
- World.prototype.timeTick = function (){
-
- //Update various world states
- this.updateStatus();
-
- this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
- this.drawBackground();
-
- //Trigger the loop call function for all emitters
- for(var i = 0 ;i < this.launchers.length ;i++){
- this.launchers[i].updateLauncherStatus();
- this.launchers[i].createGrain(1);
- this.launchers[i].paintGrain();
- }
- };
This timeTick method does the following each time it is called in the outer loop: - Update the world state
- Clear the canvas and redraw the background
- 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~ - World.prototype.updateStatus = function (){
- this.time+=this.timeProgress;
- this.wind = Util .randomFloat(this.minWind,this.maxWind);
- this.heat = Util .randomFloat(this.minHeat,this.maxHeat);
- };
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? - World.prototype.createLauncher = function(config){
- var _launcher = new Launcher(config);
- this .launchers.push(_launcher);
- };
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~ - define(function (require, exports, module) {
- var Util = require ('./Util');
- var Grain = require ('./Grain');
-
- /**
- * Emitter constructor
- * @param config
- * id identity is used for subsequent maintenance of the visual editor
- * world The host of this launcher
- *
- * grainImage particle image
- * grainList particle queue
- * grainLife The life of the particles produced
- * grainLifeRange particle life fluctuation range
- * maxAliveCount Maximum number of surviving particles
- *
- * x Transmitter position x
- * y emitter position y
- * rangeX transmitter position x fluctuation range
- * rangeY The y fluctuation range of the transmitter position
- *
- * sizeX: particle horizontal size
- * sizeY: particle vertical size
- * sizeRange Particle size fluctuation range
- *
- * mass particle mass (not useful for now)
- * massRange particle mass fluctuation range
- *
- * heat The heat of the transmitter's own system
- * heatEnable The heat enable switch of the transmitter's own system
- * minHeat Minimum value of random heat
- * maxHeat Minimum value of random heat
- *
- * wind The wind force of the transmitter itself
- * windEnable Transmitter's own system wind power enable switch
- * minWind Minimum random wind speed
- * maxWind Minimum random wind speed
- *
- * grainInfluencedByWorldWind Particles are affected by world wind force switch
- * grainInfluencedByWorldHeat Particles are affected by world heat.
- * grainInfluencedByWorldGravity Particles are affected by world gravity switch
- *
- * grainInfluencedByLauncherWind Particles are affected by the wind of the launcher
- * grainInfluencedByLauncherHeat Particles are affected by the heat of the emitter
- *
- * @constructor
- */
-
- function Launcher(config) {
- //Too long, details omitted
- }
-
- Launcher.prototype.updateLauncherStatus = function () {};
- Launcher.prototype.swipeDeadGrain = function (grain_id) {};
- Launcher.prototype.createGrain = function (count) {};
- Launcher.prototype.paintGrain = function () {};
-
- module.exports = Launcher ;
-
- });
The transmitter is responsible for giving birth to the baby. How does it do that? - Launcher.prototype.createGrain = function (count) {
- if (count + this.grainList.length < = this.maxAliveCount) {
- //The count of new ones added together with the old ones have not reached the maximum limit
- } else if (this.grainList.length > = this.maxAliveCount &&
- count + this.grainList.length > this.maxAliveCount) {
- //The number of old particles alone has not reached the maximum limit
- //The number of newly created count items plus the old ones exceeds the maximum limit
- count = this .maxAliveCount - this.grainList.length;
- } else {
- count = 0 ;
- }
- for (var i = 0 ; i < count ; i++) {
- var _rd = Util .randomFloat(0, Math.PI * 2);
- var _grain = new Grain({/*particle configuration*/});
- this.grainList.push(_grain);
- }
- };
After giving birth, I still have to clean up after the baby dies... (So sad, I blame it on insufficient memory) - Launcher.prototype.swipeDeadGrain = function (grain_id) {
- for (var i = 0 ; i < this.grainList.length ; i++) {
- if ( grain_id == this.grainList[i].id) {
- this this.grainList = this.grainList.remove(i); //remove is a self-defined Array method
- this.createGrain(1);
- break;
- }
- }
- };
After giving birth, you have to let the child out to play: - Launcher.prototype.paintGrain = function () {
- for (var i = 0 ; i < this.grainList.length ; i++) {
- this.grainList[i].paint();
- }
- };
Don’t forget to maintain your own little inner world~ (similar to the big world outside) - Launcher.prototype.updateLauncherStatus = function () {
- if (this.grainInfluencedByLauncherWind) {
- this.wind = Util .randomFloat(this.minWind, this.maxWind);
- }
- if(this.grainInfluencedByLauncherHeat){
- this.heat = Util .randomFloat(this.minHeat, this.maxHeat);
- }
- };
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. - define(function (require, exports, module) {
- var Util = require ('./Util');
-
- /**
- * Particle constructor
- * @param config
- * id unique identifier
- * world world host
- * launcher launcher host
- *
- * x position x
- * y position y
- * vx horizontal speed
- * vy vertical speed
- *
- * sizeX horizontal size
- * sizeY vertical size
- *
- * mass
- * life life span
- * birthTime birth time
- *
- * color_r
- * color_g
- * color_b
- * alpha transparency
- * initAlpha initialization transparency
- *
- * influencedByWorldWind
- * influencedByWorldHeat
- * influencedByWorldGravity
- * influencedByLauncherWind
- * influencedByLauncherHeat
- *
- * @constructor
- */
- function Grain(config) {
- //Too long, details omitted
- }
-
- Grain.prototype.isDead = function () {};
- Grain.prototype.calculate = function () {};
- Grain.prototype.paint = function () {};
- module.exports = Grain ;
- });
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:-) - Grain.prototype.calculate = function () {
- //Calculate the position
- if (this.influencedByWorldGravity) {
- this.vy += this.world.gravity+Util.randomFloat(0,0.3*this.world.gravity);
- }
- if (this.influencedByWorldHeat && this.world.heatEnable) {
- this.vy - = this .world.heat+Util.randomFloat(0,0.3*this.world.heat);
- }
- if (this.influencedByLauncherHeat && this.launcher.heatEnable) {
- this.vy - = this .launcher.heat+Util.randomFloat(0,0.3*this.launcher.heat);
- }
- if (this.influencedByWorldWind && this.world.windEnable) {
- this.vx += this.world.wind+Util.randomFloat(0,0.3*this.world.wind);
- }
- if (this.influencedByLauncherWind && this.launcher.windEnable) {
- this.vx += this.launcher.wind+Util.randomFloat(0,0.3*this.launcher.wind);
- }
- this.y += this.vy;
- this.x += this.vx;
- this this.alpha = this.initAlpha * (1 - (this.world.time - this.birthTime) / this.life);
-
- //TODO calculate color and other
-
- };
How do particles know if they are dead? - Grain.prototype.isDead = function () {
- return Math.abs(this.world.time - this.birthTime) > this.life;
- };
How should the particles present themselves? - Grain.prototype.paint = function () {
- if (this.isDead()) {
- this.launcher.swipeDeadGrain(this.id);
- } else {
- this.calculate();
- this.world.context.save();
- this.world.context.globalCompositeOperation = 'lighter' ;
- this this.world.context.globalAlpha = this.alpha;
- this.world.context.drawImage(this.launcher.grainImage, this.x-(this.sizeX)/2, this.y-(this.sizeY)/2, this.sizeX, this.sizeY);
- this.world.context.restore();
- }
- };
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 |