├── ParticleDemo ├── ParticleDemo.arproj └── scripts │ ├── tsconfig.json │ ├── script.js │ ├── PFTween.js │ └── Particle.js ├── package.json ├── README.md └── Particle.js /ParticleDemo/ParticleDemo.arproj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pofulu/sparkar-particle/HEAD/ParticleDemo/ParticleDemo.arproj -------------------------------------------------------------------------------- /ParticleDemo/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | {"include":["*.js"],"compilerOptions":{"baseUrl":"/private/var/folders/pw/z3qr359x5d166nr1v4xb1gw40000gn/T/3901792c460cc14ac4dfd8bdd359d221fa53b/skylight-typedefs","allowJs":true,"checkJs":true,"outDir":"/private/var/folders/pw/z3qr359x5d166nr1v4xb1gw40000gn/T/3901792c460cc14ac4dfd8bdd359d221fa53b/skylight-typedefs/out-dir"}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sparkar-particle", 3 | "version": "0.2.2", 4 | "description": "A wrapped tool for contolling emitters in SparkAR.", 5 | "main": "Particle.js", 6 | "author": "Pofu Lu", 7 | "keywords": [ 8 | "sparkar", 9 | "particle", 10 | "emitter", 11 | "pofu" 12 | ], 13 | "files": [ 14 | "./Particle.js" 15 | ], 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/pofulu/sparkar-particle.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/pofulu/sparkar-particle/issues" 25 | }, 26 | "homepage": "https://github.com/pofulu/sparkar-particle#readme", 27 | "license": "MIT", 28 | "dependencies": { 29 | "sparkar-pftween": "^1.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ParticleDemo/scripts/script.js: -------------------------------------------------------------------------------- 1 | import { Particle } from './Particle'; 2 | import { Ease } from './PFTween'; 3 | 4 | const TouchGestures = require('TouchGestures'); 5 | 6 | //–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– Method 1 7 | // const Scene = require('Scene'); 8 | 9 | // Scene.root.findFirst('emitter0').then(emitter0 => { 10 | // const particle = new Particle(emitter0) 11 | // .setFadeout(Ease.easeOutSine) 12 | // .setScaleout(Ease.easeInCubic) 13 | // .stop() 14 | 15 | // TouchGestures.onTap().subscribe(() => 16 | // particle 17 | // .setHue(Math.random(), 0.1) 18 | // .setValue(1.5) 19 | // .burst() 20 | // ); 21 | // }); 22 | 23 | //–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– Method 2 24 | // const particles = new Particle.findFirst('emitter0') 25 | // const particles = new Particle.findAll('emitter0') 26 | const particles = new Particle.findByPath('**/emitter*') 27 | // const particles = new Particle.findByPath('**/div0/emitter*') 28 | .setFadeout(Ease.easeOutSine) 29 | .setScaleout(Ease.easeInCubic) 30 | .stop() 31 | 32 | TouchGestures.onTap().subscribe(() => 33 | particles 34 | .setHue(Math.random(), 0.1) 35 | .setValue(1.5) 36 | .burst() 37 | ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Particle 2 | 3 | A wrapped tool for controlling emitters in SparkAR. 4 | 5 | 6 | 7 | ## Install 8 | 9 | ### Import 10 | 11 | 0. [Download Particle.js](https://raw.githubusercontent.com/pofulu/sparkar-particle/master/ParticleDemo/scripts/Particle.js) (Right click and Save as) 12 | 13 | 1. [Download PFTween.js](https://github.com/pofulu/sparkar-pftween/raw/master/PFTween.js) (Right click and Save as) 14 | 15 | 2. Drag/Drop or import them to Spark AR 16 | 17 | 3. Import `Particle` module 18 | 19 | ```javascript 20 | import { Particle } from './Particle'; 21 | // Your script... 22 | ``` 23 | 24 | 4. You can also [Click Here to Download a Sample Project](https://yehonal.github.io/DownGit/#home?url=https://github.com/pofulu/sparkar-particle/tree/master/ParticleDemo). 25 | 26 | ### npm 27 | 28 | 0. Add package with `yarn` or `npm` 29 | 30 | ```shell 31 | yarn add sparkar-particle 32 | ``` 33 | or 34 | ```shell 35 | npm i sparkar-particle 36 | ``` 37 | 38 | 1. Import `Particle` module 39 | 40 | ```javascript 41 | import { Particle } from 'sparkar-particle'; 42 | // Your script... 43 | ``` 44 | 45 | 46 | 47 | ## Usage 48 | 49 | ```javascript 50 | import { Particle } from './Particle'; 51 | 52 | const TouchGestures = require('TouchGestures'); 53 | 54 | const ps = Particle.findFirst('emitter0').setFadeout().stop(); 55 | //or 56 | //const ps = Particle.findAll('emitter0').setFadeout().stop(); 57 | //or 58 | //const ps = Particle.findByPath('**/emitter0').setFadeout().stop(); 59 | 60 | TouchGestures.onTap().subscribe(() => ps.burst()) 61 | ``` 62 | 63 | ## Donations 64 | If this is useful for you, please consider a donation🙏🏼. One-time donations can be made with PayPal. 65 | 66 | [![](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HW99ESSALJZ36) 67 | -------------------------------------------------------------------------------- /ParticleDemo/scripts/PFTween.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * weakmap-polyfill v2.0.0 - ECMAScript6 WeakMap polyfill 3 | * https://github.com/polygonplanet/weakmap-polyfill 4 | * Copyright (c) 2015-2016 polygon planet 5 | * @license MIT 6 | */ 7 | (function (e) { "use strict"; if (e.WeakMap) { return } var t = Object.prototype.hasOwnProperty; var r = function (e, t, r) { if (Object.defineProperty) { Object.defineProperty(e, t, { configurable: true, writable: true, value: r }) } else { e[t] = r } }; e.WeakMap = function () { function WeakMap() { if (this === void 0) { throw new TypeError("Constructor WeakMap requires 'new'") } r(this, "_id", genId("_WeakMap")); if (arguments.length > 0) { throw new TypeError("WeakMap iterable is not supported") } } r(WeakMap.prototype, "delete", function (e) { checkInstance(this, "delete"); if (!isObject(e)) { return false } var t = e[this._id]; if (t && t[0] === e) { delete e[this._id]; return true } return false }); r(WeakMap.prototype, "get", function (e) { checkInstance(this, "get"); if (!isObject(e)) { return void 0 } var t = e[this._id]; if (t && t[0] === e) { return t[1] } return void 0 }); r(WeakMap.prototype, "has", function (e) { checkInstance(this, "has"); if (!isObject(e)) { return false } var t = e[this._id]; if (t && t[0] === e) { return true } return false }); r(WeakMap.prototype, "set", function (e, t) { checkInstance(this, "set"); if (!isObject(e)) { throw new TypeError("Invalid value used as weak map key") } var n = e[this._id]; if (n && n[0] === e) { n[1] = t; return this } r(e, this._id, [e, t]); return this }); function checkInstance(e, r) { if (!isObject(e) || !t.call(e, "_id")) { throw new TypeError(r + " method called on incompatible receiver " + typeof e) } } function genId(e) { return e + "_" + rand() + "." + rand() } function rand() { return Math.random().toString().substring(2) } r(WeakMap, "_polyfill", true); return WeakMap }(); function isObject(e) { return Object(e) === e } })(typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : this); 8 | 9 | 10 | const Animation = require('Animation'); 11 | const Reactive = require('Reactive'); 12 | const Time = require('Time'); 13 | const Diagnostics = require('Diagnostics'); 14 | 15 | const samplers = { 16 | linear: (begin, end) => Animation.samplers.linear(begin, end), 17 | easeInQuad: (begin, end) => Animation.samplers.easeInQuad(begin, end), 18 | easeOutQuad: (begin, end) => Animation.samplers.easeOutQuad(begin, end), 19 | easeInOutQuad: (begin, end) => Animation.samplers.easeInOutQuad(begin, end), 20 | easeInCubic: (begin, end) => Animation.samplers.easeInCubic(begin, end), 21 | easeOutCubic: (begin, end) => Animation.samplers.easeOutCubic(begin, end), 22 | easeInOutCubic: (begin, end) => Animation.samplers.easeInOutCubic(begin, end), 23 | easeInQuart: (begin, end) => Animation.samplers.easeInQuart(begin, end), 24 | easeOutQuart: (begin, end) => Animation.samplers.easeOutQuart(begin, end), 25 | easeInOutQuart: (begin, end) => Animation.samplers.easeInOutQuart(begin, end), 26 | easeInQuint: (begin, end) => Animation.samplers.easeInQuint(begin, end), 27 | easeOutQuint: (begin, end) => Animation.samplers.easeOutQuint(begin, end), 28 | easeInOutQuint: (begin, end) => Animation.samplers.easeInOutQuint(begin, end), 29 | easeInSine: (begin, end) => Animation.samplers.easeInSine(begin, end), 30 | easeOutSine: (begin, end) => Animation.samplers.easeOutSine(begin, end), 31 | easeInOutSine: (begin, end) => Animation.samplers.easeInOutSine(begin, end), 32 | easeInExpo: (begin, end) => Animation.samplers.easeInExpo(begin, end), 33 | easeOutExpo: (begin, end) => Animation.samplers.easeOutExpo(begin, end), 34 | easeInOutExpo: (begin, end) => Animation.samplers.easeInOutExpo(begin, end), 35 | easeInCirc: (begin, end) => Animation.samplers.easeInCirc(begin, end), 36 | easeOutCirc: (begin, end) => Animation.samplers.easeOutCirc(begin, end), 37 | easeInOutCirc: (begin, end) => Animation.samplers.easeInOutCirc(begin, end), 38 | easeInBack: (begin, end) => Animation.samplers.easeInBack(begin, end), 39 | easeOutBack: (begin, end) => Animation.samplers.easeOutBack(begin, end), 40 | easeInOutBack: (begin, end) => Animation.samplers.easeInOutBack(begin, end), 41 | easeInElastic: (begin, end) => Animation.samplers.easeInElastic(begin, end), 42 | easeOutElastic: (begin, end) => Animation.samplers.easeOutElastic(begin, end), 43 | easeInOutElastic: (begin, end) => Animation.samplers.easeInOutElastic(begin, end), 44 | easeInBounce: (begin, end) => Animation.samplers.easeInBounce(begin, end), 45 | easeOutBounce: (begin, end) => Animation.samplers.easeOutBounce(begin, end), 46 | easeInOutBounce: (begin, end) => Animation.samplers.easeInOutBounce(begin, end), 47 | punch: (begin, amount) => Animation.samplers.polyline({ 48 | keyframes: [ 49 | begin + (amount / 5) * 4, 50 | begin - (amount / 5) * 3, 51 | begin + (amount / 5) * 2, 52 | begin - (amount / 5) * 1, 53 | begin 54 | ], 55 | knots: [0, 1, 2, 3, 4] 56 | }), 57 | }; 58 | 59 | const degreeToRadian = Math.PI / 180; 60 | const privates = instantiatePrivateMap(); 61 | 62 | class PFTween { 63 | constructor(begin, end, durationMilliseconds) { 64 | privates(this).duration = durationMilliseconds; 65 | privates(this).start = []; 66 | privates(this).complete = []; 67 | privates(this).update = []; 68 | privates(this).loop = []; 69 | privates(this).sampler = samplers.linear( 70 | typeof begin.pinLastValue === 'function' ? begin.pinLastValue() : begin, 71 | typeof end.pinLastValue === 'function' ? end.pinLastValue() : end 72 | ); 73 | } 74 | 75 | /** 76 | * @param {{(tweener: PFTweener) : void}} setter 77 | */ 78 | static To(getter, setter, end, durationMilliseconds) { 79 | return new PFTween(getter, end, durationMilliseconds).bind(setter); 80 | } 81 | 82 | /** 83 | * @param {...any} clips 84 | * @returns {{(result?:any):Promise}} 85 | */ 86 | static combine(...clips) { 87 | return result => 88 | Promise.all(clips.map(i => i())).then(endValues => 89 | Promise.resolve(result != undefined ? result : endValues) 90 | ); 91 | } 92 | 93 | /** 94 | * @returns {{(result?:any):Promise}} 95 | */ 96 | static concat(...clips) { 97 | return result => { 98 | const firstClip = clips.shift(); 99 | return clips.reduce((pre, cur) => pre.then(cur), firstClip(result)); 100 | } 101 | } 102 | 103 | /** 104 | * If `isMirror` is not assigned, mirror animation is enabled by default. 105 | * @param {boolean=} isMirror 106 | */ 107 | setMirror(isMirror = true) { 108 | privates(this).isMirror = isMirror; 109 | return this; 110 | } 111 | 112 | /** 113 | * If `loopCount` is not assigned, it will be an infinite loop. 114 | * @param {number=} loopCount 115 | */ 116 | setLoops(loopCount = Infinity) { 117 | privates(this).loopCount = loopCount; 118 | return this; 119 | } 120 | 121 | setBegin(number) { 122 | privates(this).sampler.begin = typeof number.pinLastValue === 'function' ? number.pinLastValue() : number; 123 | return this; 124 | } 125 | 126 | setEnd(number) { 127 | privates(this).sampler.end = typeof number.pinLastValue === 'function' ? number.pinLastValue() : number; 128 | return this; 129 | } 130 | 131 | /** 132 | * @param {{(begin: number, end: number):ScalarSampler}} ease 133 | */ 134 | setEase(ease) { 135 | privates(this).sampler = ease(privates(this).sampler.begin, privates(this).sampler.end); 136 | return this; 137 | } 138 | 139 | /** 140 | * @param {number} delayMilliseconds 141 | */ 142 | setDelay(delayMilliseconds) { 143 | privates(this).delay = delayMilliseconds; 144 | return this; 145 | } 146 | 147 | /** 148 | * @param {{(tweener: PFTweener) : void}} call 149 | */ 150 | bind(call) { 151 | privates(this).update.push(call); 152 | return this; 153 | } 154 | 155 | /** 156 | * @param {{(iteration: number) : void}} call 157 | */ 158 | onLoop(call) { 159 | privates(this).loop.push(call); 160 | return this; 161 | } 162 | 163 | /** 164 | * @param {{() : void}} call 165 | */ 166 | onStart(call) { 167 | privates(this).start.push(call); 168 | return this; 169 | } 170 | 171 | /** 172 | * @param {{() : void}} call 173 | */ 174 | onComplete(call) { 175 | privates(this).complete.push(call); 176 | return this; 177 | } 178 | 179 | /** 180 | * @param {SceneObjectBase} sceneObject 181 | */ 182 | onStartVisible(sceneObject) { 183 | privates(this).start.push(() => sceneObject.hidden = Reactive.val(false)); 184 | return this; 185 | } 186 | 187 | /** 188 | * @param {SceneObjectBase} sceneObject 189 | */ 190 | onAnimatingVisibleOnly(sceneObject) { 191 | this.onStartVisible(sceneObject); 192 | this.onCompleteHidden(sceneObject); 193 | return this; 194 | } 195 | 196 | /** 197 | * @param {SceneObjectBase} sceneObject 198 | */ 199 | onStartHidden(sceneObject) { 200 | privates(this).start.push(() => sceneObject.hidden = Reactive.val(true)); 201 | return this; 202 | } 203 | 204 | /** 205 | * @param {SceneObjectBase} sceneObject 206 | */ 207 | onCompleteVisible(sceneObject) { 208 | privates(this).complete.push(() => sceneObject.hidden = Reactive.val(false)); 209 | return this; 210 | } 211 | 212 | /** 213 | * @param {SceneObjectBase} sceneObject 214 | */ 215 | onCompleteHidden(sceneObject) { 216 | privates(this).complete.push(() => sceneObject.hidden = Reactive.val(true)); 217 | return this; 218 | } 219 | 220 | /** 221 | * @param {SceneObjectBase} sceneObject 222 | */ 223 | onCompleteResetPosition(sceneObject) { 224 | const original = Reactive.pack3( 225 | sceneObject.transform.x.pinLastValue(), 226 | sceneObject.transform.y.pinLastValue(), 227 | sceneObject.transform.z.pinLastValue(), 228 | ); 229 | 230 | privates(this).complete.push(() => sceneObject.transform.position = original); 231 | return this; 232 | } 233 | 234 | /** 235 | * @param {SceneObjectBase} sceneObject 236 | */ 237 | onCompleteResetRotation(sceneObject) { 238 | const original = { 239 | x: sceneObject.transform.rotationX.pinLastValue(), 240 | y: sceneObject.transform.rotationY.pinLastValue(), 241 | z: sceneObject.transform.rotationZ.pinLastValue(), 242 | }; 243 | 244 | privates(this).complete.push(() => { 245 | sceneObject.transform.rotationX = original.x; 246 | sceneObject.transform.rotationY = original.y; 247 | sceneObject.transform.rotationZ = original.z; 248 | }); 249 | return this; 250 | } 251 | 252 | /** 253 | * @param {SceneObjectBase} sceneObject 254 | */ 255 | onCompleteResetScale(sceneObject) { 256 | const original = Reactive.scale( 257 | sceneObject.transform.scaleX.pinLastValue(), 258 | sceneObject.transform.scaleY.pinLastValue(), 259 | sceneObject.transform.scaleZ.pinLastValue(), 260 | ); 261 | 262 | privates(this).complete.push(() => sceneObject.transform.scale = original); 263 | return this; 264 | } 265 | 266 | /** 267 | * Please note that this can only be used on `SceneObject` containing material property. 268 | * @param {SceneObjectBase} sceneObject 269 | */ 270 | onCompleteResetOpacity(sceneObject) { 271 | const original = sceneObject.material.opacity.pinLastValue(); 272 | privates(this).complete.push(() => sceneObject.material.opacity = original); 273 | return this; 274 | } 275 | 276 | apply(autoPlay = true) { 277 | return animate(privates(this), autoPlay); 278 | } 279 | 280 | get clip() { 281 | const completePromise = result => 282 | new Promise(resolve => privates(this).complete.push(() => 283 | resolve(result != undefined ? result : privates(this).sampler.end)) 284 | ); 285 | 286 | if (privates(this).loopCount == Infinity) { 287 | Diagnostics.log('Please note that set infinite loop will stuck the clips chain.'); 288 | } 289 | 290 | return this.apply(false).getPromise(completePromise); 291 | } 292 | 293 | get log() { 294 | return privates(this) 295 | } 296 | 297 | get scalar() { 298 | return this.apply(true).scalar; 299 | } 300 | 301 | get scale() { 302 | return this.apply(true).scale; 303 | } 304 | 305 | get pack2() { 306 | return this.apply(true).pack2; 307 | } 308 | 309 | get pack3() { 310 | return this.apply(true).pack3; 311 | } 312 | 313 | get pack4() { 314 | return this.apply(true).pack4; 315 | } 316 | 317 | get rotation() { 318 | return this.apply(true).rotation; 319 | } 320 | } 321 | 322 | class PFTweener { 323 | constructor(driver, animate, delay, start, update) { 324 | privates(this).delay = delay; 325 | privates(this).animate = animate; 326 | privates(this).driver = driver; 327 | privates(this).onStart = start; 328 | privates(this).onUpdate = update; 329 | } 330 | 331 | /** 332 | * Generally, you should get `clip` directly instead of `apply()` and then call this function. The animation will replay immediately when call this funtion. 333 | * @param {{(result?:any):Promise}} promise 334 | * @returns {{(result?:any):Promise}} 335 | */ 336 | getPromise(promise) { 337 | return result => { 338 | this.replay(); 339 | return promise(result); 340 | }; 341 | } 342 | 343 | replay() { 344 | this.reset(); 345 | this.start(); 346 | } 347 | 348 | reset() { 349 | privates(this).driver.reset(); 350 | } 351 | 352 | reverse() { 353 | privates(this).driver.reverse(); 354 | } 355 | 356 | start() { 357 | const play = () => { 358 | invoke(privates(this).onStart); 359 | invoke(privates(this).onUpdate, this); 360 | privates(this).driver.start(); 361 | } 362 | 363 | if (privates(this).delay != undefined) { 364 | Time.setTimeout(play, privates(this).delay); 365 | } else { 366 | play(); 367 | } 368 | } 369 | 370 | stop() { 371 | privates(this).driver.stop(); 372 | } 373 | 374 | /**@returns {BoolSignal} */ 375 | get isRunning() { 376 | return privates(this).driver.isRunning(); 377 | } 378 | 379 | /**@returns {ScalarSignal} */ 380 | get scalar() { 381 | return privates(this).animate; 382 | } 383 | 384 | /**@returns {ScaleSignal} */ 385 | get scale() { 386 | const scalar = this.scalar; 387 | return Reactive.scale(scalar, scalar, scalar); 388 | } 389 | 390 | /**@returns {Point2DSignal} */ 391 | get pack2() { 392 | const scalar = this.scalar 393 | return Reactive.pack2(scalar, scalar); 394 | } 395 | 396 | /**@returns {PointSignal} */ 397 | get pack3() { 398 | const scalar = this.scalar 399 | return Reactive.pack3(scalar, scalar, scalar); 400 | } 401 | 402 | /**@returns {Point4DSignal} */ 403 | get pack4() { 404 | const scalar = this.scalar 405 | return Reactive.pack4(scalar, scalar, scalar, scalar); 406 | } 407 | 408 | /**@returns {ScalarSignal} convert degree to radian*/ 409 | get rotation() { 410 | const scalar = this.scalar 411 | return scalar.mul(degreeToRadian); 412 | } 413 | } 414 | 415 | function animate(config, autoPlay) { 416 | const driver = Animation.timeDriver({ 417 | durationMilliseconds: config.duration, 418 | loopCount: config.loopCount, 419 | mirror: config.isMirror 420 | }); 421 | 422 | driver.onCompleted().subscribe(() => invoke(config.complete)); 423 | driver.onAfterIteration().subscribe(index => invoke(config.loop, index)); 424 | 425 | const animate = Animation.animate(driver, config.sampler); 426 | const tweener = new PFTweener(driver, animate, config.delay, config.start, config.update); 427 | 428 | if (autoPlay) tweener.start(); 429 | 430 | return tweener; 431 | } 432 | 433 | function invoke(calls, arg) { 434 | for (let i = 0; i < calls.length; i++) { 435 | calls[i](arg); 436 | } 437 | } 438 | 439 | function instantiatePrivateMap() { 440 | const map = new WeakMap(); 441 | return obj => { 442 | let props = map.get(obj); 443 | if (!props) { 444 | props = {}; 445 | map.set(obj, props); 446 | } 447 | return props; 448 | }; 449 | } 450 | 451 | export { PFTween, samplers as Ease }; -------------------------------------------------------------------------------- /Particle.js: -------------------------------------------------------------------------------- 1 | import { Ease } from 'sparkar-pftween'; 2 | 3 | const Animation = require('Animation'); 4 | const Time = require('Time'); 5 | const Scene = require('Scene'); 6 | const Diagnostics = require('Diagnostics'); 7 | const Reactive = require('Reactive'); 8 | 9 | class ParticleHSVAModifier { 10 | constructor() { 11 | this.h = Animation.samplers.constant(1); 12 | this.s = Animation.samplers.constant(1); 13 | this.v = Animation.samplers.constant(1); 14 | this.a = Animation.samplers.constant(1); 15 | } 16 | 17 | static modifyHSVA(emitter, modifier) { 18 | emitter.hsvaColorModulationModifier = Animation.samplers.HSVA([modifier.h, modifier.s, modifier.v, modifier.a,]); 19 | } 20 | } 21 | 22 | class ParticleHSVA { 23 | constructor() { 24 | this.hue = 1; 25 | this.saturation = 1; 26 | this.value = 1; 27 | this.alpha = 1; 28 | 29 | this.hueDelta = 1; 30 | this.saturationDelta = 1; 31 | this.valueDelta = 1; 32 | this.alphaDelta = 1; 33 | } 34 | 35 | static setHSVA(emitter, color) { 36 | const colorSignal = Reactive.HSVA(color.hue, color.saturation, color.value, color.alpha); 37 | const colorSignalDelta = Reactive.HSVA(color.hueDelta, color.saturationDelta, color.valueDelta, color.alphaDelta); 38 | 39 | emitter.colorModulationHSVA = colorSignal; 40 | emitter.colorModulationHSVADelta = colorSignalDelta; 41 | } 42 | } 43 | 44 | export class Particle { 45 | /** 46 | * @param {SceneObjectBase} emitter 47 | */ 48 | constructor(emitter) { 49 | if (emitter == undefined) { 50 | return undefined; 51 | } 52 | this.emitter = emitter; 53 | this.colorModifier = new ParticleHSVAModifier(); 54 | this.color = new ParticleHSVA(); 55 | this.burstSubscription = undefined; 56 | } 57 | 58 | /** 59 | * @param {number} begin 60 | * @param {number} end 61 | * @param {{(begin: number, end: number): ScalarSampler}} ease 62 | */ 63 | modifyHue(begin, end, ease) { 64 | this.colorModifier.h = ease(begin, end); 65 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 66 | return this; 67 | } 68 | 69 | /** 70 | * @param {number} begin 71 | * @param {number} end 72 | * @param {{(begin: number, end: number): ScalarSampler}} ease 73 | */ 74 | modifySaturation(begin, end, ease) { 75 | this.colorModifier.s = ease(begin, end); 76 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 77 | return this; 78 | } 79 | 80 | /** 81 | * @param {number} begin 82 | * @param {number} end 83 | * @param {{(begin: number, end: number): ScalarSampler}} ease 84 | */ 85 | modifyValue(begin, end, ease) { 86 | this.colorModifier.v = ease(begin, end); 87 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 88 | return this; 89 | } 90 | 91 | /** 92 | * @param {number} begin 93 | * @param {number} end 94 | * @param {{(begin: number, end: number): ScalarSampler}} ease 95 | */ 96 | modifyAlpha(begin, end, ease) { 97 | this.colorModifier.a = ease(begin, end); 98 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 99 | return this; 100 | } 101 | 102 | /** 103 | * @param {number} from 104 | * @param {number} to 105 | * @param {{(begin: number, end: number): ScalarSampler}} ease 106 | */ 107 | modifyScale(from, to, ease) { 108 | this.emitter.sizeModifier = ease(from, to);; 109 | return this; 110 | } 111 | 112 | /** 113 | * @param {{(begin: number, end: number): ScalarSampler}} ease 114 | */ 115 | setScaleout(ease = Ease.easeInCubic) { 116 | if (this.emitter.scaleDelta.pinLastValue() != 0) { 117 | Diagnostics.log(`The particle "${this.emitter.name}" will not scale out perfectly if its "scale delta" is not "0"`); 118 | } 119 | this.modifyScale(0, this.emitter.scale.mul(this.emitter.scaleDelta.add(1)).neg().pinLastValue(), ease); 120 | return this; 121 | } 122 | 123 | /** 124 | * @param {{(begin: number, end: number): ScalarSampler}} ease 125 | */ 126 | setFadeout(ease = Ease.easeInCubic) { 127 | this.modifyAlpha(1, 0, ease); 128 | return this; 129 | } 130 | 131 | /** 132 | * @param {ScalarSignal | Number=} birthrate 133 | * @param {number} duration 134 | * @returns {Promise} 135 | */ 136 | burst(birthrate = 200, duration = 100) { 137 | if (this.burstSubscription) { 138 | this.burstSubscription.unsubscribe(); 139 | if (this.cancellation) 140 | this.cancellation(); 141 | } 142 | 143 | return new Promise((resolve, reject) => { 144 | this.start(birthrate); 145 | 146 | this.burstSubscription = Time.setTimeout(() => { 147 | this.stop(); 148 | resolve(); 149 | }, duration); 150 | 151 | this.cancellation = () => { 152 | reject('you called burst when bursting, it is fine, just a hint'); 153 | } 154 | }); 155 | } 156 | 157 | /** 158 | * @param {ScalarSignal | Number=} hue 159 | * @param {ScalarSignal | Number=} hueDelta 160 | */ 161 | setHue(hue, hueDelta = 0) { 162 | this.color.hue = hue; 163 | this.color.hueDelta = hueDelta; 164 | ParticleHSVA.setHSVA(this.emitter, this.color); 165 | return this; 166 | } 167 | 168 | /** 169 | * @param {ScalarSignal | Number=} saturation 170 | * @param {ScalarSignal | Number=} saturationDelta 171 | */ 172 | setSaturation(saturation, saturationDelta = 0) { 173 | this.color.saturation = saturation; 174 | this.color.saturationDelta = saturationDelta; 175 | ParticleHSVA.setHSVA(this.emitter, this.color); 176 | return this; 177 | } 178 | 179 | /** 180 | * @param {ScalarSignal | Number=} value 181 | * @param {ScalarSignal | Number=} value 182 | */ 183 | setValue(value, valueDelta = 0) { 184 | this.color.value = value; 185 | this.color.valueDelta = valueDelta; 186 | ParticleHSVA.setHSVA(this.emitter, this.color); 187 | return this; 188 | } 189 | 190 | /** 191 | * @param {ScalarSignal | Number=} alpha 192 | * @param {ScalarSignal | Number=} alphaDelta 193 | */ 194 | setAlpha(alpha, alphaDelta = 0) { 195 | this.color.alpha = alpha; 196 | this.color.alphaDelta = alphaDelta; 197 | ParticleHSVA.setHSVA(this.emitter, this.color); 198 | return this; 199 | } 200 | 201 | /** 202 | * @param {ScalarSignal | Number} birthrate 203 | */ 204 | start(birthrate) { 205 | if (this.burstSubscription) { 206 | this.burstSubscription.unsubscribe(); 207 | } 208 | 209 | this.emitter.birthrate = birthrate; 210 | return this; 211 | } 212 | 213 | stop() { 214 | if (this.burstSubscription) { 215 | this.burstSubscription.unsubscribe(); 216 | } 217 | 218 | this.emitter.birthrate = 0; 219 | return this; 220 | } 221 | } 222 | 223 | Particle.findByPath = class { 224 | constructor(emitterPath) { 225 | this.emittersPromise = Scene.root.findByPath(emitterPath); 226 | this.particlesPromise = this.emittersPromise.then(emitters => this.particles = emitters.map(em => new Particle(em))); 227 | } 228 | 229 | /** 230 | * @param {number} begin 231 | * @param {number} end 232 | * @param {{(begin: number, end: number): ScalarSampler}} ease 233 | */ 234 | modifyHue(begin, end, ease) { 235 | this.particlesPromise.then(() => { 236 | this.particles.forEach(em => em.modifyHue(begin, end, ease)); 237 | }); 238 | return this; 239 | } 240 | 241 | /** 242 | * @param {number} begin 243 | * @param {number} end 244 | * @param {{(begin: number, end: number): ScalarSampler}} ease 245 | */ 246 | modifySaturation(begin, end, ease) { 247 | this.particlesPromise.then(() => { 248 | this.particles.forEach(em => em.modifySaturation(begin, end, ease)); 249 | }); 250 | return this; 251 | } 252 | 253 | /** 254 | * @param {number} begin 255 | * @param {number} end 256 | * @param {{(begin: number, end: number): ScalarSampler}} ease 257 | */ 258 | modifyValue(begin, end, ease) { 259 | this.particlesPromise.then(() => { 260 | this.particles.forEach(em => em.modifyValue(begin, end, ease)); 261 | }); 262 | return this; 263 | } 264 | 265 | /** 266 | * @param {number} begin 267 | * @param {number} end 268 | * @param {{(begin: number, end: number): ScalarSampler}} ease 269 | */ 270 | modifyAlpha(begin, end, ease) { 271 | this.particlesPromise.then(() => { 272 | this.particles.forEach(em => em.modifyAlpha(begin, end, ease)); 273 | }); 274 | return this; 275 | } 276 | 277 | /** 278 | * @param {number} from 279 | * @param {number} to 280 | * @param {{(begin: number, end: number): ScalarSampler}} ease 281 | */ 282 | modifyScale(from, to, ease) { 283 | this.particlesPromise.then(() => { 284 | this.particles.forEach(em => em.modifyScale(from, to, ease)); 285 | }); 286 | return this; 287 | } 288 | 289 | /** 290 | * @param {{(begin: number, end: number): ScalarSampler}} ease 291 | */ 292 | setScaleout(ease = Ease.easeInCubic) { 293 | this.particlesPromise.then(() => { 294 | this.particles.forEach(em => em.setScaleout(ease)); 295 | }); 296 | return this; 297 | } 298 | 299 | /** 300 | * @param {{(begin: number, end: number): ScalarSampler}} ease 301 | */ 302 | setFadeout(ease = Ease.easeInCubic) { 303 | this.particlesPromise.then(() => { 304 | this.particles.forEach(em => em.setFadeout(ease)); 305 | }); 306 | return this; 307 | } 308 | 309 | /** 310 | * @param {ScalarSignal | Number=} birthrate 311 | * @param {number} duration 312 | * @returns {Promise} 313 | */ 314 | burst(birthrate = 200, duration = 100) { 315 | this.particlesPromise.then(() => { 316 | this.particles.forEach(em => em.burst(birthrate, duration)); 317 | }); 318 | return this; 319 | } 320 | 321 | /** 322 | * @param {ScalarSignal | Number=} hue 323 | * @param {ScalarSignal | Number=} hueDelta 324 | */ 325 | setHue(hue, hueDelta = 0) { 326 | this.particlesPromise.then(() => { 327 | this.particles.forEach(em => em.setHue(hue, hueDelta)); 328 | }); 329 | return this; 330 | } 331 | 332 | /** 333 | * @param {ScalarSignal | Number=} saturation 334 | * @param {ScalarSignal | Number=} saturationDelta 335 | */ 336 | setSaturation(saturation, saturationDelta = 0) { 337 | this.particlesPromise.then(() => { 338 | this.particles.forEach(em => em.setSaturation(saturation, saturationDelta)); 339 | }); 340 | return this; 341 | } 342 | 343 | /** 344 | * @param {ScalarSignal | Number=} value 345 | * @param {ScalarSignal | Number=} value 346 | */ 347 | setValue(value, valueDelta = 0) { 348 | this.particlesPromise.then(() => { 349 | this.particles.forEach(em => em.setValue(value, valueDelta)); 350 | }); 351 | return this; 352 | } 353 | 354 | /** 355 | * @param {ScalarSignal | Number=} alpha 356 | * @param {ScalarSignal | Number=} alphaDelta 357 | */ 358 | setAlpha(alpha, alphaDelta = 0) { 359 | this.particlesPromise.then(() => { 360 | this.particles.forEach(em => em.setAlpha(alpha, alphaDelta)); 361 | }); 362 | return this; 363 | } 364 | 365 | /** 366 | * @param {ScalarSignal | Number} birthrate 367 | */ 368 | start(birthrate) { 369 | this.particlesPromise.then(() => { 370 | this.particles.forEach(em => em.start(birthrate)); 371 | }); 372 | return this; 373 | } 374 | 375 | stop() { 376 | this.particlesPromise.then(() => { 377 | this.particles.forEach(em => em.stop()); 378 | }); 379 | return this; 380 | } 381 | } 382 | 383 | Particle.findAll = class { 384 | constructor(emittersName, recursive = true) { 385 | this.emittersPromise = Scene.root.findAll(emittersName, { recursive: recursive }) 386 | this.particlesPromise = this.emittersPromise.then(emitters => this.particles = emitters.map(em => new Particle(em))); 387 | } 388 | 389 | /** 390 | * @param {number} begin 391 | * @param {number} end 392 | * @param {{(begin: number, end: number): ScalarSampler}} ease 393 | */ 394 | modifyHue(begin, end, ease) { 395 | this.particlesPromise.then(() => { 396 | this.particles.forEach(em => em.modifyHue(begin, end, ease)); 397 | }); 398 | return this; 399 | } 400 | 401 | /** 402 | * @param {number} begin 403 | * @param {number} end 404 | * @param {{(begin: number, end: number): ScalarSampler}} ease 405 | */ 406 | modifySaturation(begin, end, ease) { 407 | this.particlesPromise.then(() => { 408 | this.particles.forEach(em => em.modifySaturation(begin, end, ease)); 409 | }); 410 | return this; 411 | } 412 | 413 | /** 414 | * @param {number} begin 415 | * @param {number} end 416 | * @param {{(begin: number, end: number): ScalarSampler}} ease 417 | */ 418 | modifyValue(begin, end, ease) { 419 | this.particlesPromise.then(() => { 420 | this.particles.forEach(em => em.modifyValue(begin, end, ease)); 421 | }); 422 | return this; 423 | } 424 | 425 | /** 426 | * @param {number} begin 427 | * @param {number} end 428 | * @param {{(begin: number, end: number): ScalarSampler}} ease 429 | */ 430 | modifyAlpha(begin, end, ease) { 431 | this.particlesPromise.then(() => { 432 | this.particles.forEach(em => em.modifyAlpha(begin, end, ease)); 433 | }); 434 | return this; 435 | } 436 | 437 | /** 438 | * @param {number} from 439 | * @param {number} to 440 | * @param {{(begin: number, end: number): ScalarSampler}} ease 441 | */ 442 | modifyScale(from, to, ease) { 443 | this.particlesPromise.then(() => { 444 | this.particles.forEach(em => em.modifyScale(from, to, ease)); 445 | }); 446 | return this; 447 | } 448 | 449 | /** 450 | * @param {{(begin: number, end: number): ScalarSampler}} ease 451 | */ 452 | setScaleout(ease = Ease.easeInCubic) { 453 | this.particlesPromise.then(() => { 454 | this.particles.forEach(em => em.setScaleout(ease)); 455 | }); 456 | return this; 457 | } 458 | 459 | /** 460 | * @param {{(begin: number, end: number): ScalarSampler}} ease 461 | */ 462 | setFadeout(ease = Ease.easeInCubic) { 463 | this.particlesPromise.then(() => { 464 | this.particles.forEach(em => em.setFadeout(ease)); 465 | }); 466 | return this; 467 | } 468 | 469 | /** 470 | * @param {ScalarSignal | Number=} birthrate 471 | * @param {number} duration 472 | * @returns {Promise} 473 | */ 474 | burst(birthrate = 200, duration = 100) { 475 | this.particlesPromise.then(() => { 476 | this.particles.forEach(em => em.burst(birthrate, duration)); 477 | }); 478 | return this; 479 | } 480 | 481 | /** 482 | * @param {ScalarSignal | Number=} hue 483 | * @param {ScalarSignal | Number=} hueDelta 484 | */ 485 | setHue(hue, hueDelta = 0) { 486 | this.particlesPromise.then(() => { 487 | this.particles.forEach(em => em.setHue(hue, hueDelta)); 488 | }); 489 | return this; 490 | } 491 | 492 | /** 493 | * @param {ScalarSignal | Number=} saturation 494 | * @param {ScalarSignal | Number=} saturationDelta 495 | */ 496 | setSaturation(saturation, saturationDelta = 0) { 497 | this.particlesPromise.then(() => { 498 | this.particles.forEach(em => em.setSaturation(saturation, saturationDelta)); 499 | }); 500 | return this; 501 | } 502 | 503 | /** 504 | * @param {ScalarSignal | Number=} value 505 | * @param {ScalarSignal | Number=} value 506 | */ 507 | setValue(value, valueDelta = 0) { 508 | this.particlesPromise.then(() => { 509 | this.particles.forEach(em => em.setValue(value, valueDelta)); 510 | }); 511 | return this; 512 | } 513 | 514 | /** 515 | * @param {ScalarSignal | Number=} alpha 516 | * @param {ScalarSignal | Number=} alphaDelta 517 | */ 518 | setAlpha(alpha, alphaDelta = 0) { 519 | this.particlesPromise.then(() => { 520 | this.particles.forEach(em => em.setAlpha(alpha, alphaDelta)); 521 | }); 522 | return this; 523 | } 524 | 525 | /** 526 | * @param {ScalarSignal | Number} birthrate 527 | */ 528 | start(birthrate) { 529 | this.particlesPromise.then(() => { 530 | this.particles.forEach(em => em.start(birthrate)); 531 | }); 532 | return this; 533 | } 534 | 535 | stop() { 536 | this.particlesPromise.then(() => { 537 | this.particles.forEach(em => em.stop()); 538 | }); 539 | return this; 540 | } 541 | } 542 | 543 | Particle.findFirst = class { 544 | constructor(emitterName) { 545 | this.emitterPromise = Scene.root.findFirst(emitterName); 546 | this.particlePromise = this.emitterPromise.then(emitter => this.particle = new Particle(emitter)); 547 | } 548 | 549 | /** 550 | * @param {number} begin 551 | * @param {number} end 552 | * @param {{(begin: number, end: number): ScalarSampler}} ease 553 | */ 554 | modifyHue(begin, end, ease) { 555 | this.particlePromise.then(() => { 556 | this.particle.modifyHue(begin, end, ease); 557 | }) 558 | return this; 559 | } 560 | 561 | /** 562 | * @param {number} begin 563 | * @param {number} end 564 | * @param {{(begin: number, end: number): ScalarSampler}} ease 565 | */ 566 | modifySaturation(begin, end, ease) { 567 | this.particlePromise.then(() => { 568 | this.particle.modifySaturation(begin, end, ease); 569 | }); 570 | return this; 571 | } 572 | 573 | /** 574 | * @param {number} begin 575 | * @param {number} end 576 | * @param {{(begin: number, end: number): ScalarSampler}} ease 577 | */ 578 | modifyValue(begin, end, ease) { 579 | this.particlePromise.then(() => { 580 | this.particle.modifyValue(begin, end, ease); 581 | }); 582 | return this; 583 | } 584 | 585 | /** 586 | * @param {number} begin 587 | * @param {number} end 588 | * @param {{(begin: number, end: number): ScalarSampler}} ease 589 | */ 590 | modifyAlpha(begin, end, ease) { 591 | this.particlePromise.then(() => { 592 | this.particle.modifyAlpha(begin, end, ease); 593 | }); 594 | return this; 595 | } 596 | 597 | /** 598 | * @param {number} from 599 | * @param {number} to 600 | * @param {{(begin: number, end: number): ScalarSampler}} ease 601 | */ 602 | modifyScale(from, to, ease) { 603 | this.particlePromise.then(() => { 604 | this.particle.modifyScale(from, to, ease); 605 | }); 606 | return this; 607 | } 608 | 609 | /** 610 | * @param {{(begin: number, end: number): ScalarSampler}} ease 611 | */ 612 | setScaleout(ease = Ease.easeInCubic) { 613 | this.particlePromise.then(() => { 614 | this.particle.setScaleout(ease); 615 | }); 616 | return this; 617 | } 618 | 619 | /** 620 | * @param {{(begin: number, end: number): ScalarSampler}} ease 621 | */ 622 | setFadeout(ease = Ease.easeInCubic) { 623 | this.particlePromise.then(() => { 624 | this.particle.setFadeout(ease); 625 | }); 626 | return this; 627 | } 628 | 629 | /** 630 | * @param {ScalarSignal | Number=} birthrate 631 | * @param {number} duration 632 | * @returns {Promise} 633 | */ 634 | burst(birthrate = 200, duration = 100) { 635 | this.particlePromise.then(() => { 636 | this.particle.burst(birthrate, duration); 637 | }); 638 | return this; 639 | } 640 | 641 | /** 642 | * @param {ScalarSignal | Number=} hue 643 | * @param {ScalarSignal | Number=} hueDelta 644 | */ 645 | setHue(hue, hueDelta = 0) { 646 | this.particlePromise.then(() => { 647 | this.particle.setHue(hue, hueDelta); 648 | }); 649 | return this; 650 | } 651 | 652 | /** 653 | * @param {ScalarSignal | Number=} saturation 654 | * @param {ScalarSignal | Number=} saturationDelta 655 | */ 656 | setSaturation(saturation, saturationDelta = 0) { 657 | this.particlePromise.then(() => { 658 | this.particle.setSaturation(saturation, saturationDelta); 659 | }); 660 | return this; 661 | } 662 | 663 | /** 664 | * @param {ScalarSignal | Number=} value 665 | * @param {ScalarSignal | Number=} value 666 | */ 667 | setValue(value, valueDelta = 0) { 668 | this.particlePromise.then(() => { 669 | this.particle.setValue(value, valueDelta); 670 | }); 671 | return this; 672 | } 673 | 674 | /** 675 | * @param {ScalarSignal | Number=} alpha 676 | * @param {ScalarSignal | Number=} alphaDelta 677 | */ 678 | setAlpha(alpha, alphaDelta = 0) { 679 | this.particlePromise.then(() => { 680 | this.particle.setAlpha(alpha, alphaDelta); 681 | }); 682 | return this; 683 | } 684 | 685 | /** 686 | * @param {ScalarSignal | Number} birthrate 687 | */ 688 | start(birthrate) { 689 | this.particlePromise.then(() => { 690 | this.particle.start(birthrate); 691 | }); 692 | return this; 693 | } 694 | 695 | stop() { 696 | this.particlePromise.then(() => { 697 | this.particle.stop(); 698 | }); 699 | return this; 700 | } 701 | } -------------------------------------------------------------------------------- /ParticleDemo/scripts/Particle.js: -------------------------------------------------------------------------------- 1 | import { Ease } from './PFTween'; 2 | 3 | const Animation = require('Animation'); 4 | const Time = require('Time'); 5 | const Scene = require('Scene'); 6 | const Diagnostics = require('Diagnostics'); 7 | const Reactive = require('Reactive'); 8 | 9 | class ParticleHSVAModifier { 10 | constructor() { 11 | this.h = Animation.samplers.constant(1); 12 | this.s = Animation.samplers.constant(1); 13 | this.v = Animation.samplers.constant(1); 14 | this.a = Animation.samplers.constant(1); 15 | } 16 | 17 | static modifyHSVA(emitter, modifier) { 18 | emitter.hsvaColorModulationModifier = Animation.samplers.HSVA([modifier.h, modifier.s, modifier.v, modifier.a,]); 19 | } 20 | } 21 | 22 | class ParticleHSVA { 23 | constructor() { 24 | this.hue = 1; 25 | this.saturation = 1; 26 | this.value = 1; 27 | this.alpha = 1; 28 | 29 | this.hueDelta = 1; 30 | this.saturationDelta = 1; 31 | this.valueDelta = 1; 32 | this.alphaDelta = 1; 33 | } 34 | 35 | static setHSVA(emitter, color) { 36 | const colorSignal = Reactive.HSVA(color.hue, color.saturation, color.value, color.alpha); 37 | const colorSignalDelta = Reactive.HSVA(color.hueDelta, color.saturationDelta, color.valueDelta, color.alphaDelta); 38 | 39 | emitter.colorModulationHSVA = colorSignal; 40 | emitter.colorModulationHSVADelta = colorSignalDelta; 41 | } 42 | } 43 | 44 | export class Particle { 45 | /** 46 | * @param {SceneObjectBase} emitter 47 | */ 48 | constructor(emitter) { 49 | if (emitter == undefined) { 50 | return undefined; 51 | } 52 | this.emitter = emitter; 53 | this.colorModifier = new ParticleHSVAModifier(); 54 | this.color = new ParticleHSVA(); 55 | this.burstSubscription = undefined; 56 | } 57 | 58 | /** 59 | * @param {number} begin 60 | * @param {number} end 61 | * @param {{(begin: number, end: number): ScalarSampler}} ease 62 | */ 63 | modifyHue(begin, end, ease) { 64 | this.colorModifier.h = ease(begin, end); 65 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 66 | return this; 67 | } 68 | 69 | /** 70 | * @param {number} begin 71 | * @param {number} end 72 | * @param {{(begin: number, end: number): ScalarSampler}} ease 73 | */ 74 | modifySaturation(begin, end, ease) { 75 | this.colorModifier.s = ease(begin, end); 76 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 77 | return this; 78 | } 79 | 80 | /** 81 | * @param {number} begin 82 | * @param {number} end 83 | * @param {{(begin: number, end: number): ScalarSampler}} ease 84 | */ 85 | modifyValue(begin, end, ease) { 86 | this.colorModifier.v = ease(begin, end); 87 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 88 | return this; 89 | } 90 | 91 | /** 92 | * @param {number} begin 93 | * @param {number} end 94 | * @param {{(begin: number, end: number): ScalarSampler}} ease 95 | */ 96 | modifyAlpha(begin, end, ease) { 97 | this.colorModifier.a = ease(begin, end); 98 | ParticleHSVAModifier.modifyHSVA(this.emitter, this.colorModifier); 99 | return this; 100 | } 101 | 102 | /** 103 | * @param {number} from 104 | * @param {number} to 105 | * @param {{(begin: number, end: number): ScalarSampler}} ease 106 | */ 107 | modifyScale(from, to, ease) { 108 | this.emitter.sizeModifier = ease(from, to);; 109 | return this; 110 | } 111 | 112 | /** 113 | * @param {{(begin: number, end: number): ScalarSampler}} ease 114 | */ 115 | setScaleout(ease = Ease.easeInCubic) { 116 | if (this.emitter.scaleDelta.pinLastValue() != 0) { 117 | Diagnostics.log(`The particle "${this.emitter.name}" will not scale out perfectly if its "scale delta" is not "0"`); 118 | } 119 | this.modifyScale(0, this.emitter.scale.mul(this.emitter.scaleDelta.add(1)).neg().pinLastValue(), ease); 120 | return this; 121 | } 122 | 123 | /** 124 | * @param {{(begin: number, end: number): ScalarSampler}} ease 125 | */ 126 | setFadeout(ease = Ease.easeInCubic) { 127 | this.modifyAlpha(1, 0, ease); 128 | return this; 129 | } 130 | 131 | /** 132 | * @param {ScalarSignal | Number=} birthrate 133 | * @param {number} duration 134 | * @returns {Promise} 135 | */ 136 | burst(birthrate = 200, duration = 100) { 137 | if (this.burstSubscription) { 138 | this.burstSubscription.unsubscribe(); 139 | if (this.cancellation) 140 | this.cancellation(); 141 | } 142 | 143 | return new Promise((resolve, reject) => { 144 | this.start(birthrate); 145 | 146 | this.burstSubscription = Time.setTimeout(() => { 147 | this.stop(); 148 | resolve(); 149 | }, duration); 150 | 151 | this.cancellation = () => { 152 | reject('you called burst when bursting, it is fine, just a hint'); 153 | } 154 | }); 155 | } 156 | 157 | /** 158 | * @param {ScalarSignal | Number=} hue 159 | * @param {ScalarSignal | Number=} hueDelta 160 | */ 161 | setHue(hue, hueDelta = 0) { 162 | this.color.hue = hue; 163 | this.color.hueDelta = hueDelta; 164 | ParticleHSVA.setHSVA(this.emitter, this.color); 165 | return this; 166 | } 167 | 168 | /** 169 | * @param {ScalarSignal | Number=} saturation 170 | * @param {ScalarSignal | Number=} saturationDelta 171 | */ 172 | setSaturation(saturation, saturationDelta = 0) { 173 | this.color.saturation = saturation; 174 | this.color.saturationDelta = saturationDelta; 175 | ParticleHSVA.setHSVA(this.emitter, this.color); 176 | return this; 177 | } 178 | 179 | /** 180 | * @param {ScalarSignal | Number=} value 181 | * @param {ScalarSignal | Number=} value 182 | */ 183 | setValue(value, valueDelta = 0) { 184 | this.color.value = value; 185 | this.color.valueDelta = valueDelta; 186 | ParticleHSVA.setHSVA(this.emitter, this.color); 187 | return this; 188 | } 189 | 190 | /** 191 | * @param {ScalarSignal | Number=} alpha 192 | * @param {ScalarSignal | Number=} alphaDelta 193 | */ 194 | setAlpha(alpha, alphaDelta = 0) { 195 | this.color.alpha = alpha; 196 | this.color.alphaDelta = alphaDelta; 197 | ParticleHSVA.setHSVA(this.emitter, this.color); 198 | return this; 199 | } 200 | 201 | /** 202 | * @param {ScalarSignal | Number} birthrate 203 | */ 204 | start(birthrate) { 205 | if (this.burstSubscription) { 206 | this.burstSubscription.unsubscribe(); 207 | } 208 | 209 | this.emitter.birthrate = birthrate; 210 | return this; 211 | } 212 | 213 | stop() { 214 | if (this.burstSubscription) { 215 | this.burstSubscription.unsubscribe(); 216 | } 217 | 218 | this.emitter.birthrate = 0; 219 | return this; 220 | } 221 | } 222 | 223 | Particle.findByPath = class { 224 | constructor(emitterPath) { 225 | this.emittersPromise = Scene.root.findByPath(emitterPath); 226 | this.particlesPromise = this.emittersPromise.then(emitters => this.particles = emitters.map(em => new Particle(em))); 227 | } 228 | 229 | /** 230 | * @param {number} begin 231 | * @param {number} end 232 | * @param {{(begin: number, end: number): ScalarSampler}} ease 233 | */ 234 | modifyHue(begin, end, ease) { 235 | this.particlesPromise.then(() => { 236 | this.particles.forEach(em => em.modifyHue(begin, end, ease)); 237 | }); 238 | return this; 239 | } 240 | 241 | /** 242 | * @param {number} begin 243 | * @param {number} end 244 | * @param {{(begin: number, end: number): ScalarSampler}} ease 245 | */ 246 | modifySaturation(begin, end, ease) { 247 | this.particlesPromise.then(() => { 248 | this.particles.forEach(em => em.modifySaturation(begin, end, ease)); 249 | }); 250 | return this; 251 | } 252 | 253 | /** 254 | * @param {number} begin 255 | * @param {number} end 256 | * @param {{(begin: number, end: number): ScalarSampler}} ease 257 | */ 258 | modifyValue(begin, end, ease) { 259 | this.particlesPromise.then(() => { 260 | this.particles.forEach(em => em.modifyValue(begin, end, ease)); 261 | }); 262 | return this; 263 | } 264 | 265 | /** 266 | * @param {number} begin 267 | * @param {number} end 268 | * @param {{(begin: number, end: number): ScalarSampler}} ease 269 | */ 270 | modifyAlpha(begin, end, ease) { 271 | this.particlesPromise.then(() => { 272 | this.particles.forEach(em => em.modifyAlpha(begin, end, ease)); 273 | }); 274 | return this; 275 | } 276 | 277 | /** 278 | * @param {number} from 279 | * @param {number} to 280 | * @param {{(begin: number, end: number): ScalarSampler}} ease 281 | */ 282 | modifyScale(from, to, ease) { 283 | this.particlesPromise.then(() => { 284 | this.particles.forEach(em => em.modifyScale(from, to, ease)); 285 | }); 286 | return this; 287 | } 288 | 289 | /** 290 | * @param {{(begin: number, end: number): ScalarSampler}} ease 291 | */ 292 | setScaleout(ease = Ease.easeInCubic) { 293 | this.particlesPromise.then(() => { 294 | this.particles.forEach(em => em.setScaleout(ease)); 295 | }); 296 | return this; 297 | } 298 | 299 | /** 300 | * @param {{(begin: number, end: number): ScalarSampler}} ease 301 | */ 302 | setFadeout(ease = Ease.easeInCubic) { 303 | this.particlesPromise.then(() => { 304 | this.particles.forEach(em => em.setFadeout(ease)); 305 | }); 306 | return this; 307 | } 308 | 309 | /** 310 | * @param {ScalarSignal | Number=} birthrate 311 | * @param {number} duration 312 | * @returns {Promise} 313 | */ 314 | burst(birthrate = 200, duration = 100) { 315 | this.particlesPromise.then(() => { 316 | this.particles.forEach(em => em.burst(birthrate, duration)); 317 | }); 318 | return this; 319 | } 320 | 321 | /** 322 | * @param {ScalarSignal | Number=} hue 323 | * @param {ScalarSignal | Number=} hueDelta 324 | */ 325 | setHue(hue, hueDelta = 0) { 326 | this.particlesPromise.then(() => { 327 | this.particles.forEach(em => em.setHue(hue, hueDelta)); 328 | }); 329 | return this; 330 | } 331 | 332 | /** 333 | * @param {ScalarSignal | Number=} saturation 334 | * @param {ScalarSignal | Number=} saturationDelta 335 | */ 336 | setSaturation(saturation, saturationDelta = 0) { 337 | this.particlesPromise.then(() => { 338 | this.particles.forEach(em => em.setSaturation(saturation, saturationDelta)); 339 | }); 340 | return this; 341 | } 342 | 343 | /** 344 | * @param {ScalarSignal | Number=} value 345 | * @param {ScalarSignal | Number=} value 346 | */ 347 | setValue(value, valueDelta = 0) { 348 | this.particlesPromise.then(() => { 349 | this.particles.forEach(em => em.setValue(value, valueDelta)); 350 | }); 351 | return this; 352 | } 353 | 354 | /** 355 | * @param {ScalarSignal | Number=} alpha 356 | * @param {ScalarSignal | Number=} alphaDelta 357 | */ 358 | setAlpha(alpha, alphaDelta = 0) { 359 | this.particlesPromise.then(() => { 360 | this.particles.forEach(em => em.setAlpha(alpha, alphaDelta)); 361 | }); 362 | return this; 363 | } 364 | 365 | /** 366 | * @param {ScalarSignal | Number} birthrate 367 | */ 368 | start(birthrate) { 369 | this.particlesPromise.then(() => { 370 | this.particles.forEach(em => em.start(birthrate)); 371 | }); 372 | return this; 373 | } 374 | 375 | stop() { 376 | this.particlesPromise.then(() => { 377 | this.particles.forEach(em => em.stop()); 378 | }); 379 | return this; 380 | } 381 | } 382 | 383 | Particle.findAll = class { 384 | constructor(emittersName, recursive = true) { 385 | this.emittersPromise = Scene.root.findAll(emittersName, { recursive: recursive }) 386 | this.particlesPromise = this.emittersPromise.then(emitters => this.particles = emitters.map(em => new Particle(em))); 387 | } 388 | 389 | /** 390 | * @param {number} begin 391 | * @param {number} end 392 | * @param {{(begin: number, end: number): ScalarSampler}} ease 393 | */ 394 | modifyHue(begin, end, ease) { 395 | this.particlesPromise.then(() => { 396 | this.particles.forEach(em => em.modifyHue(begin, end, ease)); 397 | }); 398 | return this; 399 | } 400 | 401 | /** 402 | * @param {number} begin 403 | * @param {number} end 404 | * @param {{(begin: number, end: number): ScalarSampler}} ease 405 | */ 406 | modifySaturation(begin, end, ease) { 407 | this.particlesPromise.then(() => { 408 | this.particles.forEach(em => em.modifySaturation(begin, end, ease)); 409 | }); 410 | return this; 411 | } 412 | 413 | /** 414 | * @param {number} begin 415 | * @param {number} end 416 | * @param {{(begin: number, end: number): ScalarSampler}} ease 417 | */ 418 | modifyValue(begin, end, ease) { 419 | this.particlesPromise.then(() => { 420 | this.particles.forEach(em => em.modifyValue(begin, end, ease)); 421 | }); 422 | return this; 423 | } 424 | 425 | /** 426 | * @param {number} begin 427 | * @param {number} end 428 | * @param {{(begin: number, end: number): ScalarSampler}} ease 429 | */ 430 | modifyAlpha(begin, end, ease) { 431 | this.particlesPromise.then(() => { 432 | this.particles.forEach(em => em.modifyAlpha(begin, end, ease)); 433 | }); 434 | return this; 435 | } 436 | 437 | /** 438 | * @param {number} from 439 | * @param {number} to 440 | * @param {{(begin: number, end: number): ScalarSampler}} ease 441 | */ 442 | modifyScale(from, to, ease) { 443 | this.particlesPromise.then(() => { 444 | this.particles.forEach(em => em.modifyScale(from, to, ease)); 445 | }); 446 | return this; 447 | } 448 | 449 | /** 450 | * @param {{(begin: number, end: number): ScalarSampler}} ease 451 | */ 452 | setScaleout(ease = Ease.easeInCubic) { 453 | this.particlesPromise.then(() => { 454 | this.particles.forEach(em => em.setScaleout(ease)); 455 | }); 456 | return this; 457 | } 458 | 459 | /** 460 | * @param {{(begin: number, end: number): ScalarSampler}} ease 461 | */ 462 | setFadeout(ease = Ease.easeInCubic) { 463 | this.particlesPromise.then(() => { 464 | this.particles.forEach(em => em.setFadeout(ease)); 465 | }); 466 | return this; 467 | } 468 | 469 | /** 470 | * @param {ScalarSignal | Number=} birthrate 471 | * @param {number} duration 472 | * @returns {Promise} 473 | */ 474 | burst(birthrate = 200, duration = 100) { 475 | this.particlesPromise.then(() => { 476 | this.particles.forEach(em => em.burst(birthrate, duration)); 477 | }); 478 | return this; 479 | } 480 | 481 | /** 482 | * @param {ScalarSignal | Number=} hue 483 | * @param {ScalarSignal | Number=} hueDelta 484 | */ 485 | setHue(hue, hueDelta = 0) { 486 | this.particlesPromise.then(() => { 487 | this.particles.forEach(em => em.setHue(hue, hueDelta)); 488 | }); 489 | return this; 490 | } 491 | 492 | /** 493 | * @param {ScalarSignal | Number=} saturation 494 | * @param {ScalarSignal | Number=} saturationDelta 495 | */ 496 | setSaturation(saturation, saturationDelta = 0) { 497 | this.particlesPromise.then(() => { 498 | this.particles.forEach(em => em.setSaturation(saturation, saturationDelta)); 499 | }); 500 | return this; 501 | } 502 | 503 | /** 504 | * @param {ScalarSignal | Number=} value 505 | * @param {ScalarSignal | Number=} value 506 | */ 507 | setValue(value, valueDelta = 0) { 508 | this.particlesPromise.then(() => { 509 | this.particles.forEach(em => em.setValue(value, valueDelta)); 510 | }); 511 | return this; 512 | } 513 | 514 | /** 515 | * @param {ScalarSignal | Number=} alpha 516 | * @param {ScalarSignal | Number=} alphaDelta 517 | */ 518 | setAlpha(alpha, alphaDelta = 0) { 519 | this.particlesPromise.then(() => { 520 | this.particles.forEach(em => em.setAlpha(alpha, alphaDelta)); 521 | }); 522 | return this; 523 | } 524 | 525 | /** 526 | * @param {ScalarSignal | Number} birthrate 527 | */ 528 | start(birthrate) { 529 | this.particlesPromise.then(() => { 530 | this.particles.forEach(em => em.start(birthrate)); 531 | }); 532 | return this; 533 | } 534 | 535 | stop() { 536 | this.particlesPromise.then(() => { 537 | this.particles.forEach(em => em.stop()); 538 | }); 539 | return this; 540 | } 541 | } 542 | 543 | Particle.findFirst = class { 544 | constructor(emitterName) { 545 | this.emitterPromise = Scene.root.findFirst(emitterName); 546 | this.particlePromise = this.emitterPromise.then(emitter => this.particle = new Particle(emitter)); 547 | } 548 | 549 | /** 550 | * @param {number} begin 551 | * @param {number} end 552 | * @param {{(begin: number, end: number): ScalarSampler}} ease 553 | */ 554 | modifyHue(begin, end, ease) { 555 | this.particlePromise.then(() => { 556 | this.particle.modifyHue(begin, end, ease); 557 | }) 558 | return this; 559 | } 560 | 561 | /** 562 | * @param {number} begin 563 | * @param {number} end 564 | * @param {{(begin: number, end: number): ScalarSampler}} ease 565 | */ 566 | modifySaturation(begin, end, ease) { 567 | this.particlePromise.then(() => { 568 | this.particle.modifySaturation(begin, end, ease); 569 | }); 570 | return this; 571 | } 572 | 573 | /** 574 | * @param {number} begin 575 | * @param {number} end 576 | * @param {{(begin: number, end: number): ScalarSampler}} ease 577 | */ 578 | modifyValue(begin, end, ease) { 579 | this.particlePromise.then(() => { 580 | this.particle.modifyValue(begin, end, ease); 581 | }); 582 | return this; 583 | } 584 | 585 | /** 586 | * @param {number} begin 587 | * @param {number} end 588 | * @param {{(begin: number, end: number): ScalarSampler}} ease 589 | */ 590 | modifyAlpha(begin, end, ease) { 591 | this.particlePromise.then(() => { 592 | this.particle.modifyAlpha(begin, end, ease); 593 | }); 594 | return this; 595 | } 596 | 597 | /** 598 | * @param {number} from 599 | * @param {number} to 600 | * @param {{(begin: number, end: number): ScalarSampler}} ease 601 | */ 602 | modifyScale(from, to, ease) { 603 | this.particlePromise.then(() => { 604 | this.particle.modifyScale(from, to, ease); 605 | }); 606 | return this; 607 | } 608 | 609 | /** 610 | * @param {{(begin: number, end: number): ScalarSampler}} ease 611 | */ 612 | setScaleout(ease = Ease.easeInCubic) { 613 | this.particlePromise.then(() => { 614 | this.particle.setScaleout(ease); 615 | }); 616 | return this; 617 | } 618 | 619 | /** 620 | * @param {{(begin: number, end: number): ScalarSampler}} ease 621 | */ 622 | setFadeout(ease = Ease.easeInCubic) { 623 | this.particlePromise.then(() => { 624 | this.particle.setFadeout(ease); 625 | }); 626 | return this; 627 | } 628 | 629 | /** 630 | * @param {ScalarSignal | Number=} birthrate 631 | * @param {number} duration 632 | * @returns {Promise} 633 | */ 634 | burst(birthrate = 200, duration = 100) { 635 | this.particlePromise.then(() => { 636 | this.particle.burst(birthrate, duration); 637 | }); 638 | return this; 639 | } 640 | 641 | /** 642 | * @param {ScalarSignal | Number=} hue 643 | * @param {ScalarSignal | Number=} hueDelta 644 | */ 645 | setHue(hue, hueDelta = 0) { 646 | this.particlePromise.then(() => { 647 | this.particle.setHue(hue, hueDelta); 648 | }); 649 | return this; 650 | } 651 | 652 | /** 653 | * @param {ScalarSignal | Number=} saturation 654 | * @param {ScalarSignal | Number=} saturationDelta 655 | */ 656 | setSaturation(saturation, saturationDelta = 0) { 657 | this.particlePromise.then(() => { 658 | this.particle.setSaturation(saturation, saturationDelta); 659 | }); 660 | return this; 661 | } 662 | 663 | /** 664 | * @param {ScalarSignal | Number=} value 665 | * @param {ScalarSignal | Number=} value 666 | */ 667 | setValue(value, valueDelta = 0) { 668 | this.particlePromise.then(() => { 669 | this.particle.setValue(value, valueDelta); 670 | }); 671 | return this; 672 | } 673 | 674 | /** 675 | * @param {ScalarSignal | Number=} alpha 676 | * @param {ScalarSignal | Number=} alphaDelta 677 | */ 678 | setAlpha(alpha, alphaDelta = 0) { 679 | this.particlePromise.then(() => { 680 | this.particle.setAlpha(alpha, alphaDelta); 681 | }); 682 | return this; 683 | } 684 | 685 | /** 686 | * @param {ScalarSignal | Number} birthrate 687 | */ 688 | start(birthrate) { 689 | this.particlePromise.then(() => { 690 | this.particle.start(birthrate); 691 | }); 692 | return this; 693 | } 694 | 695 | stop() { 696 | this.particlePromise.then(() => { 697 | this.particle.stop(); 698 | }); 699 | return this; 700 | } 701 | } --------------------------------------------------------------------------------