mz-particles
Version:
TypeScript library for creating particle effects on canvas.
260 lines (201 loc) • 6.92 kB
text/typescript
import { IParticle, ISettings, IState } from '../interfaces';
import { circle } from 'mz-canvas';
import { getRandom, getRandomBoolean, getRandomHexColor, getRandomItemFromArray, Vector2 } from 'mz-math';
import { getPathBBox } from 'mz-svg';
import tinycolor from 'tinycolor2';
import { rgbaToString } from './colors-provider';
/**
* This method is called once on the particles' initialization.
*/
export const createParticles = (options: ISettings, state: IState) : IParticle[] => {
const { $canvas } = state;
const particles: IParticle[] = [];
let svgPathData: string|undefined = undefined;
let svgSize: Vector2|undefined = undefined;
let scaleSize: Vector2|undefined = undefined;
const minSize = options.minSize as number;
const maxSize = options.maxSize as number;
const minSpeed = options.minSpeed as number;
const maxSpeed = options.maxSpeed as number;
const maxScale = options.maxScale as number;
const minScale = options.minScale as number;
for(let i= 0; i< state.vpParticlesNumber; i++) {
const rnd = getRandom(minSize, maxSize);
const particleSize: Vector2 = [rnd, rnd];
// handle case when provided SVG path data -------------
if(options.svgPathData && options.svgPathData.length > 0){
// get random SVG shape from the provided list, and calculate it's size
svgPathData = getRandomItemFromArray(options.svgPathData);
const bbox = getPathBBox(svgPathData);
if(bbox){
svgSize = [
bbox.w,
bbox.h,
];
scaleSize = [
particleSize[0] / bbox.w,
particleSize[1] / bbox.h,
];
}
}
let color;
if(options.particlesColors && options.particlesColors.length > 0){
color = getRandomItemFromArray(options.particlesColors);
}
else{
// when no color is provided ---> get random color
color = getRandomHexColor();
}
const tColor = tinycolor(color);
const tColorRGB = tColor.toRgb();
particles.push({
center: [
getRandom(0, $canvas.width),
getRandom(0, $canvas.height),
],
speed: [
getRandom(minSpeed, maxSpeed),
getRandom(minSpeed, maxSpeed),
],
size: particleSize,
color,
rgbaColor: [tColorRGB.r, tColorRGB.g, tColorRGB.b, tColorRGB.a],
// SVG path props ----------
svgPathData,
svgSize,
scaleSize,
// rotation effect ------------
angleRad: 0,
rotateCounterClockwise: getRandomBoolean(),
// scale effect ---------------
scale: (minScale + maxScale) / 2,
scaleDirection: getRandomItemFromArray([-1, 1]),
// fade in/out effect ---------
opacity: getRandom(0, 1),
opacityDirection: getRandomItemFromArray([-1, 1]),
});
}
return particles;
};
export const moveParticle = (particle: IParticle, options: ISettings, state: IState) : IParticle => {
const { $canvas } = state;
const copy = { ...particle };
const [cx, cy]: Vector2 = particle.center;
const speed: Vector2 = [...particle.speed] as Vector2;
if(cx > $canvas.width || cx < 0) {
speed[0] = -speed[0];
}
if(cy > $canvas.height || cy < 0) {
speed[1] = -speed[1];
}
copy.speed = speed;
copy.center = [cx + copy.speed[0], cy + copy.speed[1]];
if(options.rotate) {
if(particle.rotateCounterClockwise){
copy.angleRad += Math.PI / 180;
}
else{
copy.angleRad -= Math.PI / 180;
}
}
if(options.scaleInOut){
const scaleStep = options.scaleStep as number;
const maxScale = options.maxScale as number;
const minScale = options.minScale as number;
if(copy.scaleDirection > 0){
copy.scale += scaleStep;
}
else{
copy.scale -= scaleStep;
}
if(copy.scale > maxScale){
copy.scale = maxScale;
copy.scaleDirection = -1;
}
if(copy.scale < minScale){
copy.scale = minScale;
copy.scaleDirection = 1;
}
}
if(options.fadeInOut){
const opacityStep = options.opacityStep as number;
if(copy.opacityDirection > 0){
copy.opacity += opacityStep;
}
else{
copy.opacity -= opacityStep;
}
if(copy.opacity > 1){
copy.scale = 1;
copy.opacityDirection = -1;
}
if(copy.opacity < 0){
copy.opacity = 0;
copy.opacityDirection = 1;
}
}
return copy;
};
export const drawParticle = (particle: IParticle, options: ISettings, state: IState) => {
const { ctx } = state;
if(!options.svgPathData){
ctx.save();
const { size } = particle;
let r = size[0];
if(options.scaleInOut){
r *= particle.scale;
}
const fillStyle = options.fadeInOut ?
rgbaToString(
particle.rgbaColor[0],
particle.rgbaColor[1],
particle.rgbaColor[2],
particle.opacity
) :
particle.color;
circle({
cx: particle.center[0],
cy: particle.center[1],
r,
fillStyle,
}, ctx);
ctx.restore();
return;
}
const [w, h] = particle.svgSize ? particle.svgSize : particle.size;
const path = new Path2D(particle.svgPathData);
ctx.save();
const halfWidth = w/2;
const halfHeight = h/2;
const [cx, cy] = [particle.center[0] - halfWidth, particle.center[1] - halfHeight];
ctx.translate(cx, cy);
if(particle.scaleSize){
// scale the path -------------------------------
ctx.translate(halfWidth, halfHeight);
ctx.scale(...particle.scaleSize);
ctx.translate(-halfWidth, -halfHeight);
}
if(options.rotate) {
ctx.translate(halfWidth, halfHeight);
ctx.rotate(particle.angleRad);
ctx.translate(-halfWidth, -halfHeight);
}
if(options.scaleInOut){
ctx.translate(halfWidth, halfHeight);
ctx.scale(particle.scale, particle.scale);
ctx.translate(-halfWidth, -halfHeight);
}
if(options.fadeInOut){
ctx.fillStyle = rgbaToString(
particle.rgbaColor[0],
particle.rgbaColor[1],
particle.rgbaColor[2],
particle.opacity,
);
}
else{
ctx.fillStyle = particle.color;
}
ctx.fill(path);
ctx.restore();
};