refactor: particle physics :^)
This commit is contained in:
parent
6a348fc693
commit
1f14ca546b
|
@ -12,8 +12,6 @@ export {Renderable} from './renderable';
|
||||||
export {Renderer} from './renderer';
|
export {Renderer} from './renderer';
|
||||||
export {Sprite} from './sprite';
|
export {Sprite} from './sprite';
|
||||||
export {Stage} from './stage';
|
export {Stage} from './stage';
|
||||||
// Proton.
|
|
||||||
export {Proton, TextNode, TextNodeRenderer} from './proton';
|
|
||||||
// Pixelly!
|
// Pixelly!
|
||||||
settings.SCALE_MODE = SCALE_MODES.NEAREST;
|
settings.SCALE_MODE = SCALE_MODES.NEAREST;
|
||||||
// Lil pixi manegement.
|
// Lil pixi manegement.
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export {Proton} from './proton';
|
|
||||||
export {TextNode, TextNodeRenderer} from './text-node-renderer';
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ export {BodyView} from './body-view';
|
||||||
export {CircleShape} from './circle';
|
export {CircleShape} from './circle';
|
||||||
export {ShapeList} from './list';
|
export {ShapeList} from './list';
|
||||||
export {PolygonShape} from './polygon';
|
export {PolygonShape} from './polygon';
|
||||||
|
export {Proton} from './proton';
|
||||||
export {RectangleShape} from './rectangle';
|
export {RectangleShape} from './rectangle';
|
||||||
export {shapeFromJSON} from './shape-from-json';
|
export {shapeFromJSON} from './shape-from-json';
|
||||||
export {ShapeView} from './shape-view';
|
export {ShapeView} from './shape-view';
|
||||||
|
|
1
packages/physics/proton/index.js
Normal file
1
packages/physics/proton/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {Proton} from './proton';
|
8
packages/physics/proton/proton.js
Normal file
8
packages/physics/proton/proton.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import Proton_ from 'proton-js';
|
||||||
|
export class Proton extends Proton_ {
|
||||||
|
|
||||||
|
tick(elapsed) {
|
||||||
|
this.update(elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
83
packages/physics/traits/emitted.trait.js
Normal file
83
packages/physics/traits/emitted.trait.js
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
116
packages/physics/traits/emitter.trait.js
Normal file
116
packages/physics/traits/emitter.trait.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user