Recently, a small program has been developed that addresses the need for dynamic effects. We originally planned to use a gif diagram to achieve this dynamic effect, but the gif diagram has three drawbacks:
- The gif map of high quality kinetics performance is at least 2MB in size.
- Too many dynamic details make it difficult to achieve a seamless cycle.
- Technical b is not high:).
- Source Address Welcome to issue
So the author started to use canvas to achieve dynamic effect, the first and most important step is to open github and search particle.After a five-minute search, it was found that implementing a particle system was an unprecedented pioneer.
em.... Let me give you a few reasons to explain this mindset of repeating wheels:
- Most repositories do not achieve what we want.
- Some repositories seem to be able to do what we want with demo, but there's nothing more than demo; just say what to do with it.
- Cannot be compatible with applets: Core code contains DOM API or only supports webgl mode.
Before we start repeating the wheel work, explain the origin of this wheel. I have finished a JavaScript version before developing the particle system below, but when I look back at the code I think the API design is unreasonable and not flexible enough, so I decided to rewrite one with TypeScript and read it during that time. egret-libs After the code, API design and particle launch control are optimized. Project Address
Object-oriented
Briefly, let's talk about the two things we need to abstract, ParticleSystem and Article.ParticleSystem borrows the world concept from the physical engine, which is the space in which particles exist, assuming that there are two properties in space, vertical acceleration of gravity and horizontal acceleration (horizontal wind).Particle is an object that exists in space. Objects have properties such as size, mass, speed, position, rotation angle, etc.
Particle class
Start simply by building a Particle class
class Particle { // life cycle public lifespan: number // speed public velocityX: number public velocityY: number // position public x: number public y: number // Time elapsed public currentTime: number // Particle size private _startSize: number // Scale public scale: number // End rotation angle public endRotation: number // Aspect ratio private ratio: number // Input image width and height private _width: number private _height: number // Particle texture public texture: CanvasImageSource set startSize (size: number) { this._startSize = size; this._width = size; this._height = size / this.ratio; } // Get Particle Size get startSize (): number { return this._startSize; } // Setting particle texture and texture width and height information public setTextureInfo (texture: CanvasImageSource, config: { width: number, height: number }) { this.texture = texture; this.ratio = config.width / config.height; } }
For length reasons, the code above shows the vast majority of the most important information.In fact, in the development process, the particle's attribute definition is not all-in-one, some attributes need to be filled up later, and some attributes find that the functions implemented are duplicated and need to be streamlined. Particle has many public properties and methods, but this is not open to developers. In fact, the entire Particle class is not developed for external use, and there is no need to manually instantiate this class when using it, because the entire system is designed to be highly coupled with Particle and ArticleSystem. There is a member method in the particle class, setTextureInfo, which sets the texture and width information of the particle, the first parameter of ctx.drawImage(..), which will be mentioned again later.The need to set width and height manually is based on compatibility considerations, although all compatibility can be written out here, it is ultimately decided that DOM API s should not be included as much as possible in the entire particle system, and that the best compatibility would be to leave the operation of obtaining picture attributes to the developer by passing in width and height information, focusing on core functions, and not implementing compatibility issues:).
ParticleSystem class
ParticleSystem class is undoubtedly the core of the particle system. The next step is to analyze its important functions.
constructor
Initialize the particle system with the incoming parameters
constructor ( texture: CanvasImageSource, textureInfo: { width: number, height: number }, config: string | any, ctx?: CanvasRenderingContext2D, canvasInfo?: { width: number, height: number } ) { if (canvasInfo) { this.canvasWidth = canvasInfo.width; this.canvasHeight = canvasInfo.height; } // Save canvas canvas this.ctx = ctx; // Save texture information this.changeTexture(texture, textureInfo); // Resolve and save configuration information this.changeConfig(config); // Create Particle Object Pool this.createParticlePool(); }
You can see from the constructor's parameters how to design the initialization API, and the reason why textureInfo was designed is explained above.ctx is optional, which is case-by-case, which is required when the particle system is required to complete the drawing of the canvas, and it is not necessary to pass in ctx when only the particle system is required to provide the drawing data.canvasInfo is also optional, and its function is the data needed by the particle system to empty the canvas, which is actually a compatibility parameter, which will be mentioned later.
Object pool
There are many "particle objects" when running a particle system, and a pool of objects is created to reduce the cost of creating and destroying particles during operation. Strictly speaking, object pools should be independent of ParticleSystems, but there is no need for reuse and you are too lazy to think about how separate systems should be designed, so you write object pools as built-in features of ParticleSystem. A simple object pool has three key properties and methods. pool:Array<Particle>Collection of available Particle objects addOneParticle(): Remove a particle from the object pool to join the rendered particle collection RemoveOneParticle: Removes a particle from the rendered particle collection and recycles it to the object pool ParticleList: Array<Particle>renders a collection of particles, which should not be included in a separate object pool design.
addOneParticle
private addOneParticle () { let particle: Particle; if (this.pool.length) { particle = this.pool.pop(); } else { particle = new Particle; } particle.setTextureInfo(this.texture, { width: this.textureWidth, height: this.textureHeight }) // Initialize newly removed particles this.initParticle(particle); this.particleList.push(particle); }
removeOneParticle
private removeOneParticle (particle: Particle) { let index: number = this.particleList.indexOf(particle); this.particleList.splice(index, 1); // Clear Texture Reference particle.texture = null; this.pool.push(particle); }
Particle State Initialization and Update
For the particle system to be more expressive, some properties of particles should be random, and in conjunction with API design, we encapsulate a function randRange(range) to obtain random data.
function randRange (range: number): number { range = Math.abs(range); return Math.random() * range * 2 - range; }
Simple physical knowledge is used for initializing and updating particle states, mainly calculating particle velocities and moving distances.
initParticle(particle)
The particle state initialization method sets the initial state of a particle, in which the randRange method described above is used to represent the random variation of individual particles.
private initParticle (particle: Particle): Particle { /* Other parameter initialization is omitted */ let angle = this.angle + randRange(this.angleVariance); // Velocity decomposition particle.velocityX = this.speed * Math.cos(angle); particle.velocityY = this.speed * Math.sin(angle); particle.startSize = this.startSize + randRange(this.startSizeVariance); // Scale ratio, which is used in subsequent calculations particle.scale = particle.startSize / this.startSize; }
updateParticle(particle)
public updateParticle (particle: Particle, dt: number) { // Time interval between uploading update status and this update dt = dt / 1000; // Speed and location updates particle.velocityX += this.gravityX * particle.scale * dt; particle.velocityY += this.gravityY * particle.scale * dt; particle.x += particle.velocityX * dt; particle.y += particle.velocityY * dt; }
Update Method
Defines an update method to control whether particles in a particle system should be added, deleted, and updated.
public update (dt: number) { // Need new particles if (!this.$stopping) { this.frameTime += dt; // this.frameTime records the difference between the time since the last particle was emitted and the interval between particle emission while (this.frameTime > 0) { if (this.particleList.length < this.maxParticles) { this.addOneParticle() } this.frameTime -= this.emissionRate; } } // Update particle status or remove particles let temp: Array<Particle> = [...this.particleList]; temp.forEach((particle: Particle) => { // Update the state of a particle if its life cycle is not over // Remove a particle if its life cycle has ended if (particle.currentTime < particle.lifespan) { this.updateParticle(particle, dt); particle.currentTime += dt; } else { this.removeOneParticle(particle); if (this.$stopping && this.particleList.length === 0) { this.$stopped = true; // Callback after complete stop of particle system // Later added functionality that may not be considered when first developed this.onstopped && this.onstopped(); } } }) }
The update method only involves data updates and is public, so it is designed to allow developers to control the particle drawing process by themselves after updating the drawing data, thereby embedding the particle system into existing programs.
Rendering and redrawing
Rendering here refers to the process of "drawing" data from a particle system onto a canvas canvas canvas canvas, and there are not many canvas API s available. If you want to involve texture rotation, you need to first understand what transform s are about the canvas canvas canvas canvas. Look here Portal.
public render (dt: number) { this.update(dt); this.draw(); // Compatible applets (<any>this.ctx).draw && (<any>this.ctx).draw(); } private draw () { this.particleList.forEach((particle: Particle) => { let { texture, x, y, width, height, alpha, rotation } = particle; let halfWidth = width / 2, halfHeight = height /2; // Save Canvas State this.ctx.save(); // Move the upper right corner of the canvas to the center of the texture this.ctx.translate(x + halfWidth, y + halfHeight); // Rotate canvas this.ctx.rotate(rotation); if (alpha !== 1) { this.ctx.globalAlpha = alpha; this.ctx.drawImage(texture, -halfWidth, -halfHeight, width, height); } else { this.ctx.drawImage(texture, -halfWidth, -halfHeight, width, height); } // Restore Canvas State this.ctx.restore(); }) }
Repainting involves two steps, time control and canvas redrawing, while canvas redrawing involves clearing the canvas and calling render.
// dt indicates the time difference between circular calls private circleDraw (dt: number) { if (this.$stopped) { return; } // This is also handled for compatibility with applets (look back at the constructor parameters above) let width: number, height: number; if (this.canvasWidth) { width = this.canvasWidth; height = this.canvasHeight; } else if (this.ctx.canvas) { width = this.ctx.canvas.width; height = this.ctx.canvas.width; } // Canvas redrawing this.ctx.clearRect(0, 0, width, height); this.render(dt); // time control // With simple compatibility processing, the requestAnimationFrame has better performance advantages. // Use setTimeout instead when not supported if (typeof requestAnimationFrame !== 'undefined') { requestAnimationFrame(() => { let now = Date.now(); // Calculate time difference this.circleDraw(now - this.lastTime); this.lastTime = now; }) } else { // The disadvantage of setTimeout is that the program's callback into the background will still be executed setTimeout(() => { let now = Date.now(); this.circleDraw(now - this.lastTime); this.lastTime = now; }, 17) } }
Start and Stop
Starting is very simple, just call circleDraw to start.The render method requires an incoming time difference, so this.lastTime is needed here to save the start and last redraw timestamps.
public start () { this.$stopping = false; if (!this.$stopped) { return; } this.lastTime = Date.now(); this.$stopped = false; this.circleDraw(0); } public stop () { this.$stopping = true; }
Examples of usage
If you follow the steps above or have read the source code of the project or written it once by yourself, there is no difficulty in the usage section. Here is an example of the basic usage.
import ParticleSystem from '../src/ParticleSystem' // Create canvas const canvas: HTMLCanvasElement = document.createElement('canvas'); canvas.width = (<Window>window).innerWidth; canvas.height = (<Window>window).innerHeight; document.body.appendChild(canvas); // Get Canvas Context const ctx: CanvasRenderingContext2D = canvas.getContext('2d'); // Load Texture const img: HTMLImageElement = document.createElement('img'); img.src = './test/texture.png'; img.onload = () => { // Create Particle System const particle = new ParticleSystem( // Texture Resources img, // Texture Size { width: img.width, height: img.height }, // Particle System Parameters { gravity: { x: 10, y: 80 }, emitterX: 200, emitterY: -10, emitterXVariance: 200, emitterYVariance: 10, maxParticles: 1, endRotation: 2, endRotationVariance: 50, speed: 50, angle: Math.PI / 2, angleVariance: Math.PI / 2, startSize: 15, startSizeVariance: 5, lifespan: 5000 }, // Canvas Context ctx ) particle.start(); }
On applet platforms, there may be performance issues that cause FPS to run at 15-60 There are large fluctuations between them.We can do this by separating computation from rendering.The general idea is that the particle system runs into a subthread worker, which is only responsible for the calculation of the particle position, sends the calculated data to the main thread, which calls the canvas-related API to complete the drawing of the canvas.You can try to do that.This idea has been used in the current project. FPS is 45-60 when the applet runs the particle system.
Look here demo
TODO
Adding "gravitational body" and "repulsive body" to a particle system can make the particle system more interactive by attracting and repelling the particle and changing its position at any time.Interested partners can try it out on their own.
[Author's introduction]: Ye Mao, Phragmites technology web front-end development engineer, representative works: Lipstick Challenge Net Red Mini game, service-side rendering of the official website.Good at website construction, public number development, WeChat applet development, games, public number development, focusing on front-end domain framework, interactive design, image drawing, data analysis and other research.Work side by side: yemao@talkmoney.cn Access www.talkmoney.cn Learn more