From 1f14ca546baa0b9a4270fb68429de1f557bcf9fa Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 3 Nov 2019 10:41:23 -0600 Subject: [PATCH] refactor: particle physics :^) --- packages/graphics/index.js | 2 - packages/graphics/proton/index.js | 2 - packages/graphics/proton/proton.js | 19 -- .../graphics/proton/text-node-renderer.js | 169 ------------------ packages/graphics/traits/emitter.trait.js | 120 ------------- packages/physics/index.js | 1 + packages/physics/proton/index.js | 1 + packages/physics/proton/proton.js | 8 + packages/physics/traits/emitted.trait.js | 83 +++++++++ packages/physics/traits/emitter.trait.js | 116 ++++++++++++ 10 files changed, 209 insertions(+), 312 deletions(-) delete mode 100644 packages/graphics/proton/index.js delete mode 100644 packages/graphics/proton/proton.js delete mode 100644 packages/graphics/proton/text-node-renderer.js delete mode 100644 packages/graphics/traits/emitter.trait.js create mode 100644 packages/physics/proton/index.js create mode 100644 packages/physics/proton/proton.js create mode 100644 packages/physics/traits/emitted.trait.js create mode 100644 packages/physics/traits/emitter.trait.js diff --git a/packages/graphics/index.js b/packages/graphics/index.js index 944f7da..57a3df3 100644 --- a/packages/graphics/index.js +++ b/packages/graphics/index.js @@ -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. diff --git a/packages/graphics/proton/index.js b/packages/graphics/proton/index.js deleted file mode 100644 index 134d20f..0000000 --- a/packages/graphics/proton/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export {Proton} from './proton'; -export {TextNode, TextNodeRenderer} from './text-node-renderer'; diff --git a/packages/graphics/proton/proton.js b/packages/graphics/proton/proton.js deleted file mode 100644 index 3e8deaa..0000000 --- a/packages/graphics/proton/proton.js +++ /dev/null @@ -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); - } - -} diff --git a/packages/graphics/proton/text-node-renderer.js b/packages/graphics/proton/text-node-renderer.js deleted file mode 100644 index 2345985..0000000 --- a/packages/graphics/proton/text-node-renderer.js +++ /dev/null @@ -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; - } - -} diff --git a/packages/graphics/traits/emitter.trait.js b/packages/graphics/traits/emitter.trait.js deleted file mode 100644 index 7ea3bb6..0000000 --- a/packages/graphics/traits/emitter.trait.js +++ /dev/null @@ -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); - } - } - -} diff --git a/packages/physics/index.js b/packages/physics/index.js index c2c4add..99714cd 100644 --- a/packages/physics/index.js +++ b/packages/physics/index.js @@ -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'; diff --git a/packages/physics/proton/index.js b/packages/physics/proton/index.js new file mode 100644 index 0000000..94a63df --- /dev/null +++ b/packages/physics/proton/index.js @@ -0,0 +1 @@ +export {Proton} from './proton'; diff --git a/packages/physics/proton/proton.js b/packages/physics/proton/proton.js new file mode 100644 index 0000000..0f6dc1d --- /dev/null +++ b/packages/physics/proton/proton.js @@ -0,0 +1,8 @@ +import Proton_ from 'proton-js'; +export class Proton extends Proton_ { + + tick(elapsed) { + this.update(elapsed); + } + +} diff --git a/packages/physics/traits/emitted.trait.js b/packages/physics/traits/emitted.trait.js new file mode 100644 index 0000000..087a314 --- /dev/null +++ b/packages/physics/traits/emitted.trait.js @@ -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, + }; + }, + + }; + } + +} diff --git a/packages/physics/traits/emitter.trait.js b/packages/physics/traits/emitter.trait.js new file mode 100644 index 0000000..4411d0a --- /dev/null +++ b/packages/physics/traits/emitter.trait.js @@ -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); + } + +}