HTML 代码:
<div></div>
CSS 代码:
body { background-image: linear-gradient(6deg, #214, #000); background-size: 100% 100%;overflow: hidden } canvas { display: block; }
JS 代码:
class Vector2 { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(v) { this.x += v.x; this.y += v.y; return this; } multiplyScalar(s) { this.x *= s; this.y *= s; return this; } clone() { return new Vector2(this.x, this.y); }} class Time { constructor() { const now = Time.now(); this.delta = 0; this.elapsed = 0; this.start = now; this.previous = now; } update() { const now = Time.now(); this.delta = now - this.previous; this.elapsed = now - this.start; this.previous = now; } static now() { return Date.now() / 1000; }} class Particle { constructor(position, velocity = new Vector2(), color = 'white', radius = 1, lifetime = 1, mass = 1) { this.position = position; this.velocity = velocity; this.color = color; this.radius = radius; this.lifetime = lifetime; this.mass = mass; this.isInCanvas = true; this.createdOn = Time.now(); } update(time) { if (!this.getRemainingLifetime()) { return; } this.velocity.add(Particle.GRAVITATION.clone().multiplyScalar(this.mass)); this.position.add(this.velocity.clone().multiplyScalar(time.delta)); } render(canvas, context) { const remainingLifetime = this.getRemainingLifetime(); if (!remainingLifetime) return; const radius = this.radius * remainingLifetime; context.globalAlpha = remainingLifetime; context.globalCompositeOperation = 'lighter'; context.fillStyle = this.color; context.beginPath(); context.arc(this.position.x, this.position.y, radius, 0, Math.PI * 2); context.fill(); } getRemainingLifetime() { const elapsedLifetime = Time.now() - this.createdOn; return Math.max(0, this.lifetime - elapsedLifetime) / this.lifetime; }} Particle.GRAVITATION = new Vector2(0, 9.81); class Trail extends Particle { constructor(childFactory, position, velocity = new Vector2(), lifetime = 1, mass = 1) { super(position, velocity); this.childFactory = childFactory; this.children = []; this.lifetime = lifetime; this.mass = mass; this.isAlive = true; } update(time) { super.update(time); // Add a new child on every frame if (this.isAlive && this.getRemainingLifetime()) { this.children.push(this.childFactory(this)); } // Remove particles that are dead this.children = this.children.filter(function (child) { if (child instanceof Trail) { return child.isAlive; } return child.getRemainingLifetime(); }); // Kill trail if all particles fade away if (!this.children.length) { this.isAlive = false; } // Update particles this.children.forEach(function (child) { child.update(time); }); } render(canvas, context) { // Render all children this.children.forEach(function (child) { child.render(canvas, context); }); }} class Rocket extends Trail { constructor(childFactory, explosionFactory, position, velocity = new Vector2()) { super(childFactory, position, velocity); this.explosionFactory = explosionFactory; this.lifetime = 10; } update(time) { if (this.getRemainingLifetime() && this.velocity.y > 0) { this.explosionFactory(this); this.lifetime = 0; } super.update(time); }} const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const time = new Time(); let rockets = []; const getTrustParticleFactory = function (baseHue) { function getColor() { const hue = Math.floor(Math.random() * 15 + 30); return `hsl(${hue}, 100%, 75%`; } return function (parent) { const position = this.position.clone(); const velocity = this.velocity.clone().multiplyScalar(-.1); velocity.x += (Math.random() - .5) * 8; const color = getColor(); const radius = 1 + Math.random(); const lifetime = .5 + Math.random() * .5; const mass = .01; return new Particle(position, velocity, color, radius, lifetime, mass); }; }; const getExplosionFactory = function (baseHue) { function getColor() { const hue = Math.floor(baseHue + Math.random() * 15) % 360; const lightness = Math.floor(Math.pow(Math.random(), 2) * 50 + 50); return `hsl(${hue}, 100%, ${lightness}%`; } function getChildFactory() { return function (parent) { const direction = Math.random() * Math.PI * 2; const force = 8; const velocity = new Vector2(Math.cos(direction) * force, Math.sin(direction) * force); const color = getColor(); const radius = 1 + Math.random(); const lifetime = 1; const mass = .1; return new Particle(parent.position.clone(), velocity, color, radius, lifetime, mass); }; } function getTrail(position) { const direction = Math.random() * Math.PI * 2; const force = Math.random() * 128; const velocity = new Vector2(Math.cos(direction) * force, Math.sin(direction) * force); const lifetime = .5 + Math.random(); const mass = .075; return new Trail(getChildFactory(), position, velocity, lifetime, mass); } return function (parent) { let trails = 32; while (trails--) { parent.children.push(getTrail(parent.position.clone())); } }; }; const addRocket = function () { const trustParticleFactory = getTrustParticleFactory(); const explosionFactory = getExplosionFactory(Math.random() * 360); const position = new Vector2(Math.random() * canvas.width, canvas.height); const thrust = window.innerHeight * .75; const angle = Math.PI / -2 + (Math.random() - .5) * Math.PI / 8; const velocity = new Vector2(Math.cos(angle) * thrust, Math.sin(angle) * thrust); const lifetime = 3; rockets.push(new Rocket(trustParticleFactory, explosionFactory, position, velocity, lifetime)); rockets = rockets.filter(function (rocket) { return rocket.isAlive; }); }; const render = function () { requestAnimationFrame(render); time.update(); context.clearRect(0, 0, canvas.width, canvas.height); rockets.forEach(function (rocket) { rocket.update(time); rocket.render(canvas, context); }); }; const resize = function () { canvas.height = window.innerHeight; canvas.width = window.innerWidth; }; canvas.onclick = addRocket; document.body.appendChild(canvas); window.onresize = resize; resize(); setInterval(addRocket, 2000); render();