From a074ed34f0cfff8fa44d239a118e8ec740e1b4a9 Mon Sep 17 00:00:00 2001 From: lgtst Date: Thu, 26 Jan 2023 09:45:31 +0000 Subject: [PATCH 1/4] blurfilter for image stim v0; --- src/visual/ImageStim.js | 64 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/visual/ImageStim.js b/src/visual/ImageStim.js index f043579..8d73be3 100644 --- a/src/visual/ImageStim.js +++ b/src/visual/ImageStim.js @@ -47,10 +47,34 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log */ - constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {}) + constructor({ + name, + win, + image, + mask, + pos, + anchor, + units, + ori, + size, + color, + opacity, + contrast, + texRes, + depth, + interpolate, + flipHoriz, + flipVert, + autoDraw, + autoLog, + blurVal + } = {}) { super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); + // Holds an instance of PIXI blur filter. Used if blur value is passed. + this._blurFilter = undefined; + this._addAttribute( "image", image, @@ -94,6 +118,11 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) false, this._onChange(false, false), ); + this._addAttribute( + "blurVal", + blurVal, + 0 + ); // estimate the bounding box: this._estimateBoundingBox(); @@ -234,6 +263,33 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) } } + setBlurVal (blurVal = 0, log = false) + { + this._setAttribute("blurVal", blurVal, log); + if (this._pixi instanceof PIXI.Sprite) + { + if (this._blurFilter === undefined) + { + this._blurFilter = new PIXI.filters.BlurFilter(); + this._blurFilter.blur = blurVal; + } + else + { + this._blurFilter.blur = blurVal; + } + + // this._pixi might get destroyed and recreated again with no filters. + if (this._pixi.filters instanceof Array && this._pixi.filters.indexOf(this._blurFilter) === -1) + { + this._pixi.filters.push(this._blurFilter); + } + else + { + this._pixi.filters = [this._blurFilter]; + } + } + } + /** * Estimate the bounding box. * @@ -276,6 +332,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) if (typeof this._pixi !== "undefined") { + this._pixi.filters = null; this._pixi.destroy(true); } this._pixi = undefined; @@ -359,6 +416,11 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); this._pixi.rotation = -this.ori * Math.PI / 180; + if (this._blurVal > 0) + { + this.setBlurVal(this._blurVal); + } + // re-estimate the bounding box, as the texture's width may now be available: this._estimateBoundingBox(); } From 9a294f8367dc027bbcd88ea37f76efefb8f24e2e Mon Sep 17 00:00:00 2001 From: lgtst Date: Sat, 28 Jan 2023 15:42:00 +0000 Subject: [PATCH 2/4] particle system v01. --- src/visual/ParticleSystem.js | 238 +++++++++++++++++++++++++++++++++++ src/visual/index.js | 1 + 2 files changed, 239 insertions(+) create mode 100644 src/visual/ParticleSystem.js diff --git a/src/visual/ParticleSystem.js b/src/visual/ParticleSystem.js new file mode 100644 index 0000000..c6c3074 --- /dev/null +++ b/src/visual/ParticleSystem.js @@ -0,0 +1,238 @@ +/** + * Grating Stimulus. + * + * @author Nikita Agafonov + * @version 2022.3.0 + * @copyright (c) 2020-2023 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + */ + +import * as PIXI from "pixi.js-legacy"; + +const DEFAULT_POOL_SIZE = 1024; +const DEFAULT_PARTICLE_WIDTH = 10; +const DEFAULT_PARTICLE_HEIGHT = 10; +const DEFAULT_PARTICLE_LIFETIME = 3; // ms +const DEFAULT_PARTICLE_COLOR = 0xffffff; +const DEFAULT_PARTICLES_PER_SEC = 60; +const DEFAULT_PARTICLE_ACCELERATION = 2500; + +class Particle +{ + constructor (cfg) + { + this.x = 0; + this.y = 0; + this.ax = 0; + this.ay = 0; + this.vx = 0; + this.vy = 0; + this.lifeTime = 0; + this.widthChange = 0; + this.heightChange = 0; + this.sprite = undefined; + this.inUse = false; + + if (cfg.particleImage !== undefined) + { + this.sprite = PIXI.Sprite.from(PIXI.Texture.from(cfg.particleImage)); + } + else + { + this.sprite = new PIXI.Sprite(PIXI.Texture.WHITE); + this.sprite.tint = cfg.particleColor || DEFAULT_PARTICLE_COLOR; + } + + // TODO: Should we instead incorporate that in position calculation? + // Consider: accurate spawn position of the particle confined by spawnArea. + this.sprite.anchor.set(0.5); + + this.width = cfg.width || DEFAULT_PARTICLE_WIDTH; + this.height = cfg.height || DEFAULT_PARTICLE_HEIGHT; + } + + set width (w) + { + this._width = w; + this.sprite.width = w; + } + + get width () + { + return this._width; + } + + set height (h) + { + this._height = h; + this.sprite.height = h; + } + + get height () + { + return this._height; + } + + update (dt) + { + const dt2 = dt ** 2; + + // Update position with current velocity. + this.x = this.x + this.vx * dt + this.ax * dt2 * .5; + this.y = this.y + this.vy * dt + this.ay * dt2 * .5; + + // Update velocity with current acceleration. + this.vx = this.ax * dt; + this.vy = this.ay * dt; + + this.sprite.x = this.x; + this.sprite.y = this.y; + + if (this.width > 0) + { + this.width = Math.max(0, this.width + this.widthChange); + } + + if (this.height > 0) + { + this.height = Math.max(0, this.height + this.heightChange); + } + this.lifeTime -= dt; + + if (this.width <= 0 && this.height <= 0) + { + this.lifeTime = 0; + } + + if (this.lifeTime <= 0) + { + this.inUse = false; + } + } +} + +export class ParticleSystem +{ + constructor (cfg = {}) + { + this.x = 0; + this.y = 0; + this._cfg = cfg; + this._particlesPerSec = cfg.particlesPerSec || DEFAULT_PARTICLES_PER_SEC; + this._spawnCoolDown = 0; + this._parentObj = undefined; + this._particlePool = new Array(DEFAULT_POOL_SIZE); + + if (cfg.parentObject !== undefined) + { + this._parentObj = cfg.parentObject; + } + + this._fillParticlePool(cfg); + } + + _fillParticlePool (cfg) + { + let i; + for (i = 0; i < this._particlePool.length; i++) + { + this._particlePool[i] = new Particle(cfg); + } + } + + _setupParticle (p) + { + let spawnAreaWidth = this._cfg.spawnAreaWidth || 0; + let spawnAreaHeight = this._cfg.spawnAreaHeight || 0; + + if (this._parentObj !== undefined && this._cfg.useParentSizeAsSpawnArea) + { + spawnAreaWidth = this._parentObj.width; + spawnAreaHeight = this._parentObj.height; + } + + const spawnOffsetX = Math.random() * spawnAreaWidth - spawnAreaWidth * .5; + const spawnOffsetY = Math.random() * spawnAreaHeight - spawnAreaHeight * .5; + const x = this.x + spawnOffsetX; + const y = this.y + spawnOffsetY; + + p.x = x; + p.y = y; + + p.ax = this._cfg.initialAx || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; + p.ay = this._cfg.initialAy || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; + p.vx = this._cfg.initialVx || 0; + p.vy = this._cfg.initialVy || 0; + p.lifeTime = this._cfg.lifeTime || DEFAULT_PARTICLE_LIFETIME; + p.width = this._cfg.width || DEFAULT_PARTICLE_WIDTH; + p.height = this._cfg.height || DEFAULT_PARTICLE_HEIGHT; + p.widthChange = this._cfg.widthChange || 0; + p.heightChange = this._cfg.heightChange || 0; + + if (this._cfg.particleColor !== undefined) + { + p.sprite.tint = this._cfg.particleColor; + } + else + { + p.sprite.tint = 0xffffff; + } + } + + _spawnParticles (n = 0) + { + let i; + for (i = 0; i < this._particlePool.length && n > 0; i++) + { + if (this._particlePool[i].inUse === false) + { + this._particlePool[i].inUse = true; + n--; + + this._setupParticle(this._particlePool[i]); + this._cfg.container.addChild(this._particlePool[i].sprite); + } + } + } + + update (dt) + { + // Sync with parent object if it exists. + if (this._parentObj !== undefined) + { + this.x = this._parentObj.x; + this.y = this._parentObj.y; + } + + if (this._spawnCoolDown <= 0) + { + this._spawnCoolDown = 1 / this._particlesPerSec; + + // Assuming that we have at least 60FPS. + const frameTime = Math.min(dt, 1 / 60); + const particlesPerFrame = Math.ceil(frameTime / this._spawnCoolDown); + + // TODO: figure out how to calc amount of particles when it's more than 1 per frame. + this._spawnParticles(particlesPerFrame); + } + else + { + this._spawnCoolDown -= dt; + } + + let i; + for (i = 0; i < this._particlePool.length; i++) + { + if (this._particlePool[i].inUse) + { + this._particlePool[i].update(dt); + } + + // Check if particle should be removed. + if (this._particlePool[i].lifeTime <= 0 && this._particlePool[i].sprite.parent) + { + this._cfg.container.removeChild(this._particlePool[i].sprite); + } + } + } +} diff --git a/src/visual/index.js b/src/visual/index.js index 8c604fa..9fd2574 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -13,3 +13,4 @@ export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; export * from "./Survey.js"; +export * from "./ParticleSystem.js"; From 33967660ab8feefadf93f7d166a4ecfb315344b2 Mon Sep 17 00:00:00 2001 From: lgtst Date: Tue, 31 Jan 2023 07:21:38 +0000 Subject: [PATCH 3/4] particle emitter update. Now supports proper initial settings; particles orient themselves in the direction of velocity. --- .../{ParticleSystem.js => ParticleEmitter.js} | 145 ++++++++++++++---- src/visual/index.js | 2 +- 2 files changed, 120 insertions(+), 27 deletions(-) rename src/visual/{ParticleSystem.js => ParticleEmitter.js} (59%) diff --git a/src/visual/ParticleSystem.js b/src/visual/ParticleEmitter.js similarity index 59% rename from src/visual/ParticleSystem.js rename to src/visual/ParticleEmitter.js index c6c3074..ae7fb23 100644 --- a/src/visual/ParticleSystem.js +++ b/src/visual/ParticleEmitter.js @@ -12,10 +12,10 @@ import * as PIXI from "pixi.js-legacy"; const DEFAULT_POOL_SIZE = 1024; const DEFAULT_PARTICLE_WIDTH = 10; const DEFAULT_PARTICLE_HEIGHT = 10; -const DEFAULT_PARTICLE_LIFETIME = 3; // ms +const DEFAULT_PARTICLE_LIFETIME = 3; // Seconds. const DEFAULT_PARTICLE_COLOR = 0xffffff; const DEFAULT_PARTICLES_PER_SEC = 60; -const DEFAULT_PARTICLE_ACCELERATION = 2500; +const DEFAULT_PARTICLE_V = 100; class Particle { @@ -47,8 +47,8 @@ class Particle // Consider: accurate spawn position of the particle confined by spawnArea. this.sprite.anchor.set(0.5); - this.width = cfg.width || DEFAULT_PARTICLE_WIDTH; - this.height = cfg.height || DEFAULT_PARTICLE_HEIGHT; + this.width = cfg.particleWidth || DEFAULT_PARTICLE_WIDTH; + this.height = cfg.particleHeight || DEFAULT_PARTICLE_HEIGHT; } set width (w) @@ -75,15 +75,17 @@ class Particle update (dt) { - const dt2 = dt ** 2; + const dt2 = dt * dt; - // Update position with current velocity. + // Update velocity with current acceleration. + this.vx += this.ax * dt; + this.vy += this.ay * dt; + + // Update position with current velocity and acceleration. this.x = this.x + this.vx * dt + this.ax * dt2 * .5; this.y = this.y + this.vy * dt + this.ay * dt2 * .5; - // Update velocity with current acceleration. - this.vx = this.ax * dt; - this.vy = this.ay * dt; + this.sprite.rotation = Math.atan2(this.vy, this.vx); this.sprite.x = this.x; this.sprite.y = this.y; @@ -111,7 +113,7 @@ class Particle } } -export class ParticleSystem +export class ParticleEmitter { constructor (cfg = {}) { @@ -122,12 +124,7 @@ export class ParticleSystem this._spawnCoolDown = 0; this._parentObj = undefined; this._particlePool = new Array(DEFAULT_POOL_SIZE); - - if (cfg.parentObject !== undefined) - { - this._parentObj = cfg.parentObject; - } - + this.setParentObject(cfg.parentObject); this._fillParticlePool(cfg); } @@ -159,15 +156,50 @@ export class ParticleSystem p.x = x; p.y = y; - p.ax = this._cfg.initialAx || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; - p.ay = this._cfg.initialAy || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; - p.vx = this._cfg.initialVx || 0; - p.vy = this._cfg.initialVy || 0; + p.ax = 0; + p.ay = 0; + + if (Number.isFinite(this._cfg.initialVx)) + { + p.vx = this._cfg.initialVx; + } + else if (this._cfg.initialVx instanceof Array && this._cfg.initialVx.length >= 2) + { + p.vx = Math.random() * (this._cfg.initialVx[1] - this._cfg.initialVx[0]) + this._cfg.initialVx[0]; + } + else + { + p.vx = Math.random() * DEFAULT_PARTICLE_V - DEFAULT_PARTICLE_V * .5; + } + + if (Number.isFinite(this._cfg.initialVy)) + { + p.vy = this._cfg.initialVy; + } + else if (this._cfg.initialVy instanceof Array && this._cfg.initialVy.length >= 2) + { + p.vy = Math.random() * (this._cfg.initialVy[1] - this._cfg.initialVy[0]) + this._cfg.initialVy[0]; + } + else + { + p.vy = Math.random() * DEFAULT_PARTICLE_V - DEFAULT_PARTICLE_V * .5; + } + p.lifeTime = this._cfg.lifeTime || DEFAULT_PARTICLE_LIFETIME; - p.width = this._cfg.width || DEFAULT_PARTICLE_WIDTH; - p.height = this._cfg.height || DEFAULT_PARTICLE_HEIGHT; - p.widthChange = this._cfg.widthChange || 0; - p.heightChange = this._cfg.heightChange || 0; + p.width = this._cfg.particleWidth || DEFAULT_PARTICLE_WIDTH; + p.height = this._cfg.particleHeight || DEFAULT_PARTICLE_HEIGHT; + p.widthChange = this._cfg.particleWidthChange || 0; + p.heightChange = this._cfg.particleHeightChange || 0; + + // TODO: run proper checks here. + if (this._cfg.particleImage) + { + p.sprite.texture = PIXI.Texture.from(this._cfg.particleImage); + } + else + { + p.sprite.texture = PIXI.Texture.WHITE; + } if (this._cfg.particleColor !== undefined) { @@ -195,8 +227,58 @@ export class ParticleSystem } } + _getResultingExternalForce () + { + let externalForce = [0, 0]; + if (this._cfg.externalForces instanceof Array) + { + let i; + for (i = 0; i < this._cfg.externalForces.length; i++) + { + externalForce[0] += this._cfg.externalForces[i][0]; + externalForce[1] += this._cfg.externalForces[i][1]; + } + } + + return externalForce; + } + + setParentObject (po) + { + this._parentObj = po; + } + + /** + * @desc: Adds external force which acts on a particle + * @param: f - Array with two elements, first is x component, second is y component. + * It's a vector of length L which sets the direction and the margnitude of the force. + * */ + addExternalForce (f) + { + this._cfg.externalForces.push(f); + } + + removeExternalForce (f) + { + const i = this._cfg.externalForces.indexOf(f); + if (i !== -1) + { + this._cfg.externalForces.splice(i, 1); + } + } + + removeExternalForceByIdx (idx) + { + if (this._cfg.externalForces[idx] !== undefined) + { + this._cfg.externalForces.splice(idx, 1); + } + } + update (dt) { + let externalForce; + // Sync with parent object if it exists. if (this._parentObj !== undefined) { @@ -204,6 +286,16 @@ export class ParticleSystem this.y = this._parentObj.y; } + if (Number.isFinite(this._cfg.positionOffsetX)) + { + this.x += this._cfg.positionOffsetX; + } + + if (Number.isFinite(this._cfg.positionOffsetY)) + { + this.y += this._cfg.positionOffsetY; + } + if (this._spawnCoolDown <= 0) { this._spawnCoolDown = 1 / this._particlesPerSec; @@ -211,8 +303,6 @@ export class ParticleSystem // Assuming that we have at least 60FPS. const frameTime = Math.min(dt, 1 / 60); const particlesPerFrame = Math.ceil(frameTime / this._spawnCoolDown); - - // TODO: figure out how to calc amount of particles when it's more than 1 per frame. this._spawnParticles(particlesPerFrame); } else @@ -225,6 +315,9 @@ export class ParticleSystem { if (this._particlePool[i].inUse) { + externalForce = this._getResultingExternalForce(); + this._particlePool[i].ax = externalForce[0]; + this._particlePool[i].ay = externalForce[1]; this._particlePool[i].update(dt); } diff --git a/src/visual/index.js b/src/visual/index.js index 9fd2574..cbbf0a5 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -13,4 +13,4 @@ export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; export * from "./Survey.js"; -export * from "./ParticleSystem.js"; +export * from "./ParticleEmitter.js"; From 3e164977efc56be2bca4f3d2c5892a20a43934d3 Mon Sep 17 00:00:00 2001 From: lightest Date: Fri, 21 Jul 2023 18:32:34 +0100 Subject: [PATCH 4/4] names corrections. --- src/visual/ParticleEmitter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/visual/ParticleEmitter.js b/src/visual/ParticleEmitter.js index ae7fb23..b0f1ed0 100644 --- a/src/visual/ParticleEmitter.js +++ b/src/visual/ParticleEmitter.js @@ -1,8 +1,8 @@ /** - * Grating Stimulus. + * Particle Emitter. * * @author Nikita Agafonov - * @version 2022.3.0 + * @version 2023.2.0 * @copyright (c) 2020-2023 Open Science Tools Ltd. (https://opensciencetools.org) * @license Distributed under the terms of the MIT License */