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.

499 lines (498 loc) 21 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/OptionsUtils.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); } } export class Particle { backColor; bubble; 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; slow; 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; #pluginManager; constructor(pluginManager, container) { this.#pluginManager = pluginManager; this.#container = container; } destroy(override) { if (this.unbreakable || this.destroyed) { return; } this.destroyed = true; this.bubble.inRange = false; this.slow.inRange = false; 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.bubble.color ?? getHslFromAnimation(this.fillColor)); } getMass() { return this.getRadius() ** squareExp * Math.PI * half; } getOpacity() { const zIndexOptions = this.options.zIndex, zIndexFactor = zIndexFactorOffset - this.zIndexFactor, zOpacityFactor = zIndexFactor ** zIndexOptions.opacityRate, opacity = this.bubble.opacity ?? getRangeValue(this.opacity?.value ?? defaultOpacity), 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.bubble.radius ?? this.size.value; } 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.bubble.color ?? getHslFromAnimation(this.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; this.id = id; this.group = group; this.justWarped = false; this.effectClose = true; this.shapeClose = true; this.pathRotation = false; this.lastPathTime = 0; this.destroyed = false; this.unbreakable = false; this.isRotating = false; this.rotation = 0; this.misplaced = false; this.retina = { maxDistance: {}, maxSpeed: 0, moveDrift: 0, moveSpeed: 0, sizeAnimationSpeed: 0, }; this.size = { value: 1, max: 1, min: 1, enable: false, }; this.outType = ParticleOutType.normal; this.ignoresResizeRatio = true; const mainOptions = container.actualOptions, particlesOptions = loadParticlesOptions(this.#pluginManager, container, mainOptions.particles), reduceDuplicates = particlesOptions.reduceDuplicates, effectType = particlesOptions.effect.type, shapeType = particlesOptions.shape.type; this.effect = itemFromSingleOrMultiple(effectType, this.id, reduceDuplicates); this.shape = itemFromSingleOrMultiple(shapeType, this.id, reduceDuplicates); const effectOptions = particlesOptions.effect, shapeOptions = particlesOptions.shape; if (overrideOptions) { if (overrideOptions.effect?.type && overrideOptions.effect.type !== this.effect) { const overrideEffectType = overrideOptions.effect.type, effect = itemFromSingleOrMultiple(overrideEffectType, this.id, reduceDuplicates); if (effect) { this.effect = effect; effectOptions.load(overrideOptions.effect); } } if (overrideOptions.shape?.type && overrideOptions.shape.type !== this.shape) { const overrideShapeType = overrideOptions.shape.type, shape = itemFromSingleOrMultiple(overrideShapeType, this.id, reduceDuplicates); if (shape) { this.shape = shape; shapeOptions.load(overrideOptions.shape); } } } if (this.effect === randomColorValue) { const availableEffects = [...this.#container.effectDrawers.keys()]; this.effect = availableEffects[Math.floor(getRandom() * availableEffects.length)]; } if (this.shape === randomColorValue) { const availableShapes = [...this.#container.shapeDrawers.keys()]; this.shape = availableShapes[Math.floor(getRandom() * availableShapes.length)]; } this.effectData = this.effect ? loadEffectData(this.effect, effectOptions, this.id, reduceDuplicates) : undefined; this.shapeData = this.shape ? loadShapeData(this.shape, shapeOptions, this.id, reduceDuplicates) : undefined; particlesOptions.load(overrideOptions); const effectData = this.effectData, shapeData = this.shapeData; if (effectData) { particlesOptions.load(effectData.particles); } if (shapeData) { particlesOptions.load(shapeData.particles); } this.effectClose = effectData?.close ?? particlesOptions.effect.close; this.shapeClose = shapeData?.close ?? particlesOptions.shape.close; this.options = particlesOptions; container.retina.initParticle(this); for (const updater of container.particleUpdaters) { updater.preInit?.(this); } this.bubble = { inRange: false, }; this.slow = { inRange: false, factor: 1, }; this.#initPosition(position); this.initialVelocity = this.#calculateVelocity(); this.velocity = this.initialVelocity.copy(); this.zIndexFactor = this.position.z / container.zLayers; this.sides = 24; let effectDrawer, shapeDrawer; if (this.effect) { effectDrawer = container.effectDrawers.get(this.effect); } if (effectDrawer?.loadEffect) { effectDrawer.loadEffect(this); } if (this.shape) { shapeDrawer = container.shapeDrawers.get(this.shape); } if (shapeDrawer?.loadShape) { shapeDrawer.loadShape(this); } const sideCountFunc = shapeDrawer?.getSidesCount; if (sideCountFunc) { this.sides = sideCountFunc(this); } this.spawning = false; for (const updater of container.particleUpdaters) { updater.init(this); } effectDrawer?.particleInit?.(container, this); shapeDrawer?.particleInit?.(container, this); for (const plugin of container.particleCreatedPlugins) { plugin.particleCreated?.(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 normalizedAngle = angle % doublePI, adjustedAngle = normalizedAngle < defaultAngle ? normalizedAngle + doublePI : normalizedAngle; return adjustedAngle >= Math.PI * half && adjustedAngle < Math.PI * triple * half; } if (this.roll.horizontal) { const normalizedAngle = (angle + Math.PI * half) % (Math.PI * double), adjustedAngle = normalizedAngle < defaultAngle ? normalizedAngle + Math.PI * double : normalizedAngle; return adjustedAngle >= Math.PI && adjustedAngle < Math.PI * double; } if (this.roll.vertical) { const normalizedAngle = angle % (Math.PI * double), adjustedAngle = normalizedAngle < defaultAngle ? normalizedAngle + Math.PI * double : normalizedAngle; return adjustedAngle >= Math.PI && adjustedAngle < Math.PI * double; } return false; } isVisible() { return !this.destroyed && !this.spawning && this.isInsideCanvas(); } reset() { for (const updater of this.#container.particleUpdaters) { updater.reset?.(this); } } #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, abortController = new AbortController(), { signal } = abortController; while (!signal.aborted) { 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; } return posVec; }; #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, }; }; }