mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
635 lines
21 KiB
635 lines
21 KiB
// ============================================================
|
|
// SoulSync — Worker Orbs
|
|
// Dashboard header buttons shrink to floating orbs, expand on hover
|
|
// ============================================================
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Disable on mobile
|
|
if (window.innerWidth <= 768 || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) return;
|
|
|
|
// ── Worker definitions with brand colors ──
|
|
const WORKER_DEFS = [
|
|
{ container: '.mb-button-container', color: [186, 85, 211] },
|
|
{ container: '.audiodb-button-container', color: [0, 188, 212] },
|
|
{ container: '.deezer-button-container', color: [162, 56, 255] },
|
|
{ container: '.spotify-enrich-button-container', color: [30, 215, 96] },
|
|
{ container: '.itunes-enrich-button-container', color: [251, 91, 137] },
|
|
{ container: '.lastfm-enrich-button-container', color: [213, 16, 7] },
|
|
{ container: '.genius-enrich-button-container', color: [255, 255, 100] },
|
|
{ container: '.tidal-enrich-button-container', color: [180, 180, 255] },
|
|
{ container: '.qobuz-enrich-button-container', color: [1, 112, 239] },
|
|
{ container: '.hydrabase-button-container', color: [200, 200, 200] },
|
|
{ container: '.soulid-button-container', color: [29, 185, 84], rainbow: true },
|
|
{ container: '.repair-button-container', color: [180, 130, 255], rainbow: true },
|
|
];
|
|
|
|
const ORB_RADIUS = 7;
|
|
const ORB_DIAMETER = ORB_RADIUS * 2;
|
|
const CONNECTION_DIST = 70;
|
|
const LERP_SPEED = 0.08;
|
|
const EXPAND_STAGGER = 35;
|
|
const MAX_SPARKS = 60; // global spark pool cap
|
|
const SPARK_RATE = 0.12; // chance per frame per active orb to emit
|
|
|
|
let dashboardHeader = null;
|
|
let headerActions = null;
|
|
let canvas = null;
|
|
let ctx = null;
|
|
let orbs = [];
|
|
let sparks = []; // particle emissions from active orbs
|
|
let state = 'idle';
|
|
let animFrame = null;
|
|
let onDashboard = false;
|
|
let expandProgress = 0;
|
|
let staggerTimers = [];
|
|
let collapseDelay = null;
|
|
const COLLAPSE_DELAY_MS = 7000;
|
|
|
|
// ── Init ──
|
|
|
|
function init() {
|
|
dashboardHeader = document.querySelector('#dashboard-page .dashboard-header');
|
|
headerActions = document.querySelector('#dashboard-page .header-actions');
|
|
if (!dashboardHeader || !headerActions) return;
|
|
|
|
canvas = document.createElement('canvas');
|
|
canvas.className = 'worker-orb-canvas';
|
|
canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;';
|
|
dashboardHeader.appendChild(canvas);
|
|
ctx = canvas.getContext('2d');
|
|
|
|
WORKER_DEFS.forEach((def, i) => {
|
|
const el = headerActions.querySelector(def.container);
|
|
if (!el) return;
|
|
|
|
orbs.push({
|
|
el,
|
|
color: def.color,
|
|
rainbow: def.rainbow || false,
|
|
index: i,
|
|
x: 0, y: 0,
|
|
vx: (Math.random() - 0.5) * 0.6,
|
|
vy: (Math.random() - 0.5) * 0.6,
|
|
homeX: 0, homeY: 0,
|
|
visible: true,
|
|
phase: Math.random() * Math.PI * 2,
|
|
active: false,
|
|
});
|
|
});
|
|
|
|
computeHomes();
|
|
scatterOrbs();
|
|
|
|
dashboardHeader.addEventListener('mouseenter', onMouseEnter);
|
|
dashboardHeader.addEventListener('mouseleave', onMouseLeave);
|
|
window.addEventListener('resize', onResize);
|
|
document.addEventListener('visibilitychange', onVisibility);
|
|
}
|
|
|
|
function computeHomes() {
|
|
if (!dashboardHeader || !headerActions) return;
|
|
const headerRect = dashboardHeader.getBoundingClientRect();
|
|
|
|
orbs.forEach(orb => {
|
|
const elRect = orb.el.getBoundingClientRect();
|
|
orb.homeX = (elRect.left - headerRect.left) + elRect.width / 2;
|
|
orb.homeY = (elRect.top - headerRect.top) + elRect.height / 2;
|
|
orb.visible = orb.el.offsetParent !== null;
|
|
});
|
|
}
|
|
|
|
function scatterOrbs() {
|
|
if (!canvas) return;
|
|
const w = canvas.clientWidth || 600;
|
|
const h = canvas.clientHeight || 80;
|
|
|
|
orbs.forEach(orb => {
|
|
if (!orb.visible) return;
|
|
orb.x = ORB_RADIUS + Math.random() * (w - ORB_DIAMETER);
|
|
orb.y = ORB_RADIUS + Math.random() * (h - ORB_DIAMETER);
|
|
});
|
|
}
|
|
|
|
function resizeCanvas() {
|
|
if (!canvas) return;
|
|
canvas.width = canvas.clientWidth;
|
|
canvas.height = canvas.clientHeight;
|
|
}
|
|
|
|
// ── Rainbow color cycle (matches repair button's CSS rainbow) ──
|
|
|
|
const RAINBOW = [
|
|
[255, 0, 0],
|
|
[255, 136, 0],
|
|
[255, 255, 0],
|
|
[0, 255, 0],
|
|
[0, 136, 255],
|
|
[136, 0, 255],
|
|
];
|
|
|
|
function getRainbowColor(time) {
|
|
const t = ((time * 0.33) % 1 + 1) % 1; // ~3s cycle to match CSS 3s
|
|
const idx = t * RAINBOW.length;
|
|
const i = Math.floor(idx);
|
|
const f = idx - i;
|
|
const a = RAINBOW[i % RAINBOW.length];
|
|
const b = RAINBOW[(i + 1) % RAINBOW.length];
|
|
return [
|
|
Math.round(a[0] + (b[0] - a[0]) * f),
|
|
Math.round(a[1] + (b[1] - a[1]) * f),
|
|
Math.round(a[2] + (b[2] - a[2]) * f),
|
|
];
|
|
}
|
|
|
|
// ── Spark system ──
|
|
|
|
function emitSpark(orb, colorOverride) {
|
|
if (sparks.length >= MAX_SPARKS) return;
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const speed = 0.4 + Math.random() * 0.8;
|
|
sparks.push({
|
|
x: orb.x,
|
|
y: orb.y,
|
|
vx: Math.cos(angle) * speed,
|
|
vy: Math.sin(angle) * speed,
|
|
life: 1.0, // 1.0 → 0.0
|
|
decay: 0.012 + Math.random() * 0.012,
|
|
color: colorOverride || orb.color,
|
|
radius: 1.5 + Math.random() * 1.5,
|
|
});
|
|
}
|
|
|
|
function updateSparks() {
|
|
for (let i = sparks.length - 1; i >= 0; i--) {
|
|
const s = sparks[i];
|
|
s.x += s.vx;
|
|
s.y += s.vy;
|
|
s.vx *= 0.98;
|
|
s.vy *= 0.98;
|
|
s.life -= s.decay;
|
|
if (s.life <= 0) {
|
|
sparks.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawSparks(ctx) {
|
|
for (const s of sparks) {
|
|
const [r, g, b] = s.color;
|
|
const alpha = s.life * 0.6;
|
|
const radius = s.radius * s.life;
|
|
|
|
// Spark glow
|
|
const glow = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, radius * 3);
|
|
glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.4})`);
|
|
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
|
ctx.beginPath();
|
|
ctx.arc(s.x, s.y, radius * 3, 0, Math.PI * 2);
|
|
ctx.fillStyle = glow;
|
|
ctx.fill();
|
|
|
|
// Spark core
|
|
ctx.beginPath();
|
|
ctx.arc(s.x, s.y, radius, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// ── State machine ──
|
|
|
|
function enterOrbState() {
|
|
if (state === 'orbs') return;
|
|
state = 'orbs';
|
|
expandProgress = 0;
|
|
|
|
orbs.forEach(orb => {
|
|
orb.el.classList.add('worker-orb-hidden');
|
|
});
|
|
|
|
canvas.style.opacity = '1';
|
|
canvas.style.display = '';
|
|
resizeCanvas();
|
|
startLoop();
|
|
}
|
|
|
|
function enterExpandedState() {
|
|
state = 'expanded';
|
|
expandProgress = 1;
|
|
|
|
clearStaggerTimers();
|
|
orbs.forEach((orb, i) => {
|
|
const t = setTimeout(() => {
|
|
orb.el.classList.remove('worker-orb-hidden');
|
|
orb.el.classList.add('worker-orb-reveal');
|
|
}, i * EXPAND_STAGGER);
|
|
staggerTimers.push(t);
|
|
});
|
|
|
|
canvas.style.opacity = '0';
|
|
setTimeout(() => {
|
|
if (state === 'expanded') {
|
|
canvas.style.display = 'none';
|
|
stopLoop();
|
|
}
|
|
}, 400);
|
|
}
|
|
|
|
function enterCollapsingState() {
|
|
state = 'collapsing';
|
|
clearStaggerTimers();
|
|
|
|
const total = orbs.length;
|
|
orbs.forEach((orb, i) => {
|
|
const t = setTimeout(() => {
|
|
orb.el.classList.remove('worker-orb-reveal');
|
|
orb.el.classList.add('worker-orb-hidden');
|
|
}, (total - 1 - i) * 20);
|
|
staggerTimers.push(t);
|
|
});
|
|
|
|
canvas.style.display = '';
|
|
canvas.style.opacity = '1';
|
|
resizeCanvas();
|
|
computeHomes();
|
|
orbs.forEach(orb => {
|
|
orb.x = orb.homeX;
|
|
orb.y = orb.homeY;
|
|
orb.vx = (Math.random() - 0.5) * 0.4;
|
|
orb.vy = (Math.random() - 0.5) * 0.4;
|
|
});
|
|
|
|
startLoop();
|
|
|
|
setTimeout(() => {
|
|
if (state === 'collapsing') {
|
|
state = 'orbs';
|
|
expandProgress = 0;
|
|
}
|
|
}, total * 20 + 100);
|
|
}
|
|
|
|
function clearStaggerTimers() {
|
|
staggerTimers.forEach(t => clearTimeout(t));
|
|
staggerTimers = [];
|
|
}
|
|
|
|
// ── Events ──
|
|
|
|
function onMouseEnter() {
|
|
if (!onDashboard) return;
|
|
// Cancel any pending collapse
|
|
if (collapseDelay) {
|
|
clearTimeout(collapseDelay);
|
|
collapseDelay = null;
|
|
}
|
|
if (state === 'orbs' || state === 'collapsing') {
|
|
state = 'expanding';
|
|
expandProgress = 0;
|
|
}
|
|
}
|
|
|
|
function onMouseLeave() {
|
|
if (!onDashboard) return;
|
|
if (state === 'expanded' || state === 'expanding') {
|
|
// Delay before collapsing back to orbs
|
|
if (collapseDelay) clearTimeout(collapseDelay);
|
|
collapseDelay = setTimeout(() => {
|
|
collapseDelay = null;
|
|
if (state === 'expanded' || state === 'expanding') {
|
|
enterCollapsingState();
|
|
}
|
|
}, COLLAPSE_DELAY_MS);
|
|
}
|
|
}
|
|
|
|
function onResize() {
|
|
computeHomes();
|
|
resizeCanvas();
|
|
const w = canvas ? canvas.width : 600;
|
|
const h = canvas ? canvas.height : 80;
|
|
orbs.forEach(orb => {
|
|
orb.x = Math.max(ORB_RADIUS, Math.min(w - ORB_RADIUS, orb.x));
|
|
orb.y = Math.max(ORB_RADIUS, Math.min(h - ORB_RADIUS, orb.y));
|
|
});
|
|
}
|
|
|
|
function onVisibility() {
|
|
if (document.hidden) {
|
|
stopLoop();
|
|
} else if (onDashboard && (state === 'orbs' || state === 'expanding' || state === 'collapsing')) {
|
|
startLoop();
|
|
}
|
|
}
|
|
|
|
// ── Animation loop ──
|
|
|
|
let frameCount = 0;
|
|
|
|
function startLoop() {
|
|
if (animFrame) return;
|
|
tick();
|
|
}
|
|
|
|
function stopLoop() {
|
|
if (animFrame) {
|
|
cancelAnimationFrame(animFrame);
|
|
animFrame = null;
|
|
}
|
|
}
|
|
|
|
function tick() {
|
|
animFrame = requestAnimationFrame(tick);
|
|
if (!canvas || !ctx) return;
|
|
|
|
frameCount++;
|
|
const time = frameCount / 60;
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
|
|
if (w === 0 || h === 0) {
|
|
resizeCanvas();
|
|
return;
|
|
}
|
|
|
|
// Check active state every 30 frames
|
|
if (frameCount % 30 === 0) {
|
|
orbs.forEach(orb => {
|
|
orb.visible = orb.el.offsetParent !== null;
|
|
const btn = orb.el.querySelector('button');
|
|
orb.active = btn ? btn.classList.contains('active') : false;
|
|
});
|
|
}
|
|
|
|
const visibleOrbs = orbs.filter(o => o.visible);
|
|
|
|
if (state === 'orbs' || state === 'collapsing') {
|
|
updatePhysics(visibleOrbs, w, h);
|
|
} else if (state === 'expanding') {
|
|
updateExpanding(visibleOrbs, w, h);
|
|
}
|
|
|
|
// Emit sparks from active orbs
|
|
for (const orb of visibleOrbs) {
|
|
if (orb.active && Math.random() < SPARK_RATE) {
|
|
emitSpark(orb, orb.rainbow ? getRainbowColor(time) : null);
|
|
}
|
|
}
|
|
updateSparks();
|
|
|
|
// Draw
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
drawConnections(ctx, visibleOrbs, time);
|
|
drawSparks(ctx);
|
|
drawOrbs(ctx, visibleOrbs, time);
|
|
}
|
|
|
|
// ── Physics ──
|
|
|
|
function updatePhysics(visible, w, h) {
|
|
const cx = w * 0.5;
|
|
const cy = h * 0.5;
|
|
|
|
for (const orb of visible) {
|
|
// Active orbs drift faster
|
|
const driftStrength = orb.active ? 0.04 : 0.02;
|
|
orb.vx += (Math.random() - 0.5) * driftStrength;
|
|
orb.vy += (Math.random() - 0.5) * driftStrength;
|
|
|
|
// Subtle gravity toward center — keeps orbs loosely grouped
|
|
const gx = cx - orb.x;
|
|
const gy = cy - orb.y;
|
|
const gDist = Math.sqrt(gx * gx + gy * gy);
|
|
if (gDist > 1) {
|
|
const gStrength = 0.003;
|
|
orb.vx += (gx / gDist) * gStrength;
|
|
orb.vy += (gy / gDist) * gStrength;
|
|
}
|
|
|
|
// Damping
|
|
orb.vx *= 0.993;
|
|
orb.vy *= 0.993;
|
|
|
|
// Speed cap — active orbs move a bit faster
|
|
const maxSpeed = orb.active ? 0.8 : 0.5;
|
|
const speed = Math.sqrt(orb.vx * orb.vx + orb.vy * orb.vy);
|
|
if (speed > maxSpeed) {
|
|
const scale = maxSpeed / speed;
|
|
orb.vx *= scale;
|
|
orb.vy *= scale;
|
|
}
|
|
|
|
// Soft repulsion from other orbs
|
|
for (const other of visible) {
|
|
if (other === orb) continue;
|
|
const dx = orb.x - other.x;
|
|
const dy = orb.y - other.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < 35 && dist > 0.1) {
|
|
const force = 0.03 * (1 - dist / 35);
|
|
orb.vx += (dx / dist) * force;
|
|
orb.vy += (dy / dist) * force;
|
|
}
|
|
}
|
|
|
|
// Move
|
|
orb.x += orb.vx;
|
|
orb.y += orb.vy;
|
|
|
|
// Boundary bounce
|
|
if (orb.x < ORB_RADIUS) { orb.x = ORB_RADIUS; orb.vx *= -0.7; }
|
|
if (orb.x > w - ORB_RADIUS) { orb.x = w - ORB_RADIUS; orb.vx *= -0.7; }
|
|
if (orb.y < ORB_RADIUS) { orb.y = ORB_RADIUS; orb.vy *= -0.7; }
|
|
if (orb.y > h - ORB_RADIUS) { orb.y = h - ORB_RADIUS; orb.vy *= -0.7; }
|
|
}
|
|
}
|
|
|
|
function updateExpanding(visible) {
|
|
let allClose = true;
|
|
|
|
for (const orb of visible) {
|
|
const dx = orb.homeX - orb.x;
|
|
const dy = orb.homeY - orb.y;
|
|
orb.x += dx * LERP_SPEED;
|
|
orb.y += dy * LERP_SPEED;
|
|
|
|
orb.vx *= 0.9;
|
|
orb.vy *= 0.9;
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist > 3) allClose = false;
|
|
}
|
|
|
|
expandProgress = Math.min(1, expandProgress + 0.03);
|
|
|
|
if (allClose || expandProgress >= 1) {
|
|
enterExpandedState();
|
|
}
|
|
}
|
|
|
|
// ── Drawing ──
|
|
|
|
function drawOrbs(ctx, visible, time) {
|
|
for (const orb of visible) {
|
|
const [r, g, b] = orb.rainbow ? getRainbowColor(time) : orb.color;
|
|
const pulse = 0.5 + 0.5 * Math.sin(time * 2 + orb.phase);
|
|
|
|
// Active orbs are larger and breathe — size oscillates
|
|
let baseRadius = orb.active ? ORB_RADIUS + 3 : ORB_RADIUS;
|
|
if (orb.active) {
|
|
baseRadius += 2 * Math.sin(time * 3 + orb.phase);
|
|
}
|
|
|
|
// Scale up during expand transition
|
|
const currentRadius = state === 'expanding'
|
|
? baseRadius + expandProgress * 4
|
|
: baseRadius;
|
|
|
|
// Inactive orbs are dimmer
|
|
const activeMult = orb.active ? 1.0 : 0.45;
|
|
|
|
// Outer glow — much larger and brighter for active
|
|
const glowRadius = orb.active ? currentRadius * 5 : currentRadius * 3;
|
|
const glowAlpha = orb.active
|
|
? (0.25 + pulse * 0.2) * activeMult
|
|
: (0.06 + pulse * 0.03) * activeMult;
|
|
const glow = ctx.createRadialGradient(orb.x, orb.y, 0, orb.x, orb.y, glowRadius);
|
|
glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${glowAlpha})`);
|
|
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, glowRadius, 0, Math.PI * 2);
|
|
ctx.fillStyle = glow;
|
|
ctx.fill();
|
|
|
|
// Core
|
|
const coreAlpha = orb.active
|
|
? 0.85 + pulse * 0.15
|
|
: (0.3 + pulse * 0.08) * activeMult;
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, Math.max(1, currentRadius), 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${coreAlpha})`;
|
|
ctx.fill();
|
|
|
|
// Inactive: subtle border ring so they're visible against dark backgrounds
|
|
if (!orb.active) {
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, currentRadius, 0, Math.PI * 2);
|
|
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${0.3 + pulse * 0.1})`;
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Active: expanding pulse ring that fades
|
|
if (orb.active) {
|
|
// Inner ring — tight, bright
|
|
const ring1 = currentRadius + 2 + pulse * 3;
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, ring1, 0, Math.PI * 2);
|
|
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${0.25 + pulse * 0.15})`;
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
|
|
// Outer ring — wide, faint, slower pulse
|
|
const pulse2 = 0.5 + 0.5 * Math.sin(time * 1.2 + orb.phase + 1);
|
|
const ring2 = currentRadius + 6 + pulse2 * 6;
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, ring2, 0, Math.PI * 2);
|
|
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${0.06 + pulse2 * 0.06})`;
|
|
ctx.lineWidth = 0.5;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawConnections(ctx, visible, time) {
|
|
for (let i = 0; i < visible.length; i++) {
|
|
for (let j = i + 1; j < visible.length; j++) {
|
|
const a = visible[i], b = visible[j];
|
|
const dx = a.x - b.x;
|
|
const dy = a.y - b.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < CONNECTION_DIST) {
|
|
// Connections between active orbs are brighter
|
|
const activePair = a.active && b.active;
|
|
const anyActive = a.active || b.active;
|
|
const baseAlpha = activePair ? 0.3 : (anyActive ? 0.2 : 0.15);
|
|
const alpha = (1 - dist / CONNECTION_DIST) * baseAlpha;
|
|
|
|
const [r1, g1, b1] = a.rainbow ? getRainbowColor(time) : a.color;
|
|
const [r2, g2, b2] = b.rainbow ? getRainbowColor(time) : b.color;
|
|
const mr = (r1 + r2) >> 1;
|
|
const mg = (g1 + g2) >> 1;
|
|
const mb = (b1 + b2) >> 1;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.strokeStyle = `rgba(${mr}, ${mg}, ${mb}, ${alpha})`;
|
|
ctx.lineWidth = anyActive ? 0.8 : 0.5;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Page awareness ──
|
|
|
|
function isEnabled() {
|
|
return window._workerOrbsEnabled !== false;
|
|
}
|
|
|
|
function setPage(pageId) {
|
|
const wasDashboard = onDashboard;
|
|
onDashboard = (pageId === 'dashboard') && isEnabled();
|
|
|
|
if (onDashboard && !wasDashboard) {
|
|
computeHomes();
|
|
resizeCanvas();
|
|
sparks = [];
|
|
enterOrbState();
|
|
} else if (!onDashboard && wasDashboard) {
|
|
if (collapseDelay) { clearTimeout(collapseDelay); collapseDelay = null; }
|
|
stopLoop();
|
|
state = 'idle';
|
|
sparks = [];
|
|
orbs.forEach(orb => {
|
|
orb.el.classList.remove('worker-orb-hidden', 'worker-orb-reveal');
|
|
});
|
|
if (canvas) {
|
|
canvas.style.display = 'none';
|
|
canvas.style.opacity = '0';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Bootstrap ──
|
|
|
|
function bootstrap() {
|
|
init();
|
|
if (!dashboardHeader) return;
|
|
|
|
window.workerOrbs = { setPage };
|
|
|
|
const activePage = document.querySelector('.page.active');
|
|
if (activePage && activePage.id === 'dashboard-page' && isEnabled()) {
|
|
setTimeout(() => {
|
|
computeHomes();
|
|
resizeCanvas();
|
|
enterOrbState();
|
|
onDashboard = true;
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', bootstrap);
|
|
} else {
|
|
setTimeout(bootstrap, 100);
|
|
}
|
|
|
|
})();
|