refactor: particle physics :^)

This commit is contained in:
cha0s 2019-11-03 10:41:23 -06:00
parent 6a348fc693
commit 1f14ca546b
10 changed files with 209 additions and 312 deletions

View File

@ -12,8 +12,6 @@ export {Renderable} from './renderable';
export {Renderer} from './renderer';
export {Sprite} from './sprite';
export {Stage} from './stage';
// Proton.
export {Proton, TextNode, TextNodeRenderer} from './proton';
// Pixelly!
settings.SCALE_MODE = SCALE_MODES.NEAREST;
// Lil pixi manegement.

View File

@ -1,2 +0,0 @@
export {Proton} from './proton';
export {TextNode, TextNodeRenderer} from './text-node-renderer';

View File

@ -1,19 +0,0 @@
// Fake for server.
class FakeProton {
addEmitter() {}
update() {}
}
FakeProton.BaseRender = class {};
FakeProton.Emitter = class {};
const Proton_ = 'undefined' !== typeof window ? require('three.proton.js/build/three.proton') : FakeProton;
export class Proton extends Proton_ {
tick(elapsed) {
this.update(elapsed);
}
}

View File

@ -1,169 +0,0 @@
'undefined' !== typeof window ? require('web-animations-js') : '';
import {Proton} from './proton';
import {Vector} from '@avocado/math';
export class TextNode {
constructor(text) {
this.text = text;
}
applyCssRule(rule) {
for (const attribute in rule) {
const value = rule[attribute];
this.div.style[attribute] = value;
}
}
copyFrom(other) {
this.span.className = other.spanClassName();
this.span.textContent = other.text;
this.span.style.fontSize = `${other.sizeInPx()}px`;
}
createDom() {
const div = window.document.createElement('div');
div.className = 'particle';
const span = window.document.createElement('span');
this.span = span;
div.appendChild(span);
this.div = div;
}
sizeInPx() {
return 12;
}
spanClassName() {
return 'text';
}
particleStateToCssRule(particle) {
const color = particle.color;
// Position.
const transforms = [];
const position = [particle.p.x, particle.p.y];
const scale = particle.scale;
// Offset for font size.
const {style, textContent} = particle.target.span;
const fontSize = this.sizeInPx();
const length = textContent.length;
const offset = (fontSize / 2) * (particle.scale / 2);
position[0] -= offset * length;
position[1] += offset;
transforms.push(`translate(${position[0]}px, ${-position[1]}px)`);
// Angle.
const angle = 360 * (particle.rotation.x / (Math.PI * 2));
transforms.push(`rotate(${angle}deg)`);
// Scale.
transforms.push(`scale(${scale})`);
const rules = {
transform: transforms.join(' '),
};
// Alpha?
if (particle.useAlpha) {
rules.opacity = particle.alpha || 1;
}
// Color?
if (particle.useColor) {
rules.color = `rgb(${color.r * 255}, ${color.g * 255}, ${color.b * 255})`;
}
return rules;
}
}
export class TextNodeRenderer extends Proton.BaseRender {
static get freeList() {
if (!this._freeList) {
this._freeList = [];
}
return this._freeList;
}
static pushToFreeList(target) {
const freeList = this.freeList;
freeList.push(target);
}
static pullFromFreeList() {
const freeList = this.freeList;
return freeList.pop();
}
constructor(selector, stage) {
super();
this._body = new TextNode('');
this.name = 'NodeRenderer';
this.queued = [];
const promise = stage.findUiElement(selector);
promise.then((element) => {
if (this.parent) {
let freeNode;
while (freeNode = this.constructor.pullFromFreeList()) {
this.parent.removeChild(freeNode.div);
}
}
this.parent = element;
// Start with a warm cache.
if (this.parent && 0 === this.constructor.freeList.length) {
for (let i = 0; i < 500; ++i) {
const target = new TextNode();
target.createDom();
this.parent.appendChild(target.div);
target.div.style.opacity = 0;
this.constructor.pushToFreeList(target);
}
}
});
}
onParticleCreated(particle) {
// Clone global body if none was passed.
if (!particle.body) {
particle.body = this._body.clone();
}
// Pull from free list if we can.
const target = this.constructor.pullFromFreeList();
if (target) {
particle.target = target;
particle.target.div.style.opacity = 1;
}
else {
particle.target = particle.body;
particle.target.createDom();
if (this.parent) {
this.parent.appendChild(particle.target.div);
}
}
particle.target.copyFrom(particle.body);
// Queue if we don't have a parent yet.
if (!this.parent) {
this.queued.push(particle.target);
}
else {
for (let i = 0; i < this.queued.length; ++i) {
this.parent.appendChild(this.queued[i].div);
}
this.queued = [];
}
const rule = particle.target.particleStateToCssRule(particle);
particle.target.applyCssRule(rule);
}
onParticleUpdate(particle) {
const rule = particle.target.particleStateToCssRule(particle);
particle.target.applyCssRule(rule);
}
onParticleDead(particle) {
particle.target.div.style.opacity = 0;
this.constructor.pushToFreeList(particle.target);
particle.target = undefined;
}
}

View File

@ -1,120 +0,0 @@
import {compose, Property} from '@avocado/core';
import {Trait} from '@avocado/entity';
import {Ticker} from '@avocado/timing';
const decorate = compose(
);
export class Emitter extends decorate(Trait) {
constructor(entity, params, state) {
super(entity, params, state);
this.emitters = {};
if (AVOCADO_CLIENT) {
this.ticker = new Ticker(1 / 60);
this.ticker.on('tick', this.onTick, this);
}
}
static type() {
return 'emitter';
}
static addEmitter(emitter) {
if (!this._emitters) {
this._emitters = [];
}
this._emitters.push(emitter);
}
static particleCount() {
let particleCount = 0;
if (!this._emitters) {
return particleCount;
}
for (let i = 0; i < this._emitters.length; i++) {
const emitter = this._emitters[i];
particleCount += emitter.particleCount;
}
return particleCount;
}
static removeEmitter(emitter) {
const index = this._emitters.indexOf(emitter);
if (-1 === index) {
return;
}
this._emitters.splice(index, 1);
}
onTick(elapsed) {
for (const key in this.emitters) {
const emitter = this.emitters[key];
emitter.tick(elapsed);
}
}
updateFrequency() {
const particleCount = this.constructor.particleCount();
const updatesPerSecond = Math.max(15, (60 - (particleCount / 10)));
this.ticker.frequency = 1 / updatesPerSecond;
}
hooks() {
const hooks = {};
if (AVOCADO_CLIENT) {
hooks.afterDestructionTickers = () => {
return (elapsed) => {
for (const key in this.emitters) {
const emitter = this.emitters[key];
this.ticker.tick(elapsed);
if (!emitter.hasParticles()) {
this.ticker.off('tick', this.onTick);
emitter.destroy();
this.constructor.removeEmitter(emitter);
delete this.emitters[key];
}
}
return 0 === Object.keys(this.emitters).length;
};
};
}
return hooks;
}
methods() {
return {
addEmitter: (key, emitter) => {
if (AVOCADO_CLIENT) {
this.emitters[key] = emitter;
this.constructor.addEmitter(emitter);
}
},
addEmitterRenderer: (key, renderer) => {
if (AVOCADO_CLIENT) {
if (!this.emitters[key]) {
return;
}
this.emitters[key].addRenderer(renderer);
}
},
emitParticle: (key, ...args) => {
if (!this.emitters[key]) {
return;
}
this.emitters[key].emit(...args);
},
}
}
tick(elapsed) {
if (AVOCADO_CLIENT) {
this.ticker.tick(elapsed);
}
}
}

View File

@ -2,6 +2,7 @@ export {BodyView} from './body-view';
export {CircleShape} from './circle';
export {ShapeList} from './list';
export {PolygonShape} from './polygon';
export {Proton} from './proton';
export {RectangleShape} from './rectangle';
export {shapeFromJSON} from './shape-from-json';
export {ShapeView} from './shape-view';

View File

@ -0,0 +1 @@
export {Proton} from './proton';

View File

@ -0,0 +1,8 @@
import Proton_ from 'proton-js';
export class Proton extends Proton_ {
tick(elapsed) {
this.update(elapsed);
}
}

View File

@ -0,0 +1,83 @@
import {compose} from '@avocado/core';
import {StateProperty, Trait} from '@avocado/entity';
// import {Proton, TextNode} from '@avocado/graphics';
import {Range, Vector} from '@avocado/math';
const decorate = compose(
);
export class Emitted extends decorate(Trait) {
static defaultParams() {
return {
alpha: {
start: 1,
end: 1,
},
force: [0, 0],
mass: 1,
position: [0, 0],
rotation: {
start: 0,
add: 0,
},
scale: {
start: 1,
end: 1,
},
ttl: 2,
velocity: [0, 0],
};
}
static type() {
return 'emitted';
}
constructor(entity, params, state) {
super(entity, params, state);
this.alphaStart = new Range(this.params.alpha.start);
this.alphaEnd = new Range(this.params.alpha.end);
this.force = new Vector.Range(this.params.force);
this.mass = this.params.mass;
this.position = new Vector.Range(this.params.position);
this.rotationStart = new Range(this.params.rotation.start);
this.rotationAdd = new Range(this.params.rotation.add);
this.scaleStart = new Range(this.params.scale.start);
this.scaleEnd = new Range(this.params.scale.end);
this.ttl = this.params.ttl;
this.velocity = new Vector.Range(this.params.velocity);
}
methods() {
return {
particle: () => {
const position = this.position.value();
const velocity = this.velocity.value();
const force = this.force.value();
return {
alpha: {
start: this.alphaStart.value(),
end: this.alphaEnd.value(),
},
force,
mass: this.mass,
position,
rotation: {
start: this.rotationStart.value(),
add: this.rotationAdd.value(),
},
scale: {
start: this.scaleStart.value(),
end: this.scaleEnd.value(),
},
ttl: this.ttl,
velocity,
};
},
};
}
}

View File

@ -0,0 +1,116 @@
import {compose} from '@avocado/core';
import {Entity, StateProperty, Trait} from '@avocado/entity';
import {Vector} from '@avocado/math';
import {Proton} from '../proton';
const PI_180 = Math.PI / 180;
const decorate = compose(
);
export class Emitter extends decorate(Trait) {
static type() {
return 'emitter';
}
constructor(entity, params, state) {
super(entity, params, state);
this.emitter = new Proton.Emitter();
this.proton = new Proton();
this.proton.addEmitter(this.emitter);
this.onParticleDead = this.onParticleDead.bind(this);
this.onParticleUpdate = this.onParticleUpdate.bind(this);
this.emitter.bindEvent = true;
this.emitter.addEventListener('PARTICLE_DEAD', this.onParticleDead);
this.emitter.addEventListener('PARTICLE_UPDATE', this.onParticleUpdate);
}
onParticleDead(particle) {
particle.body.destroy();
}
onParticleUpdate(particle) {
particle.body.position = [particle.p.x, particle.p.y];
particle.body.opacity = particle.alpha;
particle.body.visibleScale = [particle.scale, particle.scale];
particle.body.rotation = particle.rotation * PI_180;
}
hooks() {
const hooks = {};
if (AVOCADO_CLIENT) {
hooks.afterDestructionTickers = () => {
return (elapsed) => {
this.tick(elapsed);
if (0 === this.emitter.particles.length) {
this.emitter.destroy();
return true;
}
};
};
}
return hooks;
}
methods() {
return {
emitParticle: (entity) => {
const particle = entity.particle();
const initializers = [
new Proton.Body(entity),
new Proton.Position(new Proton.PointZone(
particle.position[0],
particle.position[1]
)),
new Proton.Mass(particle.mass),
new Proton.Life(particle.ttl),
new Proton.Velocity(
particle.velocity[0],
particle.velocity[1]
),
];
const behaviors = [
new Proton.Alpha(
particle.alpha.start,
particle.alpha.end
),
new Proton.Force(
particle.force[0],
particle.force[1]
),
new Proton.Rotate(
particle.rotation.start,
particle.rotation.add,
'add'
),
new Proton.Scale(
particle.scale.start,
particle.scale.end
),
];
const protonParticle = this.emitter.createParticle(
initializers,
behaviors
);
// Prime.
this.onParticleUpdate(protonParticle);
},
emitParticleJson: (json) => {
return Entity.loadOrInstance(json).then((particle) => {
this.entity.emitParticle(particle);
return particle;
});
},
};
}
tick(elapsed) {
this.emitter.update(elapsed);
}
}