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.

553 lines (552 loc) 22.3 kB
import { Vector, Vector3d } from "./Utils/Vectors.js"; import { alterHsl, getHslFromAnimation } from "../Utils/ColorUtils.js"; import { calcExactPositionOrRandomFromSize, clamp, degToRad, getParticleBaseVelocity, getParticleDirectionAngle, getRandom, getRangeValue, randomInRangeValue, setRangeValue, } from "../Utils/MathUtils.js"; import { deepExtend, getPosition, isInArray, itemFromSingleOrMultiple } from "../Utils/Utils.js"; import { defaultAngle, defaultOpacity, defaultRetryCount, defaultTransform, double, doublePI, half, identity, minZ, randomColorValue, squareExp, triple, tryCountIncrement, zIndexFactorOffset, } from "./Utils/Constants.js"; import { EventType } from "../Enums/Types/EventType.js"; import { MoveDirection } from "../Enums/Directions/MoveDirection.js"; import { OutMode } from "../Enums/Modes/OutMode.js"; import { OutModeDirection } from "../Enums/Directions/OutModeDirection.js"; import { ParticleOutType } from "../Enums/Types/ParticleOutType.js"; import { loadParticlesOptions } from "../Utils/ParticlesOptionsLoader.js"; function loadEffectData(effect, effectOptions, id, reduceDuplicates) { const effectData = effectOptions.options[effect]; return deepExtend({ close: effectOptions.close, }, itemFromSingleOrMultiple(effectData, id, reduceDuplicates)); } function loadShapeData(shape, shapeOptions, id, reduceDuplicates) { const shapeData = shapeOptions.options[shape]; return deepExtend({ close: shapeOptions.close, }, itemFromSingleOrMultiple(shapeData, id, reduceDuplicates)); } function fixOutMode(data) { if (!isInArray(data.outMode, data.checkModes)) { return; } const diameter = data.radius * double; if (data.coord > data.maxCoord - diameter) { data.setCb(-data.radius); } else if (data.coord < diameter) { data.setCb(data.radius); } } function normalizeAngle(angle, modulus) { const normalized = angle % modulus; return normalized < defaultAngle ? normalized + modulus : normalized; } function initParticleState(particle, id, group) { particle.id = id; particle.group = group; particle.justWarped = false; particle.effectClose = true; particle.shapeClose = true; particle.pathRotation = false; particle.lastPathTime = 0; particle.destroyed = false; particle.unbreakable = false; particle.isRotating = false; particle.rotation = 0; particle.misplaced = false; particle.retina = { maxDistance: {}, maxSpeed: 0, moveDrift: 0, moveSpeed: 0, sizeAnimationSpeed: 0, }; particle.size = { value: 1, max: 1, min: 1, enable: false, }; particle.outType = ParticleOutType.normal; particle.ignoresResizeRatio = true; } function resolveParticleOptions(particle, container, pluginManager, overrideOptions) { const mainOptions = container.actualOptions, particlesOptions = loadParticlesOptions(pluginManager, container, mainOptions.particles), reduceDuplicates = particlesOptions.reduceDuplicates; particle.effect = itemFromSingleOrMultiple(particlesOptions.effect.type, particle.id, reduceDuplicates); particle.shape = itemFromSingleOrMultiple(particlesOptions.shape.type, particle.id, reduceDuplicates); const effectOptions = particlesOptions.effect, shapeOptions = particlesOptions.shape; if (overrideOptions) { if (overrideOptions.effect) { const overrideEffectType = overrideOptions.effect.type; if (overrideEffectType && overrideEffectType !== particle.effect) { const effect = itemFromSingleOrMultiple(overrideEffectType, particle.id, reduceDuplicates); if (effect) { particle.effect = effect; } } effectOptions.load(overrideOptions.effect); } if (overrideOptions.shape) { const overrideShapeType = overrideOptions.shape.type; if (overrideShapeType && overrideShapeType !== particle.shape) { const shape = itemFromSingleOrMultiple(overrideShapeType, particle.id, reduceDuplicates); if (shape) { particle.shape = shape; } } shapeOptions.load(overrideOptions.shape); } } if (particle.effect === randomColorValue) { const availableEffects = [...container.effectDrawers.keys()]; particle.effect = availableEffects[Math.floor(getRandom() * availableEffects.length)]; } if (particle.shape === randomColorValue) { const availableShapes = [...container.shapeDrawers.keys()]; particle.shape = availableShapes[Math.floor(getRandom() * availableShapes.length)]; } particle.effectData = particle.effect ? loadEffectData(particle.effect, effectOptions, particle.id, reduceDuplicates) : undefined; particle.shapeData = particle.shape ? loadShapeData(particle.shape, shapeOptions, particle.id, reduceDuplicates) : undefined; particlesOptions.load(overrideOptions); const effectData = particle.effectData, shapeData = particle.shapeData; if (effectData) { particlesOptions.load(effectData.particles); } if (shapeData) { particlesOptions.load(shapeData.particles); } particle.effectClose = effectData?.close ?? particlesOptions.effect.close; particle.shapeClose = shapeData?.close ?? particlesOptions.shape.close; return particlesOptions; } function initParticleDrawers(particle, container) { let effectDrawer, shapeDrawer; if (particle.effect) { effectDrawer = container.effectDrawers.get(particle.effect); } if (effectDrawer?.loadEffect) { effectDrawer.loadEffect(particle); } if (particle.shape) { shapeDrawer = container.shapeDrawers.get(particle.shape); } if (shapeDrawer?.loadShape) { shapeDrawer.loadShape(particle); } const sideCountFunc = shapeDrawer?.getSidesCount; if (sideCountFunc) { particle.sides = sideCountFunc(particle); } } function runUpdaterPreInit(updaters, particle) { for (const updater of updaters) { updater.preInit?.(particle); } } function runUpdaterInit(updaters, particle) { for (const updater of updaters) { updater.init(particle); } } function runDrawerInit(container, particle) { const shapeDrawer = particle.shape ? container.shapeDrawers.get(particle.shape) : undefined, effectDrawer = particle.effect ? container.effectDrawers.get(particle.effect) : undefined; effectDrawer?.particleInit?.(container, particle); shapeDrawer?.particleInit?.(container, particle); } function runParticleCreatedPlugins(container, particle) { for (const plugin of container.particleCreatedPlugins) { plugin.particleCreated?.(particle); } } export class Particle { backColor; destroyed; direction; effect; effectClose; effectData; fillColor; fillEnabled; fillOpacity; group; id; ignoresResizeRatio; initialPosition; initialVelocity; isRotating; justWarped; lastPathTime; misplaced; moveCenter; offset; opacity; options; outType; pathRotation; position; randomIndexData; retina; roll; rotation; shape; shapeClose; shapeData; sides; size; spawning; strokeColor; strokeOpacity; strokeWidth; unbreakable; velocity; zIndexFactor; #cachedOpacityData = { fillOpacity: defaultOpacity, opacity: defaultOpacity, strokeOpacity: defaultOpacity, }; #cachedPosition = Vector3d.origin; #cachedRotateData = { sin: 0, cos: 0 }; #cachedTransform = { a: 1, b: 0, c: 0, d: 1, }; #container; #modifiers = []; #pluginManager; constructor(pluginManager, container) { this.#pluginManager = pluginManager; this.#container = container; } addModifier(modifier) { this.#modifiers.push(modifier); this.#modifiers.sort((a, b) => a.priority - b.priority); } clearModifiers() { this.#modifiers.length = 0; } destroy(override) { if (this.unbreakable || this.destroyed) { return; } this.destroyed = true; this.clearModifiers(); const container = this.#container, shapeDrawer = this.shape ? container.shapeDrawers.get(this.shape) : undefined; shapeDrawer?.particleDestroy?.(this); for (const plugin of container.particleDestroyedPlugins) { plugin.particleDestroyed?.(this, override); } for (const updater of container.particleUpdaters) { updater.particleDestroyed?.(this, override); } this.#container.dispatchEvent(EventType.particleDestroyed, { particle: this, }); } draw(delta) { const container = this.#container, render = container.canvas.render; render.drawParticlePlugins(this, delta); render.drawParticle(this, delta); } getAngle() { return this.rotation + (this.pathRotation ? this.velocity.angle : defaultAngle); } getFillColor() { return this.#getRollColor(this.#applyModifiers(getHslFromAnimation(this.fillColor), m => m.fillColor)); } getMass() { return this.getRadius() ** squareExp * Math.PI * half; } getModifier(id) { return this.#modifiers.find(m => m.id === id); } getOpacity() { const zIndexOptions = this.options.zIndex, zIndexFactor = zIndexFactorOffset - this.zIndexFactor, zOpacityFactor = zIndexFactor ** zIndexOptions.opacityRate, baseOpacity = getRangeValue(this.opacity?.value ?? defaultOpacity), modifierOpacity = this.#applyModifiers(undefined, m => m.opacity), opacity = modifierOpacity ?? baseOpacity, fillOpacity = this.fillOpacity ?? defaultOpacity, strokeOpacity = this.strokeOpacity ?? defaultOpacity; this.#cachedOpacityData.fillOpacity = opacity * fillOpacity * zOpacityFactor; this.#cachedOpacityData.opacity = opacity * zOpacityFactor; this.#cachedOpacityData.strokeOpacity = opacity * strokeOpacity * zOpacityFactor; return this.#cachedOpacityData; } getPosition() { this.#cachedPosition.x = this.position.x + this.offset.x; this.#cachedPosition.y = this.position.y + this.offset.y; this.#cachedPosition.z = this.position.z; return this.#cachedPosition; } getRadius() { return this.#applyModifiers(this.size.value, m => m.radius); } getRotateData() { const angle = this.getAngle(); this.#cachedRotateData.sin = Math.sin(angle); this.#cachedRotateData.cos = Math.cos(angle); return this.#cachedRotateData; } getStrokeColor() { return this.#getRollColor(this.#applyModifiers(getHslFromAnimation(this.strokeColor), m => m.strokeColor)); } getTransformData(externalTransform) { const rotateData = this.getRotateData(), rotating = this.isRotating; this.#cachedTransform.a = rotateData.cos * (externalTransform.a ?? defaultTransform.a); this.#cachedTransform.b = rotating ? rotateData.sin * (externalTransform.b ?? identity) : (externalTransform.b ?? defaultTransform.b); this.#cachedTransform.c = rotating ? -rotateData.sin * (externalTransform.c ?? identity) : (externalTransform.c ?? defaultTransform.c); this.#cachedTransform.d = rotateData.cos * (externalTransform.d ?? defaultTransform.d); return this.#cachedTransform; } init(id, position, overrideOptions, group) { const container = this.#container; initParticleState(this, id, group); this.options = resolveParticleOptions(this, container, this.#pluginManager, overrideOptions); container.retina.initParticle(this); runUpdaterPreInit(container.particleUpdaters, this); this.#initPosition(position); this.initialVelocity = this.#calculateVelocity(); this.velocity = this.initialVelocity.copy(); this.zIndexFactor = this.position.z / container.zLayers; this.sides = 24; initParticleDrawers(this, container); this.spawning = false; runUpdaterInit(container.particleUpdaters, this); runDrawerInit(container, this); runParticleCreatedPlugins(container, this); } isInsideCanvas(direction) { return this.#getInsideCanvasResult({ direction }).inside; } isInsideCanvasForOutMode(outMode, direction) { return this.#getInsideCanvasResult({ direction, outMode }).inside; } isShowingBack() { if (!this.roll) { return false; } const angle = this.roll.angle; if (this.roll.horizontal && this.roll.vertical) { const adjustedAngle = normalizeAngle(angle, doublePI); return adjustedAngle >= Math.PI * half && adjustedAngle < Math.PI * triple * half; } if (this.roll.horizontal) { const adjustedAngle = normalizeAngle(angle + Math.PI * half, doublePI); return adjustedAngle >= Math.PI && adjustedAngle < Math.PI * double; } if (this.roll.vertical) { const adjustedAngle = normalizeAngle(angle, doublePI); return adjustedAngle >= Math.PI && adjustedAngle < Math.PI * double; } return false; } isVisible() { return !this.destroyed && !this.spawning && this.isInsideCanvas(); } removeModifier(id) { const idx = this.#modifiers.findIndex(m => m.id === id); if (idx >= defaultAngle) { this.#modifiers.splice(idx, identity); } } reset() { for (const updater of this.#container.particleUpdaters) { updater.reset?.(this); } } #applyModifiers(base, getter) { let value = base; for (const mod of this.#modifiers) { if (mod.enabled) { const override = getter(mod); if (override !== undefined) { value = override; } } } return value; } #calcPosition(position, zIndex) { let tryCount = defaultRetryCount, posVec = position ? Vector3d.create(position.x, position.y, zIndex) : undefined; const container = this.#container, plugins = container.particlePositionPlugins, outModes = this.options.move.outModes, radius = this.getRadius(), canvasSize = container.canvas.size; for (;;) { for (const plugin of plugins) { const pluginPos = plugin.particlePosition?.(posVec, this); if (pluginPos) { return Vector3d.create(pluginPos.x, pluginPos.y, zIndex); } } const exactPosition = calcExactPositionOrRandomFromSize({ size: canvasSize, position: posVec, }), pos = Vector3d.create(exactPosition.x, exactPosition.y, zIndex); this.#fixHorizontal(pos, radius, outModes.left ?? outModes.default); this.#fixHorizontal(pos, radius, outModes.right ?? outModes.default); this.#fixVertical(pos, radius, outModes.top ?? outModes.default); this.#fixVertical(pos, radius, outModes.bottom ?? outModes.default); let isValidPosition = true; for (const plugin of container.particles.checkParticlePositionPlugins) { isValidPosition = plugin.checkParticlePosition?.(this, pos, tryCount) ?? true; if (!isValidPosition) { break; } } if (isValidPosition) { return pos; } tryCount += tryCountIncrement; posVec = undefined; } } #calculateVelocity() { const moveOptions = this.options.move, baseVelocity = getParticleBaseVelocity(this.direction), res = baseVelocity.copy(); if (moveOptions.direction === MoveDirection.inside || moveOptions.direction === MoveDirection.outside) { return res; } const rad = degToRad(getRangeValue(moveOptions.angle.value)), radOffset = degToRad(getRangeValue(moveOptions.angle.offset)), range = { left: radOffset - rad * half, right: radOffset + rad * half, }; if (!moveOptions.straight) { res.angle += randomInRangeValue(setRangeValue(range.left, range.right)); } if (moveOptions.random && typeof moveOptions.speed === "number") { res.length *= getRandom(); } return res; } #fixHorizontal(pos, radius, outMode) { fixOutMode({ outMode, checkModes: [OutMode.bounce], coord: pos.x, maxCoord: this.#container.canvas.size.width, setCb: (value) => (pos.x += value), radius, }); } #fixVertical(pos, radius, outMode) { fixOutMode({ outMode, checkModes: [OutMode.bounce], coord: pos.y, maxCoord: this.#container.canvas.size.height, setCb: (value) => (pos.y += value), radius, }); } #getDefaultInsideCanvasResult(direction, outMode) { const radius = this.getRadius(), canvasSize = this.#container.canvas.size, position = this.position, isBounce = outMode === OutMode.bounce; if (direction === OutModeDirection.bottom) { return { inside: isBounce ? position.y + radius < canvasSize.height : position.y - radius < canvasSize.height, reason: "default", }; } if (direction === OutModeDirection.left) { return { inside: isBounce ? position.x - radius > defaultAngle : position.x + radius > defaultAngle, reason: "default", }; } if (direction === OutModeDirection.right) { return { inside: isBounce ? position.x + radius < canvasSize.width : position.x - radius < canvasSize.width, reason: "default", }; } if (direction === OutModeDirection.top) { return { inside: isBounce ? position.y - radius > defaultAngle : position.y + radius > defaultAngle, reason: "default", }; } return { inside: position.x >= -radius && position.y >= -radius && position.y <= canvasSize.height + radius && position.x <= canvasSize.width + radius, reason: "default", }; } #getInsideCanvasCallbackData(direction, outMode) { return { canvasSize: this.#container.canvas.size, direction, outMode, particle: this, radius: this.getRadius(), }; } #getInsideCanvasResult(data) { const defaultResult = this.#getDefaultInsideCanvasResult(data.direction, data.outMode), container = this.#container, shapeDrawer = this.shape ? container.shapeDrawers.get(this.shape) : undefined, effectDrawer = this.effect ? container.effectDrawers.get(this.effect) : undefined, shapeCheck = shapeDrawer?.isInsideCanvas, effectCheck = effectDrawer?.isInsideCanvas; if (!shapeCheck && !effectCheck) { return defaultResult; } const callbackData = this.#getInsideCanvasCallbackData(data.direction, data.outMode), shapeResult = shapeCheck ? this.#normalizeInsideCanvasResult(shapeCheck(callbackData), "shape") : undefined, effectResult = effectCheck ? this.#normalizeInsideCanvasResult(effectCheck(callbackData), "effect") : undefined; if (shapeResult && effectResult) { const margin = Math.max(shapeResult.margin ?? defaultAngle, effectResult.margin ?? defaultAngle); return { inside: shapeResult.inside && effectResult.inside, margin: margin > defaultAngle ? margin : undefined, reason: "combined", }; } return shapeResult ?? effectResult ?? defaultResult; } #getRollColor(color) { if (!color || !this.roll || (!this.backColor && !this.roll.alter)) { return color; } if (!this.isShowingBack()) { return color; } if (this.backColor) { return this.backColor; } if (this.roll.alter) { return alterHsl(color, this.roll.alter.type, this.roll.alter.value); } return color; } #initPosition(position) { const container = this.#container, zIndexValue = Math.floor(getRangeValue(this.options.zIndex.value)), initialPosition = this.#calcPosition(position, clamp(zIndexValue, minZ, container.zLayers)); if (!initialPosition) { throw new Error("a valid position cannot be found for particle"); } this.position = initialPosition; this.initialPosition = this.position.copy(); const canvasSize = container.canvas.size; this.moveCenter = { ...getPosition(this.options.move.center, canvasSize), radius: this.options.move.center.radius, mode: this.options.move.center.mode, }; this.direction = getParticleDirectionAngle(this.options.move.direction, this.position, this.moveCenter); switch (this.options.move.direction) { case MoveDirection.inside: this.outType = ParticleOutType.inside; break; case MoveDirection.outside: this.outType = ParticleOutType.outside; break; default: break; } this.offset = Vector.origin; } #normalizeInsideCanvasResult(result, reason) { if (typeof result === "boolean") { return { inside: result, reason, }; } return { inside: result.inside, margin: result.margin, reason: result.reason ?? reason, }; } }