UNPKG

@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.

302 lines (301 loc) • 11.4 kB
import { canvasFirstIndex, canvasTag, fullPercent, generatedAttribute, generatedFalse, generatedTrue, minRadius, percentDenominator, } from "../Core/Utils/Constants.js"; import { collisionVelocity, getDistances, getRandom, getRangeValue } from "./MathUtils.js"; import { isArray, isBoolean, isNull, isObject } from "./TypeUtils.js"; import { OutModeDirection } from "../Enums/Directions/OutModeDirection.js"; import { PixelMode } from "../Enums/Modes/PixelMode.js"; import { Vector } from "../Core/Utils/Vectors.js"; import { getLogger } from "./LogUtils.js"; function isForbiddenKey(key) { return key === "__proto__" || key === "constructor" || key === "prototype"; } function hasMatchMedia() { return typeof matchMedia !== "undefined"; } export function safeDocument() { return globalThis.document; } export function safeMatchMedia(query) { if (!hasMatchMedia()) { return; } return matchMedia(query); } 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 itemFromArray(array, index, useIndex = true) { return array[index !== undefined && useIndex ? index % array.length : Math.floor(getRandom() * array.length)]; } 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 = Object.create(null); } const sourceKeys = Object.keys(source), 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 (isForbiddenKey(key)) { continue; } const v = sourceDict[key]; if (v !== undefined) { destDict[key] = v; } } continue; } for (const key of sourceKeys) { if (isForbiddenKey(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; } 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 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; } let _cachedZIndex, _cachedStyle; export function getFullScreenStyle(zIndex) { if (_cachedZIndex !== zIndex || !_cachedStyle) { _cachedZIndex = 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); } _cachedStyle = fullScreenStyle; } return _cachedStyle; } 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, initializers) { return await Promise.all([...initializers.values()].map(t => t(container))); } export async function getItemMapFromInitializer(container, initializers) { const entries = await Promise.all([...initializers.entries()].map(([key, initializer]) => initializer(container).then(item => [key, item]))); return new Map(entries); } export async function getDataFromUrl(data) { const url = itemFromSingleOrMultiple(data.url, data.index); if (!url) { return data.fallback; } try { const response = await fetch(url); if (response.ok) { const current = (await response.json()); if (isSourceOptionsValue(current)) { return current; } getLogger().error("invalid configuration data while retrieving config file"); } else { getLogger().error(`${response.status.toString()} while retrieving config file`); } } catch (error) { getLogger().error(`error while retrieving config file: ${error}`); } return data.fallback; } function isSourceOptionsValue(value) { if (isArray(value)) { return value.every((entry) => isObject(entry) && !isArray(entry)); } return isObject(value) && !isArray(value); } export function getCanvasFromContainer(domContainer) { let canvasEl; const documentSafe = safeDocument(), isCanvas = domContainer instanceof HTMLCanvasElement || domContainer.tagName.toLowerCase() === canvasTag; if (isCanvas) { canvasEl = domContainer; canvasEl.dataset[generatedAttribute] ??= generatedFalse; if (canvasEl.dataset[generatedAttribute] === generatedTrue) { canvasEl.style.width ||= fullPercent; canvasEl.style.height ||= fullPercent; canvasEl.style.pointerEvents = "none"; canvasEl.style.setProperty("pointer-events", "none"); } } else { const existingCanvases = domContainer.getElementsByTagName(canvasTag), foundCanvas = existingCanvases.item(canvasFirstIndex); if (foundCanvas) { canvasEl = foundCanvas; canvasEl.dataset[generatedAttribute] = generatedFalse; } else { canvasEl = documentSafe.createElement(canvasTag); canvasEl.dataset[generatedAttribute] = generatedTrue; domContainer.appendChild(canvasEl); } canvasEl.style.width ||= fullPercent; canvasEl.style.height ||= fullPercent; canvasEl.style.pointerEvents = "none"; canvasEl.style.setProperty("pointer-events", "none"); } return canvasEl; } export function getDomContainer(id, source) { const documentSafe = safeDocument(); let domContainer = source ?? documentSafe.getElementById(id); if (domContainer) { return domContainer; } domContainer = documentSafe.createElement("canvas"); domContainer.id = id; domContainer.dataset[generatedAttribute] = generatedTrue; documentSafe.body.append(domContainer); return domContainer; }