@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.
488 lines (487 loc) • 18 kB
JavaScript
import { defaultCompositeValue, defaultTransformValue, defaultZoom, minStrokeWidth, minimumSize, originPoint, zIndexFactorOffset, } from "./Utils/Constants.js";
import { getStyleFromHsl, rangeColorToHsl } from "../Utils/ColorUtils.js";
import { DrawLayer } from "../Enums/DrawLayer.js";
import { getLogger } from "../Utils/LogUtils.js";
const fColorIndex = 0, sColorIndex = 1;
function setTransformValue(factor, newFactor, key) {
const newValue = newFactor[key];
if (newValue !== undefined) {
factor[key] = (factor[key] ?? defaultTransformValue) * newValue;
}
}
export class RenderManager {
#backgroundElement;
#backgroundWarnings;
#canvasClearPlugins;
#canvasManager;
#colorPlugins;
#container;
#context;
#contextSettings;
#drawParticlePlugins;
#drawParticlesCleanupPlugins;
#drawParticlesSetupPlugins;
#layers;
#pluginManager;
#postDrawUpdaters;
#preDrawUpdaters;
#reusableColorStyles = {};
#reusablePluginColors = [undefined, undefined];
#reusableTransform = {};
constructor(pluginManager, container, canvasManager) {
this.#pluginManager = pluginManager;
this.#container = container;
this.#canvasManager = canvasManager;
this.#context = null;
this.#backgroundElement = null;
this.#backgroundWarnings = new Set();
this.#preDrawUpdaters = [];
this.#postDrawUpdaters = [];
this.#canvasClearPlugins = [];
this.#colorPlugins = [];
this.#drawParticlePlugins = [];
this.#drawParticlesCleanupPlugins = [];
this.#drawParticlesSetupPlugins = [];
this.#layers = {
0: [],
1: [],
2: [],
3: [],
4: [],
5: [],
6: [],
7: [],
};
}
get settings() {
return this.#contextSettings;
}
canvasClear() {
if (!this.#container.actualOptions.clear) {
return;
}
this.draw(ctx => {
ctx.clearRect(originPoint.x, originPoint.y, this.#canvasManager.size.width, this.#canvasManager.size.height);
});
}
clear() {
for (const plugin of this.#canvasClearPlugins) {
if (plugin.canvasClear?.() ?? false) {
return;
}
}
for (const layer of Object.values(DrawLayer)) {
if (typeof layer === "number") {
for (const plugin of this.#getLayerPlugins(layer)) {
if (plugin.canvasClear?.() ?? false) {
return;
}
}
}
}
this.canvasClear();
}
destroy() {
this.stop();
this.#backgroundElement = null;
this.#backgroundWarnings.clear();
this.#preDrawUpdaters = [];
this.#postDrawUpdaters = [];
this.#canvasClearPlugins = [];
this.#colorPlugins = [];
this.#drawParticlePlugins = [];
this.#drawParticlesCleanupPlugins = [];
this.#drawParticlesSetupPlugins = [];
for (const layer of Object.values(DrawLayer)) {
if (typeof layer === "number") {
this.#layers[layer] = [];
}
}
}
draw(cb) {
const ctx = this.#context;
if (!ctx) {
return;
}
return cb(ctx);
}
drawParticle(particle, delta) {
if (particle.spawning || particle.destroyed) {
return;
}
const radius = particle.getRadius();
if (radius <= minimumSize) {
return;
}
const pfColor = particle.getFillColor(), psColor = particle.getStrokeColor();
let [fColor, sColor] = this.#getPluginParticleColors(particle);
fColor ??= pfColor;
sColor ??= psColor;
if (!fColor && !sColor) {
return;
}
const container = this.#container, zIndexOptions = particle.options.zIndex, zIndexFactor = zIndexFactorOffset - particle.zIndexFactor, { fillOpacity, opacity, strokeOpacity } = particle.getOpacity(), transform = this.#reusableTransform, colorStyles = this.#reusableColorStyles, fill = fColor
? getStyleFromHsl(fColor, container.hdr, fillOpacity * opacity, container.peakNits, container.hdrMode)
: undefined, stroke = sColor
? getStyleFromHsl(sColor, container.hdr, strokeOpacity * opacity, container.peakNits, container.hdrMode)
: fill;
transform.a = transform.b = transform.c = transform.d = undefined;
colorStyles.fill = fill;
colorStyles.stroke = stroke;
this.draw((context) => {
for (const plugin of this.#drawParticlesSetupPlugins) {
plugin.drawParticleSetup?.(context, particle, delta);
}
this.#applyPreDrawUpdaters(context, particle, radius, opacity, colorStyles, transform);
this.#drawParticle({
container,
context,
particle,
delta,
colorStyles,
radius: radius * zIndexFactor ** zIndexOptions.sizeRate,
opacity: opacity,
transform,
});
this.#applyPostDrawUpdaters(particle);
for (const plugin of this.#drawParticlesCleanupPlugins) {
plugin.drawParticleCleanup?.(context, particle, delta);
}
});
}
drawParticlePlugins(particle, delta) {
this.draw(ctx => {
for (const plugin of this.#drawParticlePlugins) {
this.#drawParticlePlugin(ctx, plugin, particle, delta);
}
});
}
drawParticles(delta) {
const { particles, actualOptions } = this.#container;
this.clear();
particles.update(delta);
this.draw(ctx => {
const width = this.#canvasManager.size.width, height = this.#canvasManager.size.height;
if (this.#backgroundElement) {
try {
ctx.drawImage(this.#backgroundElement, originPoint.x, originPoint.y, width, height);
}
catch {
this.#warnOnce("background-element-draw-error", "Error drawing background element onto canvas");
}
}
const background = actualOptions.background;
if (background.draw) {
try {
background.draw(ctx, delta);
}
catch {
this.#warnOnce("background-draw-error", "Error in background.draw callback");
}
}
for (const plugin of this.#getLayerPlugins(DrawLayer.BackgroundMask)) {
plugin.canvasPaint?.();
}
for (const plugin of this.#getLayerPlugins(DrawLayer.CanvasSetup)) {
plugin.drawSettingsSetup?.(ctx, delta);
}
for (const plugin of this.#getLayerPlugins(DrawLayer.PluginContent)) {
plugin.draw?.(ctx, delta);
}
particles.drawParticles(delta);
for (const plugin of this.#getLayerPlugins(DrawLayer.CanvasCleanup)) {
plugin.clearDraw?.(ctx, delta);
plugin.drawSettingsCleanup?.(ctx, delta);
}
});
}
init() {
this.initUpdaters();
this.initPlugins();
this.#resolveBackgroundElement();
this.paint();
}
initPlugins() {
this.#canvasClearPlugins = [];
this.#colorPlugins = [];
this.#drawParticlePlugins = [];
this.#drawParticlesSetupPlugins = [];
this.#drawParticlesCleanupPlugins = [];
for (const layer of Object.values(DrawLayer)) {
if (typeof layer === "number") {
this.#layers[layer] = [];
}
}
for (const plugin of this.#container.plugins) {
if (plugin.particleFillColor ?? plugin.particleStrokeColor) {
this.#colorPlugins.push(plugin);
}
if (plugin.drawParticle) {
this.#drawParticlePlugins.push(plugin);
}
if (plugin.drawParticleSetup) {
this.#drawParticlesSetupPlugins.push(plugin);
}
if (plugin.drawParticleCleanup) {
this.#drawParticlesCleanupPlugins.push(plugin);
}
if (plugin.canvasClear) {
this.#canvasClearPlugins.push(plugin);
}
if (plugin.canvasPaint) {
this.#getLayerPlugins(DrawLayer.BackgroundMask).push(plugin);
}
if (plugin.drawSettingsSetup) {
this.#getLayerPlugins(DrawLayer.CanvasSetup).push(plugin);
}
if (plugin.draw) {
this.#getLayerPlugins(DrawLayer.PluginContent).push(plugin);
}
if (plugin.clearDraw ?? plugin.drawSettingsCleanup) {
this.#getLayerPlugins(DrawLayer.CanvasCleanup).push(plugin);
}
}
}
initUpdaters() {
this.#preDrawUpdaters = [];
this.#postDrawUpdaters = [];
for (const updater of this.#container.particleUpdaters) {
if (updater.afterDraw) {
this.#postDrawUpdaters.push(updater);
}
if (updater.getColorStyles ?? updater.getTransformValues ?? updater.beforeDraw) {
this.#preDrawUpdaters.push(updater);
}
}
}
paint() {
let handled = false;
for (const plugin of this.#getLayerPlugins(DrawLayer.BackgroundMask)) {
handled = plugin.canvasPaint?.() ?? false;
if (handled) {
break;
}
}
if (handled) {
return;
}
this.paintBase();
}
paintBase(baseColor) {
this.draw(ctx => {
ctx.fillStyle = baseColor ?? "rgba(0,0,0,0)";
ctx.fillRect(originPoint.x, originPoint.y, this.#canvasManager.size.width, this.#canvasManager.size.height);
});
}
paintImage(image, opacity) {
this.draw(ctx => {
if (!image) {
return;
}
const prevAlpha = ctx.globalAlpha;
ctx.globalAlpha = opacity;
ctx.drawImage(image, originPoint.x, originPoint.y, this.#canvasManager.size.width, this.#canvasManager.size.height);
ctx.globalAlpha = prevAlpha;
});
}
setContext(context) {
this.#context = context;
if (this.#context) {
this.#context.globalCompositeOperation = defaultCompositeValue;
}
}
setContextSettings(settings) {
this.#contextSettings = settings;
}
stop() {
this.draw(ctx => {
ctx.clearRect(originPoint.x, originPoint.y, this.#canvasManager.size.width, this.#canvasManager.size.height);
});
}
#applyPostDrawUpdaters(particle) {
for (const updater of this.#postDrawUpdaters) {
updater.afterDraw?.(particle);
}
}
#applyPreDrawUpdaters(ctx, particle, radius, zOpacity, colorStyles, transform) {
for (const updater of this.#preDrawUpdaters) {
if (updater.getColorStyles) {
const { fill, stroke } = updater.getColorStyles(particle, ctx, radius, zOpacity);
if (fill) {
colorStyles.fill = fill;
}
if (stroke) {
colorStyles.stroke = stroke;
}
}
if (updater.getTransformValues) {
const updaterTransform = updater.getTransformValues(particle);
for (const key in updaterTransform) {
setTransformValue(transform, updaterTransform, key);
}
}
updater.beforeDraw?.(particle);
}
}
#drawAfterEffect(drawer, data) {
if (!drawer?.drawAfter) {
return;
}
const { particle } = data;
if (!particle.effect) {
return;
}
drawer.drawAfter(data);
}
#drawBeforeEffect(drawer, data) {
if (!drawer?.drawBefore) {
return;
}
const { particle } = data;
if (!particle.effect) {
return;
}
drawer.drawBefore(data);
}
#drawParticle(data) {
const { container, context, particle, delta, colorStyles, radius, opacity, transform } = data, { effectDrawers, shapeDrawers } = container, pos = particle.getPosition(), transformData = particle.getTransformData(transform), drawScale = defaultZoom, drawPosition = {
x: pos.x,
y: pos.y,
};
context.setTransform(transformData.a, transformData.b, transformData.c, transformData.d, pos.x, pos.y);
if (colorStyles.fill) {
context.fillStyle = colorStyles.fill;
}
const fillEnabled = !!particle.fillEnabled, strokeWidth = particle.strokeWidth ?? minStrokeWidth;
context.lineWidth = strokeWidth;
if (colorStyles.stroke) {
context.strokeStyle = colorStyles.stroke;
}
const drawData = {
context,
particle,
radius,
drawRadius: radius * drawScale,
opacity,
delta,
pixelRatio: container.retina.pixelRatio,
fill: fillEnabled,
stroke: strokeWidth > minStrokeWidth,
transformData,
position: { ...pos },
drawPosition,
drawScale,
};
for (const plugin of container.plugins) {
plugin.drawParticleTransform?.(drawData);
}
const effect = particle.effect ? effectDrawers.get(particle.effect) : undefined, shape = particle.shape ? shapeDrawers.get(particle.shape) : undefined;
this.#drawBeforeEffect(effect, drawData);
this.#drawShapeBeforeDraw(shape, drawData);
this.#drawShape(shape, drawData);
this.#drawShapeAfterDraw(shape, drawData);
this.#drawAfterEffect(effect, drawData);
context.resetTransform();
}
#drawParticlePlugin(context, plugin, particle, delta) {
if (!plugin.drawParticle) {
return;
}
plugin.drawParticle(context, particle, delta);
}
#drawShape(drawer, data) {
if (!drawer) {
return;
}
const { context, fill, particle, stroke } = data;
if (!particle.shape) {
return;
}
context.beginPath();
drawer.draw(data);
if (particle.shapeClose) {
context.closePath();
}
if (fill) {
context.fill();
}
if (stroke) {
context.stroke();
}
}
#drawShapeAfterDraw(drawer, data) {
if (!drawer?.afterDraw) {
return;
}
const { particle } = data;
if (!particle.shape) {
return;
}
drawer.afterDraw(data);
}
#drawShapeBeforeDraw(drawer, data) {
if (!drawer?.beforeDraw) {
return;
}
const { particle } = data;
if (!particle.shape) {
return;
}
drawer.beforeDraw(data);
}
#getLayerPlugins(layer) {
return this.#layers[layer];
}
#getPluginParticleColors(particle) {
let fColor, sColor;
for (const plugin of this.#colorPlugins) {
if (!fColor && plugin.particleFillColor) {
fColor = rangeColorToHsl(this.#pluginManager, plugin.particleFillColor(particle));
}
if (!sColor && plugin.particleStrokeColor) {
sColor = rangeColorToHsl(this.#pluginManager, plugin.particleStrokeColor(particle));
}
if (fColor && sColor) {
break;
}
}
this.#reusablePluginColors[fColorIndex] = fColor;
this.#reusablePluginColors[sColorIndex] = sColor;
return this.#reusablePluginColors;
}
#resolveBackgroundElement() {
const background = this.#container.actualOptions.background;
this.#backgroundElement = null;
if (!background.element) {
return;
}
if (typeof background.element === "string") {
if (typeof document !== "undefined") {
const node = document.querySelector(background.element);
if ((typeof HTMLCanvasElement !== "undefined" && node instanceof HTMLCanvasElement) ||
(typeof HTMLVideoElement !== "undefined" && node instanceof HTMLVideoElement) ||
(typeof HTMLImageElement !== "undefined" && node instanceof HTMLImageElement)) {
this.#backgroundElement = node;
}
else if (node) {
this.#warnOnce("background-element-not-supported", `Background element "${background.element}" is not a supported drawable element (canvas, video, or img)`);
}
else {
this.#warnOnce("background-element-not-found", `Background element selector "${background.element}" not found`);
}
}
}
else if ((typeof HTMLCanvasElement !== "undefined" && background.element instanceof HTMLCanvasElement) ||
(typeof OffscreenCanvas !== "undefined" && background.element instanceof OffscreenCanvas) ||
(typeof HTMLVideoElement !== "undefined" && background.element instanceof HTMLVideoElement) ||
(typeof HTMLImageElement !== "undefined" && background.element instanceof HTMLImageElement)) {
this.#backgroundElement = background.element;
}
}
#warnOnce(key, message) {
if (!this.#backgroundWarnings.has(key)) {
this.#backgroundWarnings.add(key);
getLogger().warning(message);
}
}
}