(() => { "use strict"; /* BLUE DOTS BACKGROUND — Webflow-ready Required: data-blue-dots-bg Content obstacle: By default, each dots host searches nearby for ".content-title". If found, dots and their home targets bounce off that rectangle. If not found, the animation behaves exactly like the original version. Optional obstacle attributes on [data-blue-dots-bg]: data-blue-dots-obstacle=".content-title" data-blue-dots-obstacle-padding="6" data-blue-dots-obstacle-restitution="0.92" data-blue-dots-obstacle-refresh="120" data-blue-dots-obstacle-enabled="true" Set data-blue-dots-obstacle="none" to disable the obstacle for one host. */ // ---------- utils ---------- const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); function attr(element, name, fallback = null) { const value = element.getAttribute(name); return value === null ? fallback : value; } function parseNumber(value, fallback) { const number = parseFloat(value); return Number.isFinite(number) ? number : fallback; } function parseBool(value, fallback) { if (value === null || value === undefined) return fallback; const normalized = String(value).trim().toLowerCase(); if ( normalized === "" || normalized === "1" || normalized === "true" || normalized === "yes" ) { return true; } if ( normalized === "0" || normalized === "false" || normalized === "no" ) { return false; } return fallback; } function prefersReducedMotion() { return Boolean( window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches ); } // ---------- shared engine ---------- const instances = new Map(); let rafId = 0; let lastNow = performance.now(); const intersectionObserver = "IntersectionObserver" in window ? new IntersectionObserver( (entries) => { for (const entry of entries) { const instance = instances.get(entry.target); if (instance) { instance.visible = Boolean(entry.isIntersecting); } } }, { threshold: 0.01 } ) : null; function startLoop() { if (rafId) return; lastNow = performance.now(); rafId = requestAnimationFrame(loop); } function stopLoopIfEmpty() { if (instances.size === 0 && rafId) { cancelAnimationFrame(rafId); rafId = 0; } } function loop(now) { const dt = clamp((now - lastNow) / 1000, 0, 0.05); lastNow = now; for (const instance of instances.values()) { if (!instance.visible) continue; if (!instance.canvas.isConnected || !instance.host.isConnected) continue; if (instance.w <= 0 || instance.h <= 0) continue; instance.step(dt, now); instance.draw(); } rafId = requestAnimationFrame(loop); } class BlueDotsInstance { constructor(host) { this.host = host; // Reuse a canvas if Webflow/current DOM already contains one. this.canvas = host.querySelector(':scope > canvas[data-blue-dots-canvas="1"]') || document.createElement("canvas"); this.canvas.setAttribute("data-blue-dots-canvas", "1"); this.canvas.setAttribute("aria-hidden", "true"); this.ctx = this.canvas.getContext("2d", { alpha: true }); // ---------- core options ---------- this.count = Math.max( 0, Math.floor(parseNumber(attr(host, "data-blue-dots-count", "20"), 20)) ); this.radius = Math.max( 0.1, parseNumber(attr(host, "data-blue-dots-size", "7"), 7) ); this.color = ( attr(host, "data-blue-dots-color", "#2E23FF") || "#2E23FF" ).trim(); // Home drift speed, px/s. this.minSpeed = Math.max( 0, parseNumber(attr(host, "data-blue-dots-min-speed", "8"), 8) ); this.maxSpeed = Math.max( this.minSpeed, parseNumber(attr(host, "data-blue-dots-max-speed", "16"), 16) ); this.useDpr = parseBool( attr(host, "data-blue-dots-dpr", "true"), true ); // ---------- pointer interaction ---------- this.repelRadius = Math.max( 0, parseNumber(attr(host, "data-blue-dots-repel-radius", "140"), 140) ); this.repelStrength = Math.max( 0, parseNumber(attr(host, "data-blue-dots-repel-strength", "950"), 950) ); this.spring = Math.max( 0, parseNumber(attr(host, "data-blue-dots-spring", "10"), 10) ); this.damping = clamp( parseNumber(attr(host, "data-blue-dots-damping", "0.90"), 0.9), 0.75, 0.995 ); this.maxVel = Math.max( 20, parseNumber(attr(host, "data-blue-dots-max-vel", "320"), 320) ); this.windStrength = Math.max( 0, parseNumber(attr(host, "data-blue-dots-wind", "0.55"), 0.55) ); this.windMax = Math.max( 0, parseNumber(attr(host, "data-blue-dots-wind-max", "260"), 260) ); this.windFalloff = Math.max( 0.1, parseNumber(attr(host, "data-blue-dots-wind-falloff", "1"), 1) ); this.afterglowMs = Math.max( 0, parseNumber(attr(host, "data-blue-dots-afterglow", "280"), 280) ); this.afterglowBoost = Math.max( 0, parseNumber( attr(host, "data-blue-dots-afterglow-boost", "2.4"), 2.4 ) ); // ---------- outer bounds ---------- const boundsMode = ( attr(host, "data-blue-dots-bounds", "bounce") || "bounce" ) .trim() .toLowerCase(); this.boundsMode = boundsMode === "wrap" ? "wrap" : "bounce"; this.bounceLoss = clamp( parseNumber(attr(host, "data-blue-dots-bounce-loss", "0.96"), 0.96), 0.7, 1 ); // ---------- dot-to-dot collisions ---------- this.enableCollisions = parseBool( attr(host, "data-blue-dots-collide", "true"), true ); this.restitution = clamp( parseNumber( attr(host, "data-blue-dots-restitution", "0.95"), 0.95 ), 0, 1 ); this.collisionIterations = Math.max( 1, Math.floor( parseNumber(attr(host, "data-blue-dots-collide-iters", "1"), 1) ) ); // ---------- content obstacle ---------- this.obstacleEnabled = parseBool( attr(host, "data-blue-dots-obstacle-enabled", "true"), true ); this.obstacleSelector = ( attr(host, "data-blue-dots-obstacle", ".content-title") || ".content-title" ).trim(); if (this.obstacleSelector.toLowerCase() === "none") { this.obstacleEnabled = false; } this.obstaclePadding = Math.max( 0, parseNumber( attr(host, "data-blue-dots-obstacle-padding", "6"), 6 ) ); this.obstacleRestitution = clamp( parseNumber( attr(host, "data-blue-dots-obstacle-restitution", "0.92"), 0.92 ), 0, 1 ); this.obstacleRefreshMs = Math.max( 16, parseNumber( attr(host, "data-blue-dots-obstacle-refresh", "120"), 120 ) ); // ---------- state ---------- this.visible = true; this.w = 0; this.h = 0; this.dpr = 1; this.obstacleEl = null; this.obstacleRect = null; this.lastObstacleUpdate = -Infinity; this.mouse = { x: 0, y: 0, active: false, vx: 0, vy: 0, lastX: 0, lastY: 0, lastT: performance.now(), lastActiveT: 0, }; this.dots = []; this.prepareHostStyles(); this.prepareCanvasStyles(); if (this.canvas.parentNode !== host) { host.insertBefore(this.canvas, host.firstChild); } // ResizeObserver handles both the canvas host and the obstacle. this.resizeObserver = new ResizeObserver(() => this.resize()); this.resizeObserver.observe(host); if (intersectionObserver) { intersectionObserver.observe(host); } this.onPointerMove = (event) => this.handlePointerMove(event); this.onPointerEnter = () => this.handlePointerEnter(); this.onPointerLeave = () => this.handlePointerLeave(); host.addEventListener("pointermove", this.onPointerMove, { passive: true, }); host.addEventListener("pointerenter", this.onPointerEnter, { passive: true, }); host.addEventListener("pointerleave", this.onPointerLeave, { passive: true, }); this.resize(); } // ---------- setup ---------- prepareHostStyles() { const computed = getComputedStyle(this.host); if (computed.position === "static") { this.host.style.position = "relative"; } if (computed.isolation === "auto") { this.host.style.isolation = "isolate"; } if (computed.zIndex === "auto") { this.host.style.zIndex = "0"; } } prepareCanvasStyles() { const style = this.canvas.style; style.position = "absolute"; style.inset = "0"; style.width = "100%"; style.height = "100%"; style.pointerEvents = "none"; style.zIndex = "-1"; style.display = "block"; } resize() { if (!this.ctx) return; const rect = this.host.getBoundingClientRect(); this.w = Math.max(1, rect.width); this.h = Math.max(1, rect.height); this.dpr = this.useDpr ? clamp(window.devicePixelRatio || 1, 1, 2.5) : 1; this.canvas.width = Math.round(this.w * this.dpr); this.canvas.height = Math.round(this.h * this.dpr); this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); this.updateObstacleRect(true); this.resetDots(); } // ---------- obstacle discovery and geometry ---------- findObstacleElement() { if (!this.obstacleEnabled || !this.obstacleSelector) return null; let root = this.host.parentElement; let levelsChecked = 0; while ( root && root !== document.documentElement && levelsChecked < 5 ) { try { const candidate = root.querySelector(this.obstacleSelector); if ( candidate && candidate !== this.host && !this.host.contains(candidate) ) { return candidate; } } catch (error) { console.warn( "[BlueDotsBG] Invalid obstacle selector:", this.obstacleSelector, error ); this.obstacleEnabled = false; return null; } root = root.parentElement; levelsChecked += 1; } return null; } setObservedObstacle(nextObstacle) { if (this.obstacleEl === nextObstacle) return; if (this.obstacleEl && this.resizeObserver) { try { this.resizeObserver.unobserve(this.obstacleEl); } catch (error) {} } this.obstacleEl = nextObstacle; if (this.obstacleEl && this.resizeObserver) { try { this.resizeObserver.observe(this.obstacleEl); } catch (error) {} } } updateObstacleRect(force = false, now = performance.now()) { if (!this.obstacleEnabled) { this.obstacleRect = null; return; } if ( !force && now - this.lastObstacleUpdate < this.obstacleRefreshMs ) { return; } this.lastObstacleUpdate = now; if (!this.obstacleEl || !this.obstacleEl.isConnected) { this.setObservedObstacle(this.findObstacleElement()); } if (!this.obstacleEl) { this.obstacleRect = null; return; } const hostRect = this.host.getBoundingClientRect(); const elementRect = this.obstacleEl.getBoundingClientRect(); const padding = this.obstaclePadding; const rect = { left: elementRect.left - hostRect.left - padding, right: elementRect.right - hostRect.left + padding, top: elementRect.top - hostRect.top - padding, bottom: elementRect.bottom - hostRect.top + padding, }; const overlapsHost = rect.right > 0 && rect.left < this.w && rect.bottom > 0 && rect.top < this.h; const hasArea = rect.right - rect.left > 0.5 && rect.bottom - rect.top > 0.5; // If .content-title does not overlap this canvas, keep original behavior. this.obstacleRect = overlapsHost && hasArea ? rect : null; } getExpandedObstacle(radius = this.radius) { const rect = this.obstacleRect; if (!rect) return null; return { left: rect.left - radius, right: rect.right + radius, top: rect.top - radius, bottom: rect.bottom + radius, }; } isInsideObstacle(x, y, radius = this.radius) { const rect = this.getExpandedObstacle(radius); if (!rect) return false; return ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ); } getCollisionSide(x, y, previousX, previousY, rect) { const candidates = []; const dx = x - previousX; const dy = y - previousY; if (previousX <= rect.left && x > rect.left && Math.abs(dx) > 0.000001) { const time = (rect.left - previousX) / dx; if (time >= 0 && time <= 1) candidates.push({ side: "left", time }); } if ( previousX >= rect.right && x < rect.right && Math.abs(dx) > 0.000001 ) { const time = (rect.right - previousX) / dx; if (time >= 0 && time <= 1) candidates.push({ side: "right", time }); } if (previousY <= rect.top && y > rect.top && Math.abs(dy) > 0.000001) { const time = (rect.top - previousY) / dy; if (time >= 0 && time <= 1) candidates.push({ side: "top", time }); } if ( previousY >= rect.bottom && y < rect.bottom && Math.abs(dy) > 0.000001 ) { const time = (rect.bottom - previousY) / dy; if (time >= 0 && time <= 1) candidates.push({ side: "bottom", time }); } if (candidates.length > 0) { candidates.sort((a, b) => a.time - b.time); return candidates[0].side; } // Fallback for a point that started inside after resize/layout changes. const distances = [ { side: "left", value: Math.abs(x - rect.left) }, { side: "right", value: Math.abs(rect.right - x) }, { side: "top", value: Math.abs(y - rect.top) }, { side: "bottom", value: Math.abs(rect.bottom - y) }, ]; distances.sort((a, b) => a.value - b.value); return distances[0].side; } resolveHomeObstacle(point, previousX, previousY) { const rect = this.getExpandedObstacle(this.radius); if (!rect) return; const isInside = point.hx > rect.left && point.hx < rect.right && point.hy > rect.top && point.hy < rect.bottom; if (!isInside) return; const side = this.getCollisionSide( point.hx, point.hy, previousX, previousY, rect ); const epsilon = 0.01; const restitution = this.obstacleRestitution; if (side === "left") { point.hx = rect.left - epsilon; if (point.hvx > 0) point.hvx = -point.hvx * restitution; } else if (side === "right") { point.hx = rect.right + epsilon; if (point.hvx < 0) point.hvx = -point.hvx * restitution; } else if (side === "top") { point.hy = rect.top - epsilon; if (point.hvy > 0) point.hvy = -point.hvy * restitution; } else { point.hy = rect.bottom + epsilon; if (point.hvy < 0) point.hvy = -point.hvy * restitution; } this.applyBoundsToHome(point); } resolveDotObstacle(point, previousX, previousY) { const rect = this.getExpandedObstacle(this.radius); if (!rect) return; const isInside = point.x > rect.left && point.x < rect.right && point.y > rect.top && point.y < rect.bottom; if (!isInside) return; const side = this.getCollisionSide( point.x, point.y, previousX, previousY, rect ); const epsilon = 0.01; const restitution = this.obstacleRestitution; if (side === "left") { point.x = rect.left - epsilon; if (point.vx > 0) point.vx = -point.vx * restitution; } else if (side === "right") { point.x = rect.right + epsilon; if (point.vx < 0) point.vx = -point.vx * restitution; } else if (side === "top") { point.y = rect.top - epsilon; if (point.vy > 0) point.vy = -point.vy * restitution; } else { point.y = rect.bottom + epsilon; if (point.vy < 0) point.vy = -point.vy * restitution; } this.applyBoundsToDot(point); } // ---------- dot creation ---------- getRandomAvailablePosition() { const pad = this.radius + 1; const minX = Math.min(pad, this.w * 0.5); const maxX = Math.max(minX, this.w - pad); const minY = Math.min(pad, this.h * 0.5); const maxY = Math.max(minY, this.h - pad); for (let attempt = 0; attempt < 80; attempt += 1) { const x = minX + Math.random() * Math.max(0, maxX - minX); const y = minY + Math.random() * Math.max(0, maxY - minY); if (!this.isInsideObstacle(x, y, this.radius)) { return { x, y }; } } // Deterministic fallback near the canvas corners. const candidates = [ { x: minX, y: minY }, { x: maxX, y: minY }, { x: minX, y: maxY }, { x: maxX, y: maxY }, ]; const available = candidates.find( (point) => !this.isInsideObstacle(point.x, point.y, this.radius) ); // Extremely unusual case: obstacle covers the whole canvas. // Returning the first corner keeps the engine stable. return available || candidates[0]; } resetDots() { this.updateObstacleRect(true); this.dots = new Array(this.count); for (let index = 0; index < this.count; index += 1) { const position = this.getRandomAvailablePosition(); const angle = Math.random() * Math.PI * 2; const speed = this.minSpeed + Math.random() * (this.maxSpeed - this.minSpeed); this.dots[index] = { x: position.x, y: position.y, vx: 0, vy: 0, hx: position.x, hy: position.y, hvx: Math.cos(angle) * speed, hvy: Math.sin(angle) * speed, }; } } // ---------- pointer ---------- handlePointerEnter() { this.mouse.active = true; this.mouse.lastActiveT = performance.now(); } handlePointerLeave() { this.mouse.active = false; this.mouse.lastActiveT = performance.now(); } handlePointerMove(event) { const rect = this.host.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; const mouse = this.mouse; const now = performance.now(); const dt = Math.max(0.001, (now - mouse.lastT) / 1000); const velocityX = (mouseX - mouse.lastX) / dt; const velocityY = (mouseY - mouse.lastY) / dt; const smoothing = 0.25; mouse.vx += (velocityX - mouse.vx) * smoothing; mouse.vy += (velocityY - mouse.vy) * smoothing; mouse.x = mouseX; mouse.y = mouseY; mouse.lastX = mouseX; mouse.lastY = mouseY; mouse.lastT = now; mouse.active = true; mouse.lastActiveT = now; } // ---------- outer bounds ---------- applyBoundsToHome(point) { const pad = this.radius + 1; if (this.boundsMode === "wrap") { if (point.hx < -pad) point.hx = this.w + pad; if (point.hx > this.w + pad) point.hx = -pad; if (point.hy < -pad) point.hy = this.h + pad; if (point.hy > this.h + pad) point.hy = -pad; return; } const loss = this.bounceLoss; if (point.hx < pad) { point.hx = pad; if (point.hvx < 0) point.hvx = -point.hvx * loss; } else if (point.hx > this.w - pad) { point.hx = this.w - pad; if (point.hvx > 0) point.hvx = -point.hvx * loss; } if (point.hy < pad) { point.hy = pad; if (point.hvy < 0) point.hvy = -point.hvy * loss; } else if (point.hy > this.h - pad) { point.hy = this.h - pad; if (point.hvy > 0) point.hvy = -point.hvy * loss; } } applyBoundsToDot(point) { const pad = this.radius + 1; if (this.boundsMode === "wrap") { if (point.x < -pad) point.x = this.w + pad; if (point.x > this.w + pad) point.x = -pad; if (point.y < -pad) point.y = this.h + pad; if (point.y > this.h + pad) point.y = -pad; return; } const loss = this.bounceLoss; if (point.x < pad) { point.x = pad; if (point.vx < 0) point.vx = -point.vx * loss; } else if (point.x > this.w - pad) { point.x = this.w - pad; if (point.vx > 0) point.vx = -point.vx * loss; } if (point.y < pad) { point.y = pad; if (point.vy < 0) point.vy = -point.vy * loss; } else if (point.y > this.h - pad) { point.y = this.h - pad; if (point.vy > 0) point.vy = -point.vy * loss; } } // ---------- dot-to-dot collisions ---------- resolveCollisions() { const dots = this.dots; const minDistance = this.radius * 2; const minDistanceSquared = minDistance * minDistance; const restitution = this.restitution; for (let firstIndex = 0; firstIndex < dots.length; firstIndex += 1) { const first = dots[firstIndex]; for ( let secondIndex = firstIndex + 1; secondIndex < dots.length; secondIndex += 1 ) { const second = dots[secondIndex]; let dx = second.x - first.x; let dy = second.y - first.y; let distanceSquared = dx * dx + dy * dy; if (distanceSquared >= minDistanceSquared) continue; if (distanceSquared < 0.000001) { dx = (Math.random() - 0.5) * 0.01; dy = (Math.random() - 0.5) * 0.01; distanceSquared = dx * dx + dy * dy; } const distance = Math.sqrt(distanceSquared); const normalX = dx / distance; const normalY = dy / distance; const overlap = minDistance - distance; const separation = overlap * 0.5; first.x -= normalX * separation; first.y -= normalY * separation; second.x += normalX * separation; second.y += normalY * separation; const relativeVelocityX = second.vx - first.vx; const relativeVelocityY = second.vy - first.vy; const velocityAlongNormal = relativeVelocityX * normalX + relativeVelocityY * normalY; if (velocityAlongNormal <= 0) { const impulseMagnitude = -(1 + restitution) * velocityAlongNormal * 0.5; const impulseX = impulseMagnitude * normalX; const impulseY = impulseMagnitude * normalY; first.vx -= impulseX; first.vy -= impulseY; second.vx += impulseX; second.vy += impulseY; } this.applyBoundsToDot(first); this.applyBoundsToDot(second); // Separation between dots can push one into .content-title. this.resolveDotObstacle(first, first.x, first.y); this.resolveDotObstacle(second, second.x, second.y); } } } // ---------- physics ---------- step(dt, now) { this.updateObstacleRect(false, now); const mouse = this.mouse; const damping = Math.pow(this.damping, dt * 60); const sinceActive = now - (mouse.lastActiveT || 0); const afterglow = this.afterglowMs > 0 && sinceActive >= 0 && sinceActive <= this.afterglowMs ? 1 + (this.afterglowBoost - 1) * (1 - sinceActive / this.afterglowMs) : 1; const springStrength = this.spring * afterglow; let mouseVelocityX = mouse.vx; let mouseVelocityY = mouse.vy; const mouseSpeed = Math.hypot(mouseVelocityX, mouseVelocityY); if (mouseSpeed > this.windMax && mouseSpeed > 0.0001) { const scale = this.windMax / mouseSpeed; mouseVelocityX *= scale; mouseVelocityY *= scale; } const repelRadiusSquared = this.repelRadius * this.repelRadius; // 1) Move every dot. for (const point of this.dots) { const previousHomeX = point.hx; const previousHomeY = point.hy; point.hx += point.hvx * dt; point.hy += point.hvy * dt; this.applyBoundsToHome(point); this.resolveHomeObstacle(point, previousHomeX, previousHomeY); point.vx += (point.hx - point.x) * springStrength * dt; point.vy += (point.hy - point.y) * springStrength * dt; const dx = point.x - mouse.x; const dy = point.y - mouse.y; const distanceSquared = dx * dx + dy * dy; if ( (mouse.active || sinceActive < this.afterglowMs) && distanceSquared > 0.0001 && distanceSquared < repelRadiusSquared ) { const distance = Math.sqrt(distanceSquared); const normalX = dx / distance; const normalY = dy / distance; const proximity = 1 - distance / this.repelRadius; const radialFalloff = proximity * proximity; const repelForce = this.repelStrength * radialFalloff; point.vx += normalX * repelForce * dt; point.vy += normalY * repelForce * dt; const windFalloff = Math.pow(proximity, this.windFalloff); const wind = this.windStrength * windFalloff; point.vx += mouseVelocityX * wind * dt; point.vy += mouseVelocityY * wind * dt; } point.vx *= damping; point.vy *= damping; const velocitySquared = point.vx * point.vx + point.vy * point.vy; const maxVelocitySquared = this.maxVel * this.maxVel; if (velocitySquared > maxVelocitySquared) { const scale = this.maxVel / Math.sqrt(velocitySquared); point.vx *= scale; point.vy *= scale; } const previousX = point.x; const previousY = point.y; point.x += point.vx * dt; point.y += point.vy * dt; this.applyBoundsToDot(point); this.resolveDotObstacle(point, previousX, previousY); } // 2) Resolve collisions between dots. if (this.enableCollisions && this.dots.length > 1) { for ( let iteration = 0; iteration < this.collisionIterations; iteration += 1 ) { this.resolveCollisions(); } } } // ---------- render ---------- draw() { if (!this.ctx) return; this.ctx.clearRect(0, 0, this.w, this.h); this.ctx.fillStyle = this.color; for (const point of this.dots) { this.ctx.beginPath(); this.ctx.arc( point.x, point.y, this.radius, 0, Math.PI * 2 ); this.ctx.fill(); } } // ---------- cleanup ---------- destroy() { try { this.resizeObserver?.disconnect(); } catch (error) {} try { intersectionObserver?.unobserve(this.host); } catch (error) {} try { this.host.removeEventListener("pointermove", this.onPointerMove); this.host.removeEventListener("pointerenter", this.onPointerEnter); this.host.removeEventListener("pointerleave", this.onPointerLeave); } catch (error) {} if (this.canvas?.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } } } function initHost(host) { if (!host || instances.has(host)) return; if (prefersReducedMotion()) return; const instance = new BlueDotsInstance(host); instances.set(host, instance); startLoop(); } function initAll(root = document) { root.querySelectorAll("[data-blue-dots-bg]").forEach(initHost); } function cleanupRemoved() { for (const [host, instance] of instances.entries()) { if (!host.isConnected) { instance.destroy(); instances.delete(host); } } stopLoopIfEmpty(); } // Webflow CMS / dynamic DOM support. const mutationObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof Element)) continue; if (node.matches?.("[data-blue-dots-bg]")) { initHost(node); } if (node.querySelectorAll) { initAll(node); } } } cleanupRemoved(); }); function boot() { initAll(document); mutationObserver.observe(document.documentElement, { childList: true, subtree: true, }); } // Optional public API. window.BlueDotsBG = { init: initHost, initAll, refresh() { for (const instance of instances.values()) { instance.resize(); } }, destroy(host) { const instance = instances.get(host); if (!instance) return; instance.destroy(); instances.delete(host); stopLoopIfEmpty(); }, }; // Webflow-friendly ready. if (window.Webflow && Array.isArray(window.Webflow)) { window.Webflow.push(boot); } else if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot, { once: true }); } else { boot(); } })();