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 {Sprite} from './sprite';
|
||||
export {Stage} from './stage';
|
||||
// Proton.
|
||||
export {Proton, TextNode, TextNodeRenderer} from './proton';
|
||||
// Pixelly!
|
||||
settings.SCALE_MODE = SCALE_MODES.NEAREST;
|
||||
// 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 {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';
|
||||
|
|
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