@tsparticles/engine
Version:
Easily create highly customizable particle, confetti and fireworks animations and use them as animated backgrounds for your website. Ready to use components available also for React, Vue.js (2.x and 3.x), Angular, Svelte, jQuery, Preact, Riot.js, Inferno.
453 lines (452 loc) • 16.5 kB
JavaScript
import { clamp, collisionVelocity, getDistances, getRandom, getRangeMax, getRangeMin, getRangeValue, randomInRangeValue, } from "./MathUtils.js";
import { half, millisecondsToSeconds, percentDenominator } from "../Core/Utils/Constants.js";
import { isArray, isBoolean, isNull, isObject } from "./TypeUtils.js";
import { AnimationMode } from "../Enums/Modes/AnimationMode.js";
import { AnimationStatus } from "../Enums/AnimationStatus.js";
import { DestroyType } from "../Enums/Types/DestroyType.js";
import { OutModeDirection } from "../Enums/Directions/OutModeDirection.js";
import { PixelMode } from "../Enums/Modes/PixelMode.js";
import { StartValueType } from "../Enums/Types/StartValueType.js";
import { Vector } from "../Core/Utils/Vectors.js";
const minRadius = 0, minMemoizeSize = 0;
export function memoize(fn, options) {
const cache = new Map(), maxSize = options?.maxSize, ttlMs = options?.ttlMs, keyFn = options?.keyFn, stableStringify = (obj, seen = new WeakSet()) => {
if (obj === null) {
return "null";
}
const t = typeof obj;
if (t === "undefined") {
return "undefined";
}
if (t === "number" || t === "boolean" || t === "string") {
return JSON.stringify(obj);
}
if (t === "function") {
try {
const fn = obj;
return fn.toString();
}
catch {
return '"[Function]"';
}
}
if (t === "symbol") {
try {
return obj.toString();
}
catch {
return '"[Symbol]"';
}
}
if (Array.isArray(obj)) {
return `[${obj.map(i => stableStringify(i, seen)).join(",")}]`;
}
if (seen.has(obj)) {
return '"[Circular]"';
}
seen.add(obj);
const keys = Object.keys(obj).sort();
return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k], seen)}`).join(",")}}`;
}, defaultKeyer = (args) => stableStringify(args), makeKey = (args) => (keyFn ? keyFn(args) : defaultKeyer(args)), ensureBounds = () => {
if (typeof maxSize === "number" && maxSize >= minMemoizeSize) {
while (cache.size > maxSize) {
const firstKey = cache.keys().next().value;
if (firstKey === undefined)
break;
cache.delete(firstKey);
}
}
};
return (...args) => {
const key = makeKey(args), now = Date.now(), entry = cache.get(key);
if (entry !== undefined) {
if (ttlMs && now - entry.ts > ttlMs) {
cache.delete(key);
}
else {
cache.delete(key);
cache.set(key, { value: entry.value, ts: entry.ts });
return entry.value;
}
}
const result = fn(...args);
cache.set(key, { value: result, ts: now });
ensureBounds();
return result;
};
}
export function hasMatchMedia() {
return typeof matchMedia !== "undefined";
}
export function safeDocument() {
return globalThis.document;
}
export function safeMatchMedia(query) {
if (!hasMatchMedia()) {
return;
}
return matchMedia(query);
}
export function safeIntersectionObserver(callback) {
if (typeof IntersectionObserver === "undefined") {
return;
}
return new IntersectionObserver(callback);
}
export function safeMutationObserver(callback) {
if (typeof MutationObserver === "undefined") {
return;
}
return new MutationObserver(callback);
}
export function isInArray(value, array) {
return value === array || (isArray(array) && array.includes(value));
}
export function arrayRandomIndex(array) {
return Math.floor(getRandom() * array.length);
}
export function itemFromArray(array, index, useIndex = true) {
return array[index !== undefined && useIndex ? index % array.length : arrayRandomIndex(array)];
}
export function isPointInside(point, size, offset, radius, direction) {
return areBoundsInside(calculateBounds(point, radius ?? minRadius), size, offset, direction);
}
export function areBoundsInside(bounds, size, offset, direction) {
let inside = true;
if (!direction || direction === OutModeDirection.bottom) {
inside = bounds.top < size.height + offset.x;
}
if (inside && (!direction || direction === OutModeDirection.left)) {
inside = bounds.right > offset.x;
}
if (inside && (!direction || direction === OutModeDirection.right)) {
inside = bounds.left < size.width + offset.y;
}
if (inside && (!direction || direction === OutModeDirection.top)) {
inside = bounds.bottom > offset.y;
}
return inside;
}
export function calculateBounds(point, radius) {
return {
bottom: point.y + radius,
left: point.x - radius,
right: point.x + radius,
top: point.y - radius,
};
}
export function deepExtend(destination, ...sources) {
for (const source of sources) {
if (isNull(source)) {
continue;
}
if (!isObject(source)) {
destination = source;
continue;
}
if (Array.isArray(source)) {
if (!Array.isArray(destination)) {
destination = [];
}
}
else if (!isObject(destination) || Array.isArray(destination)) {
destination = {};
}
const sourceKeys = Object.keys(source), dangerousKeys = new Set(["__proto__", "constructor", "prototype"]), hasNested = sourceKeys.some(k => {
const v = source[k];
return isObject(v) || Array.isArray(v);
});
if (!hasNested) {
const sourceDict = source, destDict = destination;
for (const key of sourceKeys) {
if (dangerousKeys.has(key)) {
continue;
}
if (key in sourceDict) {
const v = sourceDict[key];
if (v !== undefined) {
destDict[key] = v;
}
}
}
continue;
}
for (const key of sourceKeys) {
if (dangerousKeys.has(key)) {
continue;
}
const sourceDict = source, destDict = destination, value = sourceDict[key];
destDict[key] = Array.isArray(value)
? value.map(v => deepExtend(undefined, v))
: deepExtend(destDict[key], value);
}
}
return destination;
}
export function circleBounceDataFromParticle(p) {
return {
position: p.getPosition(),
radius: p.getRadius(),
mass: p.getMass(),
velocity: p.velocity,
factor: Vector.create(getRangeValue(p.options.bounce.horizontal.value), getRangeValue(p.options.bounce.vertical.value)),
};
}
export function circleBounce(p1, p2) {
const { x: xVelocityDiff, y: yVelocityDiff } = p1.velocity.sub(p2.velocity), [pos1, pos2] = [p1.position, p2.position], { dx: xDist, dy: yDist } = getDistances(pos2, pos1), minimumDistance = 0;
if (xVelocityDiff * xDist + yVelocityDiff * yDist < minimumDistance) {
return;
}
const angle = -Math.atan2(yDist, xDist), m1 = p1.mass, m2 = p2.mass, u1 = p1.velocity.rotate(angle), u2 = p2.velocity.rotate(angle), v1 = collisionVelocity(u1, u2, m1, m2), v2 = collisionVelocity(u2, u1, m1, m2), vFinal1 = v1.rotate(-angle), vFinal2 = v2.rotate(-angle);
p1.velocity.x = vFinal1.x * p1.factor.x;
p1.velocity.y = vFinal1.y * p1.factor.y;
p2.velocity.x = vFinal2.x * p2.factor.x;
p2.velocity.y = vFinal2.y * p2.factor.y;
}
export function executeOnSingleOrMultiple(obj, callback) {
const defaultIndex = 0;
return isArray(obj) ? obj.map((item, index) => callback(item, index)) : callback(obj, defaultIndex);
}
export function itemFromSingleOrMultiple(obj, index, useIndex) {
return isArray(obj) ? itemFromArray(obj, index, useIndex) : obj;
}
export function findItemFromSingleOrMultiple(obj, callback) {
if (isArray(obj)) {
return obj.find((t, index) => callback(t, index));
}
const defaultIndex = 0;
return callback(obj, defaultIndex) ? obj : undefined;
}
export function initParticleNumericAnimationValue(options, pxRatio) {
const valueRange = options.value, animationOptions = options.animation, res = {
delayTime: getRangeValue(animationOptions.delay) * millisecondsToSeconds,
enable: animationOptions.enable,
value: getRangeValue(options.value) * pxRatio,
max: getRangeMax(valueRange) * pxRatio,
min: getRangeMin(valueRange) * pxRatio,
loops: 0,
maxLoops: getRangeValue(animationOptions.count),
time: 0,
}, decayOffset = 1;
if (animationOptions.enable) {
res.decay = decayOffset - getRangeValue(animationOptions.decay);
switch (animationOptions.mode) {
case AnimationMode.increase:
res.status = AnimationStatus.increasing;
break;
case AnimationMode.decrease:
res.status = AnimationStatus.decreasing;
break;
case AnimationMode.random:
res.status = getRandom() >= half ? AnimationStatus.increasing : AnimationStatus.decreasing;
break;
default:
break;
}
const autoStatus = animationOptions.mode === AnimationMode.auto;
switch (animationOptions.startValue) {
case StartValueType.min:
res.value = res.min;
if (autoStatus) {
res.status = AnimationStatus.increasing;
}
break;
case StartValueType.max:
res.value = res.max;
if (autoStatus) {
res.status = AnimationStatus.decreasing;
}
break;
case StartValueType.random:
default:
res.value = randomInRangeValue(res);
if (autoStatus) {
res.status = getRandom() >= half ? AnimationStatus.increasing : AnimationStatus.decreasing;
}
break;
}
}
res.initialValue = res.value;
return res;
}
function getPositionOrSize(positionOrSize, canvasSize) {
const isPercent = positionOrSize.mode === PixelMode.percent;
if (!isPercent) {
const { mode: _, ...rest } = positionOrSize;
return rest;
}
const isPosition = "x" in positionOrSize;
if (isPosition) {
return {
x: (positionOrSize.x / percentDenominator) * canvasSize.width,
y: (positionOrSize.y / percentDenominator) * canvasSize.height,
};
}
else {
return {
width: (positionOrSize.width / percentDenominator) * canvasSize.width,
height: (positionOrSize.height / percentDenominator) * canvasSize.height,
};
}
}
export function getPosition(position, canvasSize) {
return getPositionOrSize(position, canvasSize);
}
export function getSize(size, canvasSize) {
return getPositionOrSize(size, canvasSize);
}
function checkDestroy(particle, destroyType, value, minValue, maxValue) {
switch (destroyType) {
case DestroyType.max:
if (value >= maxValue) {
particle.destroy();
}
break;
case DestroyType.min:
if (value <= minValue) {
particle.destroy();
}
break;
default:
break;
}
}
export function updateAnimation(particle, data, changeDirection, destroyType, delta) {
const minLoops = 0, minDelay = 0, identity = 1, minVelocity = 0, minDecay = 1;
if (particle.destroyed ||
!data.enable ||
((data.maxLoops ?? minLoops) > minLoops && (data.loops ?? minLoops) > (data.maxLoops ?? minLoops))) {
return;
}
const velocity = (data.velocity ?? minVelocity) * delta.factor, minValue = data.min, maxValue = data.max, decay = data.decay ?? minDecay;
data.time ??= 0;
if ((data.delayTime ?? minDelay) > minDelay && data.time < (data.delayTime ?? minDelay)) {
data.time += delta.value;
}
if ((data.delayTime ?? minDelay) > minDelay && data.time < (data.delayTime ?? minDelay)) {
return;
}
switch (data.status) {
case AnimationStatus.increasing:
data.value += velocity;
break;
case AnimationStatus.decreasing:
data.value -= velocity;
break;
default:
break;
}
if (data.velocity && decay !== identity) {
data.velocity *= decay;
}
switch (data.status) {
case AnimationStatus.increasing:
if (data.value >= maxValue) {
if (changeDirection) {
data.status = AnimationStatus.decreasing;
}
else {
data.value -= maxValue;
}
data.loops ??= minLoops;
data.loops++;
}
break;
case AnimationStatus.decreasing:
if (data.value <= minValue) {
if (changeDirection) {
data.status = AnimationStatus.increasing;
}
else {
data.value += maxValue;
}
data.loops ??= minLoops;
data.loops++;
}
break;
default:
break;
}
checkDestroy(particle, destroyType, data.value, minValue, maxValue);
if (!particle.destroyed) {
data.value = clamp(data.value, minValue, maxValue);
}
}
export function cloneStyle(style) {
const clonedStyle = safeDocument().createElement("div").style;
for (const key in style) {
const styleKey = style[key];
if (!(key in style) || isNull(styleKey)) {
continue;
}
const styleValue = style.getPropertyValue?.(styleKey);
if (!styleValue) {
continue;
}
const stylePriority = style.getPropertyPriority?.(styleKey);
if (stylePriority) {
clonedStyle.setProperty(styleKey, styleValue, stylePriority);
}
else {
clonedStyle.setProperty(styleKey, styleValue);
}
}
return clonedStyle;
}
function computeFullScreenStyle(zIndex) {
const fullScreenStyle = safeDocument().createElement("div").style, radix = 10, style = {
width: "100%",
height: "100%",
margin: "0",
padding: "0",
borderWidth: "0",
position: "fixed",
zIndex: zIndex.toString(radix),
"z-index": zIndex.toString(radix),
top: "0",
left: "0",
"pointer-events": "none",
};
for (const key in style) {
const value = style[key];
if (value === undefined) {
continue;
}
fullScreenStyle.setProperty(key, value);
}
return fullScreenStyle;
}
export const getFullScreenStyle = memoize(computeFullScreenStyle);
export function manageListener(element, event, handler, add, options) {
if (add) {
let addOptions = { passive: true };
if (isBoolean(options)) {
addOptions.capture = options;
}
else if (options !== undefined) {
addOptions = options;
}
element.addEventListener(event, handler, addOptions);
}
else {
const removeOptions = options;
element.removeEventListener(event, handler, removeOptions);
}
}
export async function getItemsFromInitializer(container, map, initializers, force = false) {
let res = map.get(container);
if (!res || force) {
res = await Promise.all([...initializers.values()].map(t => t(container)));
map.set(container, res);
}
return res;
}
export async function getItemMapFromInitializer(container, map, initializers, force = false) {
let res = map.get(container);
if (!res || force) {
const entries = await Promise.all([...initializers.entries()].map(([key, initializer]) => initializer(container).then(item => [key, item])));
res = new Map(entries);
map.set(container, res);
}
return res;
}