modules_particles_particles.js
import Component from "@pencil.js/component";
import Position from "@pencil.js/position";
import { radianCircle, random } from "@pencil.js/math";
/**
* @module Particles
*/
/**
* Particles generator class
* <br><img src="./media/examples/particles.png" alt="particles demo"/>
* @class
* @extends {module:Component}
*/
export default class Particles extends Component {
/**
* @callback OptionsGenerator
* @param {Number} index - Index of the particle
* @return ParticleData
*/
/**
* @callback ParticlesCallback
* @param {ParticleData} data - One particle data
* @param {Number} index - Index of the particle
* @param {...*} params - Additional parameters
*/
/**
* Particles constructor
* @param {PositionDefinition} positionDefinition - Origin for all particles
* @param {Component} base - Blueprint for each particle
* @param {OptionsGenerator} [generator] - Initialization function for all particles data
* @param {ParticlesCallback} [updater] - Function called on each particle draw (should not be computation intensive)
* @param {ParticleOptions} [options] -
*/
constructor (positionDefinition, base, generator, updater, options) {
super(positionDefinition, {
...base.options,
...options,
});
this.base = base;
this.generator = generator;
this.updater = updater;
this.data = [];
}
/**
* Create new particles
* @param {Number} number - Number of particles to generate
* @param {...*} params - Additional parameters for the generator function
* @return {Particles} Itself
*/
generate (number, ...params) {
this.data = this.data.concat([...new Array(number)].map((_, index) => {
const data = {
...Particles.defaultData,
...this.generator ? this.generator(index, ...params) : {},
};
data.position = Position.from(data.position);
return data;
}));
return this;
}
/**
* @inheritDoc
*/
trace (path) {
const basePath = new window.Path2D();
this.base.trace(basePath);
const matrix = new window.DOMMatrix();
const { cos, sin } = Math;
this.data = this.data.filter((data, index) => {
if (this.updater) {
this.updater(data, index);
}
const { position, scale = 1, rotation = 0, ttl } = data;
const scaleOptions = typeof scale === "number" ? [scale, scale] : Position.from(scale).toJSON();
const rotationRadian = rotation * radianCircle;
matrix.a = cos(rotationRadian) * scaleOptions[0];
matrix.b = sin(rotationRadian) * scaleOptions[0];
matrix.c = -sin(rotationRadian) * scaleOptions[1];
matrix.d = cos(rotationRadian) * scaleOptions[1];
matrix.e = position.x;
matrix.f = position.y;
path.addPath(basePath, matrix);
return !ttl || --data.ttl > 0;
});
if (this.options.emit && random() < this.options.frequency) {
const nb = Array.isArray(this.options.emit) ? random(...this.options.emit) : this.options.emit;
this.generate(Math.floor(nb), ...this.options.args);
}
return this;
}
/**
* @inheritDoc
*/
isHover () { // eslint-disable-line class-methods-use-this
return false;
}
/**
* @inheritDoc
*/
toJSON () {
const { base, data } = this;
return {
...super.toJSON(),
base,
data,
};
}
/**
* @inheritDoc
* @param {Object} definition - Particles definition
* @return {Particles}
*/
static from (definition) {
// FIXME
const base = from(definition.base);
const particles = new Particles(definition.position, base, 0);
particles.data = definition.data;
return particles;
}
/**
* @typedef {Object} ParticleOptions
* @extends ComponentOptions
* @prop {Number} [frequency=1] - Frequency of emission (randomized)
* @prop {Number|Array<Number>} [emit] - Number or range of particles emitted
* @prop {Array} [args] - Arguments passed to the generator function
*/
/**
* @type {ParticleOptions}
*/
static get defaultOptions () {
return {
...super.defaultOptions,
frequency: 1,
emit: null,
args: [],
};
}
/**
* @typedef {Object} ParticleData
* @prop {Position} [position=new Position()] - Position of the particle
* @prop {Number} [rotation=0] - Rotation applied to the particle
* @prop {Number|Position} [scale=1] - Scaling ratio or a pair of value for horizontal and vertical scaling
* @prop {Number} [ttl] - Time to live, number of frames the particle is displayed. This number will be decremented and the data removed when it reach 0
*/
/**
* @type {ParticleData}
*/
static get defaultData () {
return {
position: new Position(),
};
}
}