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.

454 lines (453 loc) 19.5 kB
import { clamp, getRandom, getRandomInRange, getRangeValue, mix, randomInRangeValue, setRangeValue, } from "./MathUtils.js"; import { decayOffset, defaultLoops, defaultOpacity, defaultRgbMin, defaultTime, defaultVelocity, double, hMax, hMin, hPhase, half, identity, lFactor, lMax, lMin, maxNits, midColorValue, millisecondsToSeconds, none, one, percentDenominator, phaseNumerator, randomColorValue, rgbMax, sMax, sMin, sNormalizedOffset, sextuple, triple, } from "../Core/Utils/Constants.js"; import { isArray, isString } from "./TypeUtils.js"; import { AlterType } from "../Enums/Types/AlterType.js"; import { AnimationStatus } from "../Enums/AnimationStatus.js"; import { HdrMode } from "../Enums/Modes/HdrMode.js"; import { itemFromArray } from "./Utils.js"; const styleCache = new Map(), maxStyleCacheSize = 2000, rgbFixedPrecision = 2, hslFixedPrecision = 2, hdrRgbFixedPrecision = 4, hdrHslFixedPrecision = 4, sdrReferenceWhiteNits = 203, acesA = 2.51, acesB = 0.03, acesC = 2.43, acesD = 0.59, acesE = 0.14, saturationBoost = 1.15, lightnessBoost = 1.05, temperatureR = 1.05, temperatureB = 0.95, contrastFactor = 1.1, luminanceR = 0.2126, luminanceG = 0.7152, luminanceB = 0.0722, dynamicLuminanceFactor = 2, channelCount = 3, srgbThreshold = 0.04045, srgbDivisor = 12.92, srgbOffset = 0.055, srgbScale = 1.055, srgbExponent = 2.4, linearThreshold = 0.0031308, linearScale = 12.92; function getCachedStyle(key, generator) { let cached = styleCache.get(key); if (!cached) { cached = generator(); if (styleCache.size > maxStyleCacheSize) { styleCache.clear(); } styleCache.set(key, cached); } return cached; } function acesFilmic(x) { return clamp((x * (acesA * x + acesB)) / (x * (acesC * x + acesD) + acesE), none, one); } function srgbToLinear(c) { return c <= srgbThreshold ? c / srgbDivisor : Math.pow((c + srgbOffset) / srgbScale, srgbExponent); } function linearToSrgb(c) { return c <= linearThreshold ? c * linearScale : srgbScale * Math.pow(c, one / srgbExponent) - srgbOffset; } function applyHdrModeAdjustments(r, g, b, mode, maxChannel) { switch (mode) { case HdrMode.vivid: { const avg = (r + g + b) / channelCount; return { b: clamp(clamp(avg + (b - avg) * saturationBoost, none, maxChannel) * lightnessBoost, none, maxChannel), g: clamp(clamp(avg + (g - avg) * saturationBoost, none, maxChannel) * lightnessBoost, none, maxChannel), r: clamp(clamp(avg + (r - avg) * saturationBoost, none, maxChannel) * lightnessBoost, none, maxChannel), }; } case HdrMode.cinematic: { const tempR = r * temperatureR, tempB = b * temperatureB, avg = (tempR + g + tempB) / channelCount; return { b: clamp(avg + (tempB - avg) * contrastFactor, none, maxChannel), g: clamp(avg + (g - avg) * contrastFactor, none, maxChannel), r: clamp(avg + (tempR - avg) * contrastFactor, none, maxChannel), }; } case HdrMode.dynamic: { const luminance = luminanceR * r + luminanceG * g + luminanceB * b, vividWeight = Math.min(one, luminance * dynamicLuminanceFactor), naturalWeight = one - vividWeight, avg = (r + g + b) / channelCount, vividR = clamp(avg + (r - avg) * saturationBoost, none, maxChannel) * lightnessBoost, vividG = clamp(avg + (g - avg) * saturationBoost, none, maxChannel) * lightnessBoost, vividB = clamp(avg + (b - avg) * saturationBoost, none, maxChannel) * lightnessBoost; return { b: clamp(b * naturalWeight + vividB * vividWeight, none, maxChannel), g: clamp(g * naturalWeight + vividG * vividWeight, none, maxChannel), r: clamp(r * naturalWeight + vividR * vividWeight, none, maxChannel), }; } case HdrMode.standard: return { b, g, r }; case HdrMode.natural: default: return { b, g, r }; } } function stringToRgba(pluginManager, input) { if (!input) { return; } for (const manager of pluginManager.colorManagers.values()) { if (manager.accepts(input)) { return manager.parseString(input); } } return undefined; } export function rangeColorToRgb(pluginManager, input, index, useIndex = true) { if (!input) { return; } const color = isString(input) ? { value: input } : input; if (isString(color.value)) { return colorToRgb(pluginManager, color.value, index, useIndex); } if (isArray(color.value)) { const value = itemFromArray(color.value, index, useIndex); if (!value) { return; } return rangeColorToRgb(pluginManager, { value, }); } for (const manager of pluginManager.colorManagers.values()) { const res = manager.handleRangeColor(color); if (res) { return res; } } return undefined; } export function colorToRgb(pluginManager, input, index, useIndex = true) { if (!input) { return; } const color = isString(input) ? { value: input } : input; if (isString(color.value)) { return color.value === randomColorValue ? getRandomRgbColor() : stringToRgb(pluginManager, color.value); } if (isArray(color.value)) { const value = itemFromArray(color.value, index, useIndex); if (!value) { return; } return colorToRgb(pluginManager, { value, }); } for (const manager of pluginManager.colorManagers.values()) { const res = manager.handleColor(color); if (res) { return res; } } return undefined; } export function rangeColorToHsl(pluginManager, color, index, useIndex = true) { const rgb = rangeColorToRgb(pluginManager, color, index, useIndex); return rgb ? rgbToHsl(rgb) : undefined; } export function rgbToHsl(color) { const r1 = color.r / rgbMax, g1 = color.g / rgbMax, b1 = color.b / rgbMax, max = Math.max(r1, g1, b1), min = Math.min(r1, g1, b1), res = { h: hMin, l: (max + min) * half, s: sMin, }; if (max !== min) { res.s = res.l < half ? (max - min) / (max + min) : (max - min) / (double - max - min); if (r1 === max) { res.h = (g1 - b1) / (max - min); } else if (g1 === max) { res.h = double + (b1 - r1) / (max - min); } else { res.h = double * double + (r1 - g1) / (max - min); } } res.l *= lMax; res.s *= sMax; res.h *= hPhase; if (res.h < hMin) { res.h += hMax; } if (res.h >= hMax) { res.h -= hMax; } return res; } export function stringToAlpha(pluginManager, input) { return stringToRgba(pluginManager, input)?.a; } export function stringToRgb(pluginManager, input) { return stringToRgba(pluginManager, input); } function hslChannel(temp1, temp2, temp3) { const temp3Min = 0, temp3Max = 1; if (temp3 < temp3Min) { temp3++; } if (temp3 > temp3Max) { temp3--; } if (temp3 * sextuple < temp3Max) { return temp1 + (temp2 - temp1) * sextuple * temp3; } if (temp3 * double < temp3Max) { return temp2; } if (temp3 * triple < temp3Max * double) { const temp3Offset = double / triple; return temp1 + (temp2 - temp1) * (temp3Offset - temp3) * sextuple; } return temp1; } export function hslToRgb(hsl) { const h = ((hsl.h % hMax) + hMax) % hMax, s = Math.max(sMin, Math.min(sMax, hsl.s)), l = Math.max(lMin, Math.min(lMax, hsl.l)), hNormalized = h / hMax, sNormalized = s / sMax, lNormalized = l / lMax; if (s === sMin) { const grayscaleValue = Math.round(lNormalized * rgbMax); return { r: grayscaleValue, g: grayscaleValue, b: grayscaleValue }; } const temp1 = lNormalized < half ? lNormalized * (sNormalizedOffset + sNormalized) : lNormalized + sNormalized - lNormalized * sNormalized, temp2 = double * lNormalized - temp1, phaseThird = phaseNumerator / triple, red = Math.min(rgbMax, rgbMax * hslChannel(temp2, temp1, hNormalized + phaseThird)), green = Math.min(rgbMax, rgbMax * hslChannel(temp2, temp1, hNormalized)), blue = Math.min(rgbMax, rgbMax * hslChannel(temp2, temp1, hNormalized - phaseThird)); return { r: Math.round(red), g: Math.round(green), b: Math.round(blue) }; } export function hslToRgbFloat(hsl) { const h = ((hsl.h % hMax) + hMax) % hMax, s = Math.max(sMin, Math.min(sMax, hsl.s)), l = Math.max(lMin, Math.min(lMax, hsl.l)), hNormalized = h / hMax, sNormalized = s / sMax, lNormalized = l / lMax; if (s === sMin) { const grayscaleValue = lNormalized * rgbMax; return { r: grayscaleValue, g: grayscaleValue, b: grayscaleValue }; } const temp1 = lNormalized < half ? lNormalized * (sNormalizedOffset + sNormalized) : lNormalized + sNormalized - lNormalized * sNormalized, temp2 = double * lNormalized - temp1, phaseThird = phaseNumerator / triple, red = Math.min(rgbMax, rgbMax * hslChannel(temp2, temp1, hNormalized + phaseThird)), green = Math.min(rgbMax, rgbMax * hslChannel(temp2, temp1, hNormalized)), blue = Math.min(rgbMax, rgbMax * hslChannel(temp2, temp1, hNormalized - phaseThird)); return { r: red, g: green, b: blue }; } export function hslaToRgba(hsla) { const rgbResult = hslToRgb(hsla); return { a: hsla.a, b: rgbResult.b, g: rgbResult.g, r: rgbResult.r, }; } export function getRandomRgbColor(min, hdr) { const fixedMin = min ?? defaultRgbMin; if (hdr) { return { r: getRandomInRange(fixedMin, rgbMax), g: getRandomInRange(fixedMin, rgbMax), b: getRandomInRange(fixedMin, rgbMax), }; } const fixedMax = rgbMax + identity, getRgbInRangeValue = () => Math.floor(getRandomInRange(fixedMin, fixedMax)); return { b: getRgbInRangeValue(), g: getRgbInRangeValue(), r: getRgbInRangeValue(), }; } export function getStyleFromRgb(color, hdr, opacity, peakNits, mode) { const rgbPrecision = hdr ? hdrRgbFixedPrecision : rgbFixedPrecision, op = opacity ?? defaultOpacity, key = hdr ? `rgb-${color.r.toFixed(rgbPrecision)}-${color.g.toFixed(rgbPrecision)}-${color.b.toFixed(rgbPrecision)}-hdr-${op.toString()}-${(peakNits ?? maxNits).toString()}-${mode ?? HdrMode.standard}` : `rgb-${color.r.toFixed(rgbPrecision)}-${color.g.toFixed(rgbPrecision)}-${color.b.toFixed(rgbPrecision)}-sdr-${op.toString()}`; return getCachedStyle(key, () => hdr ? getHdrStyleFromRgb(color, opacity, peakNits, mode) : getSdrStyleFromRgb(color, opacity)); } function getHdrStyleFromRgb(color, opacity, peakNits = maxNits, mode = HdrMode.standard) { const headroom = peakNits / sdrReferenceWhiteNits, middleGray = 0.18, mapped = hdrToneMapColor(color, headroom, middleGray, mode); return `color(display-p3 ${mapped.r.toString()} ${mapped.g.toString()} ${mapped.b.toString()} / ${(opacity ?? defaultOpacity).toString()})`; } function hdrToneMapColor(color, headroom, middleGray, mode) { const rNorm = color.r / rgbMax, gNorm = color.g / rgbMax, bNorm = color.b / rgbMax; if (mode !== HdrMode.natural && mode !== HdrMode.vivid && mode !== HdrMode.cinematic && mode !== HdrMode.dynamic) { return { b: bNorm, g: gNorm, r: rNorm }; } const maxChannel = Math.max(one, headroom), luminance = luminanceR * rNorm + luminanceG * gNorm + luminanceB * bNorm, mappedLuminance = hdrToneMap(luminance, headroom, middleGray), scale = luminance > none ? mappedLuminance / luminance : one; return applyHdrModeAdjustments(clamp(rNorm * scale, none, maxChannel), clamp(gNorm * scale, none, maxChannel), clamp(bNorm * scale, none, maxChannel), mode, maxChannel); } function hdrToneMap(normalized, headroom, middleGray) { if (normalized <= middleGray) { return acesFilmic(normalized); } const expanded = normalized + (normalized - middleGray) * (headroom - one); return acesFilmic(expanded); } function getSdrStyleFromRgb(color, opacity) { return `rgba(${color.r.toString()}, ${color.g.toString()}, ${color.b.toString()}, ${(opacity ?? defaultOpacity).toString()})`; } export function getStyleFromHsl(color, hdr, opacity, peakNits, mode) { const hslPrecision = hdr ? hdrHslFixedPrecision : hslFixedPrecision, op = opacity ?? defaultOpacity, key = hdr ? `hsl-${color.h.toFixed(hslPrecision)}-${color.s.toFixed(hslPrecision)}-${color.l.toFixed(hslPrecision)}-hdr-${op.toString()}-${(peakNits ?? maxNits).toString()}-${mode ?? HdrMode.standard}` : `hsl-${color.h.toFixed(hslPrecision)}-${color.s.toFixed(hslPrecision)}-${color.l.toFixed(hslPrecision)}-sdr-${op.toString()}`; return getCachedStyle(key, () => hdr ? getStyleFromRgb(hslToRgbFloat(color), true, opacity, peakNits, mode) : `hsla(${color.h.toString()}, ${color.s.toString()}%, ${color.l.toString()}%, ${op.toString()})`); } export function colorMix(color1, color2, size1, size2, hdr) { let rgb1 = color1, rgb2 = color2; if (!("r" in rgb1)) { rgb1 = hdr ? hslToRgbFloat(color1) : hslToRgb(color1); } if (!("r" in rgb2)) { rgb2 = hdr ? hslToRgbFloat(color2) : hslToRgb(color2); } if (hdr) { const lin1 = { b: srgbToLinear(rgb1.b / rgbMax), g: srgbToLinear(rgb1.g / rgbMax), r: srgbToLinear(rgb1.r / rgbMax), }, lin2 = { b: srgbToLinear(rgb2.b / rgbMax), g: srgbToLinear(rgb2.g / rgbMax), r: srgbToLinear(rgb2.r / rgbMax), }, totalWeight = size1 + size2; return { b: linearToSrgb((lin1.b * size1 + lin2.b * size2) / totalWeight) * rgbMax, g: linearToSrgb((lin1.g * size1 + lin2.g * size2) / totalWeight) * rgbMax, r: linearToSrgb((lin1.r * size1 + lin2.r * size2) / totalWeight) * rgbMax, }; } return { b: mix(rgb1.b, rgb2.b, size1, size2), g: mix(rgb1.g, rgb2.g, size1, size2), r: mix(rgb1.r, rgb2.r, size1, size2), }; } export function getLinkColor(p1, p2, linkColor, hdr) { if (linkColor === randomColorValue) { return getRandomRgbColor(); } else if (linkColor === midColorValue) { const sourceColor = p1.getFillColor() ?? p1.getStrokeColor(), destColor = p2?.getFillColor() ?? p2?.getStrokeColor(); if (sourceColor && destColor && p2) { return colorMix(sourceColor, destColor, p1.getRadius(), p2.getRadius(), hdr); } else { const hslColor = sourceColor ?? destColor; if (hslColor) { return hslToRgb(hslColor); } } } else { return linkColor; } return undefined; } export function getLinkRandomColor(pluginManager, optColor, blink, consent) { const color = isString(optColor) ? optColor : optColor.value; if (color === randomColorValue) { if (consent) { return rangeColorToRgb(pluginManager, { value: color, }); } if (blink) { return randomColorValue; } return midColorValue; } else if (color === midColorValue) { return midColorValue; } else { return rangeColorToRgb(pluginManager, { value: color, }); } } export function getHslFromAnimation(animation) { return animation === undefined ? undefined : { h: animation.h.value, s: animation.s.value, l: animation.l.value, }; } export function getHslAnimationFromHsl(hsl, animationOptions, reduceFactor) { const resColor = { h: { enable: false, value: hsl.h, min: hMin, max: hMax, }, s: { enable: false, value: hsl.s, min: sMin, max: sMax, }, l: { enable: false, value: hsl.l, min: lMin, max: lMax, }, }; if (animationOptions) { setColorAnimation(resColor.h, animationOptions.h, reduceFactor); setColorAnimation(resColor.s, animationOptions.s, reduceFactor); setColorAnimation(resColor.l, animationOptions.l, reduceFactor); } return resColor; } function setColorAnimation(colorValue, colorAnimation, reduceFactor) { colorValue.enable = colorAnimation.enable; colorValue.min = colorAnimation.min; colorValue.max = colorAnimation.max; if (colorValue.enable) { colorValue.velocity = (getRangeValue(colorAnimation.speed) / percentDenominator) * reduceFactor; colorValue.decay = decayOffset - getRangeValue(colorAnimation.decay); colorValue.status = AnimationStatus.increasing; colorValue.loops = defaultLoops; colorValue.maxLoops = getRangeValue(colorAnimation.count); colorValue.time = defaultTime; colorValue.delayTime = getRangeValue(colorAnimation.delay) * millisecondsToSeconds; if (!colorAnimation.sync) { colorValue.velocity *= getRandom(); colorValue.value *= getRandom(); } colorValue.initialValue = colorValue.value; colorValue.offset = setRangeValue(colorAnimation.offset); } else { colorValue.velocity = defaultVelocity; } } export function updateColorValue(data, decrease, delta, _hdr) { const minLoops = 0, minDelay = 0, identity = 1, minVelocity = 0, minOffset = 0, velocityFactor = 3.6; if (!data.enable || ((data.maxLoops ?? minLoops) > minLoops && (data.loops ?? minLoops) > (data.maxLoops ?? minLoops))) { return; } data.time ??= 0; if ((data.delayTime ?? minDelay) > minDelay && data.time < (data.delayTime ?? minDelay)) { data.time += delta.value; } if ((data.delayTime ?? minDelay) > minDelay && data.time < (data.delayTime ?? minDelay)) { return; } const offset = data.offset ? randomInRangeValue(data.offset) : minOffset, velocity = (data.velocity ?? minVelocity) * delta.factor + offset * velocityFactor, decay = data.decay ?? identity, max = data.max, min = data.min; if (!decrease || data.status === AnimationStatus.increasing) { data.value += velocity; if (data.value > max) { data.loops ??= 0; data.loops++; if (decrease) { data.status = AnimationStatus.decreasing; } else { data.value -= max; } } } else { data.value -= velocity; if (data.value < min) { data.loops ??= 0; data.loops++; data.status = AnimationStatus.increasing; } } if (data.velocity && decay !== identity) { data.velocity *= decay; } data.value = clamp(data.value, min, max); } export function updateColor(color, delta, hdr) { if (!color) { return; } const { h, s, l } = color; updateColorValue(h, false, delta, hdr); updateColorValue(s, true, delta, hdr); updateColorValue(l, true, delta, hdr); } export function alterHsl(color, type, value) { return { h: color.h, s: color.s, l: color.l + (type === AlterType.darken ? -lFactor : lFactor) * value, }; }