mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 10:40:54 +00:00
Merge pull request #569 from lightest/2023.2.0
Particle Emitter Prototype and blur filter for image stim.
This commit is contained in:
commit
fc221356ff
@ -46,11 +46,38 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @param {boolean} [options.flipVert= false] - whether or not to flip vertically
|
||||
* @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
|
||||
* @param {ImageStim.AspectRatioStrategy} [options.aspectRatio= ImageStim.AspectRatioStrategy.VARIABLE] - the aspect ratio handling strategy
|
||||
* @param {number} [options.blurVal= 0] - the blur value. Goes 0 to as hish as you like. 0 is no blur.
|
||||
*/
|
||||
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, aspectRatio, autoDraw, autoLog } = {})
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
image,
|
||||
mask,
|
||||
pos,
|
||||
anchor,
|
||||
units,
|
||||
ori,
|
||||
size,
|
||||
color,
|
||||
opacity,
|
||||
contrast,
|
||||
texRes,
|
||||
depth,
|
||||
interpolate,
|
||||
flipHoriz,
|
||||
flipVert,
|
||||
autoDraw,
|
||||
autoLog,
|
||||
aspectRatio,
|
||||
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,
|
||||
@ -100,6 +127,11 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
ImageStim.AspectRatioStrategy.VARIABLE,
|
||||
this._onChange(true, true),
|
||||
);
|
||||
this._addAttribute(
|
||||
"blurVal",
|
||||
blurVal,
|
||||
0
|
||||
);
|
||||
|
||||
// estimate the bounding box:
|
||||
this._estimateBoundingBox();
|
||||
@ -240,6 +272,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.
|
||||
*
|
||||
@ -282,6 +341,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;
|
||||
@ -392,6 +452,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();
|
||||
}
|
||||
|
331
src/visual/ParticleEmitter.js
Normal file
331
src/visual/ParticleEmitter.js
Normal file
@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Particle Emitter.
|
||||
*
|
||||
* @author Nikita Agafonov
|
||||
* @version 2023.2.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; // Seconds.
|
||||
const DEFAULT_PARTICLE_COLOR = 0xffffff;
|
||||
const DEFAULT_PARTICLES_PER_SEC = 60;
|
||||
const DEFAULT_PARTICLE_V = 100;
|
||||
|
||||
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.particleWidth || DEFAULT_PARTICLE_WIDTH;
|
||||
this.height = cfg.particleHeight || 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 * dt;
|
||||
|
||||
// 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;
|
||||
|
||||
this.sprite.rotation = Math.atan2(this.vy, this.vx);
|
||||
|
||||
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 ParticleEmitter
|
||||
{
|
||||
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);
|
||||
this.setParentObject(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 = 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.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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_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)
|
||||
{
|
||||
this.x = this._parentObj.x;
|
||||
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;
|
||||
|
||||
// Assuming that we have at least 60FPS.
|
||||
const frameTime = Math.min(dt, 1 / 60);
|
||||
const particlesPerFrame = Math.ceil(frameTime / this._spawnCoolDown);
|
||||
this._spawnParticles(particlesPerFrame);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._spawnCoolDown -= dt;
|
||||
}
|
||||
|
||||
let i;
|
||||
for (i = 0; i < this._particlePool.length; i++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,4 +13,5 @@ export * from "./TextStim.js";
|
||||
export * from "./VisualStim.js";
|
||||
export * from "./FaceDetector.js";
|
||||
export * from "./Survey.js";
|
||||
export * from "./Progress.js";
|
||||
export * from "./ParticleEmitter.js";
|
||||
export * from "./Progress.js";
|
Loading…
Reference in New Issue
Block a user