UNPKG

@tsparticles/plugin-emitters

Version:

tsParticles emitters plugin

361 lines (360 loc) 16 kB
import { AnimatableColor, Paint, PixelMode, Vector, calcPositionOrRandomFromSizeRanged, deepExtend, defaultOpacity, getRangeValue, hMax, half, isPointInside, itemFromSingleOrMultiple, lMax, millisecondsToSeconds, percentDenominator, randomInRangeValue, rangeColorToHsl, sMax, safeDocument, } from "@tsparticles/engine"; import { Emitter } from "./Options/Classes/Emitter.js"; import { EmitterSize } from "./Options/Classes/EmitterSize.js"; const defaultLifeDelay = 0, minLifeCount = 0, defaultSpawnDelay = 0, defaultEmitDelay = 0, defaultLifeCount = -1, defaultColorAnimationFactor = 1, colorFactor = 3.6, defaultStrokeWidth = 1; function setParticlesOptionsFillColor(particlesOptions, color, opacity, enable) { const paint = (particlesOptions.paint ??= new Paint()); paint.fill = { color: AnimatableColor.create(undefined, { value: color }), enable, opacity, }; } function setParticlesOptionsStrokeColor(particlesOptions, color, opacity, width) { const paint = (particlesOptions.paint ??= new Paint()); paint.stroke = { color: AnimatableColor.create(undefined, { value: color }), opacity, width, }; } export class EmitterInstance { fill; name; options; position; size; spawnFillColor; spawnFillEnabled; spawnFillOpacity; spawnStrokeColor; spawnStrokeOpacity; spawnStrokeWidth; #container; #currentDuration; #currentEmitDelay; #currentSpawnDelay; #duration; #emitDelay; #firstSpawn; #immortal; #initialPosition; #lifeCount; #mutationObserver; #particlesOptions; #paused; #pluginManager; #removeCallback; #resizeObserver; #shape; #size; #spawnDelay; #startParticlesAdded; constructor(pluginManager, container, removeCallback, options, position) { this.#pluginManager = pluginManager; this.#container = container; this.#removeCallback = removeCallback; this.#currentDuration = 0; this.#currentEmitDelay = 0; this.#currentSpawnDelay = 0; this.#initialPosition = position; if (options instanceof Emitter) { this.options = options; } else { this.options = new Emitter(); this.options.load(options); } this.#spawnDelay = container.retina.reduceFactor ? (getRangeValue(this.options.life.delay ?? defaultLifeDelay) * millisecondsToSeconds) / container.retina.reduceFactor : Infinity; this.position = this.#initialPosition ?? this.#calcPosition(); this.name = this.options.name; this.fill = this.options.fill; this.#firstSpawn = !this.options.life.wait; this.#startParticlesAdded = false; const particlesOptions = deepExtend({}, this.options.particles); particlesOptions.move ??= {}; particlesOptions.move.direction ??= this.options.direction; if (this.options.spawn.fill?.color) { this.spawnFillColor = rangeColorToHsl(this.#pluginManager, this.options.spawn.fill.color); } if (this.options.spawn.stroke?.color) { this.spawnStrokeColor = rangeColorToHsl(this.#pluginManager, this.options.spawn.stroke.color); } this.#paused = !this.options.autoPlay; this.#particlesOptions = particlesOptions; this.#size = this.#calcSize(); this.size = this.#size.mode === PixelMode.percent ? { width: (this.#size.width / percentDenominator) * this.#container.canvas.size.width, height: (this.#size.height / percentDenominator) * this.#container.canvas.size.height, } : { width: this.#size.width, height: this.#size.height }; this.#lifeCount = this.options.life.count ?? defaultLifeCount; this.#immortal = this.#lifeCount <= minLifeCount; if (this.options.domId) { const element = safeDocument().getElementById(this.options.domId); if (element) { this.#mutationObserver = new MutationObserver(() => { this.resize(); }); this.#resizeObserver = new ResizeObserver(() => { this.resize(); }); this.#mutationObserver.observe(element, { attributes: true, attributeFilter: ["style", "width", "height"], }); this.#resizeObserver.observe(element); } } const shapeOptions = this.options.shape, shapeGenerator = this.#pluginManager.emitterShapeManager?.getShapeGenerator(shapeOptions.type); if (shapeGenerator) { this.#shape = shapeGenerator.generate(this.#container, this.position, this.size, this.fill, shapeOptions.options); } this.#container.dispatchEvent("emitterCreated", { emitter: this, }); this.play(); } externalPause() { this.#paused = true; this.pause(); } externalPlay() { this.#paused = false; this.play(); } async init() { await this.#shape?.init(); } pause() { if (this.#paused) { return; } this.#emitDelay = undefined; } play() { if (this.#paused) { return; } if (!((this.#lifeCount > minLifeCount || this.#immortal || !this.options.life.count) && (this.#firstSpawn || this.#currentSpawnDelay >= (this.#spawnDelay ?? defaultSpawnDelay)))) { return; } const container = this.#container; if (this.#emitDelay === undefined) { const delay = getRangeValue(this.options.rate.delay); this.#emitDelay = container.retina.reduceFactor ? (delay * millisecondsToSeconds) / container.retina.reduceFactor : Infinity; } if (this.#lifeCount > minLifeCount || this.#immortal) { this.#prepareToDie(); } } resize() { const initialPosition = this.#initialPosition, container = this.#container; this.position = initialPosition && isPointInside(initialPosition, container.canvas.size, Vector.origin) ? initialPosition : this.#calcPosition(); this.#size = this.#calcSize(); this.size = this.#size.mode === PixelMode.percent ? { width: (this.#size.width / percentDenominator) * container.canvas.size.width, height: (this.#size.height / percentDenominator) * container.canvas.size.height, } : { width: this.#size.width, height: this.#size.height }; this.#shape?.resize(this.position, this.size); } update(delta) { if (this.#paused) { return; } const container = this.#container; if (this.#firstSpawn) { this.#firstSpawn = false; this.#currentSpawnDelay = this.#spawnDelay ?? defaultSpawnDelay; this.#currentEmitDelay = this.#emitDelay ?? defaultEmitDelay; } if (!this.#startParticlesAdded) { this.#startParticlesAdded = true; this.#emitParticles(this.options.startCount); } if (this.#duration !== undefined) { this.#currentDuration += delta.value; if (this.#currentDuration >= this.#duration) { this.pause(); if (this.#spawnDelay !== undefined) { this.#spawnDelay = undefined; } if (!this.#immortal) { this.#lifeCount--; } if (this.#lifeCount > minLifeCount || this.#immortal) { this.position = this.#calcPosition(); this.#shape?.resize(this.position, this.size); this.#spawnDelay = container.retina.reduceFactor ? (getRangeValue(this.options.life.delay ?? defaultLifeDelay) * millisecondsToSeconds) / container.retina.reduceFactor : Infinity; } else { this.#destroy(); } this.#currentDuration -= this.#duration; this.#duration = undefined; } } if (this.#spawnDelay !== undefined) { this.#currentSpawnDelay += delta.value; if (this.#currentSpawnDelay >= this.#spawnDelay) { this.#container.dispatchEvent("emitterPlay"); this.play(); this.#currentSpawnDelay -= this.#spawnDelay; this.#spawnDelay = undefined; } } if (this.#emitDelay !== undefined) { this.#currentEmitDelay += delta.value; if (this.#currentEmitDelay >= this.#emitDelay) { this.#emit(); this.#currentEmitDelay -= this.#emitDelay; } } } #calcPosition() { const container = this.#container; if (this.options.domId) { const element = safeDocument().getElementById(this.options.domId); if (element) { const elRect = element.getBoundingClientRect(), pxRatio = container.retina.pixelRatio; return { x: (elRect.x + elRect.width * half) * pxRatio, y: (elRect.y + elRect.height * half) * pxRatio, }; } } return calcPositionOrRandomFromSizeRanged({ size: container.canvas.size, position: this.options.position, }); } #calcSize() { const container = this.#container; if (this.options.domId) { const element = safeDocument().getElementById(this.options.domId); if (element) { const elRect = element.getBoundingClientRect(); return { width: elRect.width * container.retina.pixelRatio, height: elRect.height * container.retina.pixelRatio, mode: PixelMode.precise, }; } } return (this.options.size ?? (() => { const size = new EmitterSize(); size.load({ height: 0, mode: PixelMode.percent, width: 0, }); return size; })()); } #destroy() { this.#mutationObserver?.disconnect(); this.#mutationObserver = undefined; this.#resizeObserver?.disconnect(); this.#resizeObserver = undefined; this.#removeCallback(this); this.#container.dispatchEvent("emitterDestroyed", { emitter: this, }); } #emit() { if (this.#paused) { return; } const quantity = getRangeValue(this.options.rate.quantity); this.#emitParticles(quantity); } #emitParticles(quantity) { const singleParticlesOptions = (itemFromSingleOrMultiple(this.#particlesOptions) ?? {}), fillHslAnimation = this.options.spawn.fill?.color?.animation, fillEnabled = this.options.spawn.fill?.enable ?? !!this.options.spawn.fill?.color, fillOpacity = this.options.spawn.fill?.opacity === undefined ? defaultOpacity : getRangeValue(this.options.spawn.fill.opacity), strokeHslAnimation = this.options.spawn.stroke?.color?.animation, strokeOpacity = this.options.spawn.stroke?.opacity === undefined ? defaultOpacity : getRangeValue(this.options.spawn.stroke.opacity), strokeWidth = this.options.spawn.stroke?.width === undefined ? defaultStrokeWidth : getRangeValue(this.options.spawn.stroke.width), reduceFactor = this.#container.retina.reduceFactor, needsFillColorAnimation = !!fillHslAnimation, needsStrokeColorAnimation = !!strokeHslAnimation, needsShapeData = !!this.#shape, needsColorAnimation = needsFillColorAnimation || needsStrokeColorAnimation, needsCopy = needsColorAnimation || needsShapeData, maxValues = needsColorAnimation ? { h: hMax, s: sMax, l: lMax } : null, shapeOptions = this.options.shape; for (let i = 0; i < quantity * reduceFactor; i++) { const particlesOptions = needsCopy ? deepExtend({}, singleParticlesOptions) : singleParticlesOptions; this.spawnFillOpacity = fillOpacity; this.spawnFillEnabled = fillEnabled; this.spawnStrokeOpacity = strokeOpacity; this.spawnStrokeWidth = strokeWidth; if (this.spawnFillColor) { if (fillHslAnimation && maxValues) { this.spawnFillColor.h = this.#setColorAnimation(fillHslAnimation.h, this.spawnFillColor.h, maxValues.h, colorFactor); this.spawnFillColor.s = this.#setColorAnimation(fillHslAnimation.s, this.spawnFillColor.s, maxValues.s); this.spawnFillColor.l = this.#setColorAnimation(fillHslAnimation.l, this.spawnFillColor.l, maxValues.l); } setParticlesOptionsFillColor(particlesOptions, this.spawnFillColor, this.spawnFillOpacity, this.spawnFillEnabled); } if (this.spawnStrokeColor) { if (strokeHslAnimation && maxValues) { this.spawnStrokeColor.h = this.#setColorAnimation(strokeHslAnimation.h, this.spawnStrokeColor.h, maxValues.h, colorFactor); this.spawnStrokeColor.s = this.#setColorAnimation(strokeHslAnimation.s, this.spawnStrokeColor.s, maxValues.s); this.spawnStrokeColor.l = this.#setColorAnimation(strokeHslAnimation.l, this.spawnStrokeColor.l, maxValues.l); } setParticlesOptionsStrokeColor(particlesOptions, this.spawnStrokeColor, this.spawnStrokeOpacity, this.spawnStrokeWidth); } let position = this.position; if (this.#shape) { const shapePosData = this.#shape.randomPosition(); if (shapePosData) { position = shapePosData.position; const replaceData = shapeOptions.replace; if (replaceData.color && shapePosData.color) { setParticlesOptionsFillColor(particlesOptions, shapePosData.color, replaceData.opacity ? (shapePosData.opacity ?? defaultOpacity) : defaultOpacity, true); } } else { position = null; } } if (position) { this.#container.particles.addParticle(position, particlesOptions); } } } #prepareToDie() { if (this.#paused) { return; } const duration = this.options.life.duration !== undefined ? getRangeValue(this.options.life.duration) : undefined, minDuration = 0, minLifeCount = 0; if ((this.#lifeCount > minLifeCount || this.#immortal) && duration !== undefined && duration > minDuration) { this.#duration = duration * millisecondsToSeconds; } } #setColorAnimation(animation, initValue, maxValue, factor = defaultColorAnimationFactor) { const container = this.#container; if (!animation.enable) { return initValue; } const colorOffset = randomInRangeValue(animation.offset), delay = getRangeValue(this.options.rate.delay), emitFactor = container.retina.reduceFactor ? (delay * millisecondsToSeconds) / container.retina.reduceFactor : Infinity, colorSpeed = getRangeValue(animation.speed); return (initValue + (colorSpeed * container.fpsLimit) / emitFactor + colorOffset * factor) % maxValue; } }