Files
AEmotionStudio-ComfyUI-Chri…/js/background-themes.js
T

2593 lines
83 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { initSettingsCache, updateCache, loadSettingFromStorage, getSetting, BACKGROUND_THEMES, COLOR_SCHEMES } from "./settings-cache.js";
let extensionSetupComplete = false;
let isInitialSetup = true;
let isPageVisible = true;
document.addEventListener("visibilitychange", () => {
isPageVisible = document.visibilityState === "visible";
});
let isExecuting = false;
let executionEndTime = 0;
let originalDrawBackCanvas = null;
let starEntities = [];
let shootingStars = [];
let nebulaEntities = [];
let starInitialized = false;
let bgSnowflakeEntities = [];
let bgSnowflakesInitialized = false;
let lastBgColorScheme = null;
let lastBgSnowflakeColorScheme = null;
let customSnowImage = null;
let customSnowImageSrc = null;
let fireworkRockets = [];
let fireworkParticles = [];
let fireworkSparks = [];
let lastFireworkTime = 0;
let animationLoopId = null;
const FIREWORK_PALETTES = [
["#ff6b6b", "#ff8787", "#ffa8a8"],
// Red gradient
["#ffd93d", "#ffe066", "#fff3bf"],
// Gold gradient
["#6bcb77", "#8ce99a", "#b2f2bb"],
// Green gradient
["#4d96ff", "#74c0fc", "#a5d8ff"],
// Blue gradient
["#ff85c1", "#f783ac", "#faa2c1"],
// Pink gradient
["#a855f7", "#c084fc", "#d8b4fe"],
// Purple gradient
["#00d4ff", "#22d3ee", "#67e8f9"],
// Cyan gradient
["#ffffff", "#f8f9fa", "#e9ecef"],
// White/silver
["#ffd700", "#ffec99", "#fff9db"]
// Bright gold
];
const EXPLOSION_TYPES = ["chrysanthemum", "willow", "palm", "ring", "crackle", "peony"];
function createFireworkRocket(width, height, _isFinale) {
const palette = FIREWORK_PALETTES[Math.floor(Math.random() * FIREWORK_PALETTES.length)];
const explosionType = EXPLOSION_TYPES[Math.floor(Math.random() * EXPLOSION_TYPES.length)];
return {
x: Math.random() * width * 0.8 + width * 0.1,
y: height - 30,
vx: (Math.random() - 0.5) * 3,
vy: -12 - Math.random() * 6,
palette,
explosionType,
trail: [],
trailTimer: 0,
age: 0,
exploded: false,
size: 2 + Math.random()
};
}
function createExplosionParticles(x, y, palette, explosionType) {
const particles = [];
const primaryColor = palette[0];
const secondaryColor = palette[1];
const tertiaryColor = palette[2];
switch (explosionType) {
case "chrysanthemum": {
const count = 80 + Math.floor(Math.random() * 40);
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count + (Math.random() - 0.5) * 0.2;
const speed = 3 + Math.random() * 3;
const colorChoice = Math.random();
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: colorChoice < 0.5 ? primaryColor : colorChoice < 0.8 ? secondaryColor : tertiaryColor,
alpha: 1,
size: 2 + Math.random() * 1.5,
decay: 8e-3 + Math.random() * 4e-3,
trail: [],
hasTrail: true,
gravity: 0.03
});
}
break;
}
case "willow": {
const count = 60 + Math.floor(Math.random() * 30);
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count + (Math.random() - 0.5) * 0.15;
const speed = 2 + Math.random() * 2;
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
color: palette[Math.floor(Math.random() * palette.length)],
alpha: 1,
size: 1.5 + Math.random(),
decay: 5e-3 + Math.random() * 3e-3,
trail: [],
hasTrail: true,
gravity: 0.08
// Heavy gravity for drooping effect
});
}
break;
}
case "palm": {
const count = 50 + Math.floor(Math.random() * 20);
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count;
const speed = 4 + Math.random() * 2;
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 2,
color: primaryColor,
alpha: 1,
size: 3 + Math.random() * 2,
decay: 0.012 + Math.random() * 5e-3,
trail: [],
hasTrail: true,
gravity: 0.04
});
}
break;
}
case "ring": {
const count = 40;
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count;
const speed = 4;
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: palette[i % palette.length],
alpha: 1,
size: 2.5,
decay: 0.015,
trail: [],
hasTrail: false,
gravity: 0.02
});
}
break;
}
case "crackle": {
const count = 30;
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count + (Math.random() - 0.5) * 0.3;
const speed = 2 + Math.random() * 2;
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: primaryColor,
alpha: 1,
size: 2 + Math.random(),
decay: 0.02,
trail: [],
hasTrail: true,
gravity: 0.05,
crackle: true,
crackleTime: 0.3 + Math.random() * 0.3
});
}
break;
}
case "peony":
default: {
const count = 70 + Math.floor(Math.random() * 30);
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count + (Math.random() - 0.5) * 0.25;
const speed = 2.5 + Math.random() * 2.5;
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: palette[Math.floor(Math.random() * palette.length)],
alpha: 1,
size: 2 + Math.random() * 1.5,
decay: 0.01 + Math.random() * 5e-3,
trail: [],
hasTrail: Math.random() > 0.3,
gravity: 0.04
});
}
}
}
const sparkCount = 20 + Math.floor(Math.random() * 15);
for (let i = 0; i < sparkCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 5;
fireworkSparks.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
alpha: 1,
size: 0.5 + Math.random() * 0.5,
decay: 0.03 + Math.random() * 0.02,
twinkle: Math.random() * Math.PI * 2
});
}
return particles;
}
function updateBgSnowflakeColors() {
for (const flake of bgSnowflakeEntities) {
flake.color = getBgSnowflakeColor();
}
}
let countdownElement = null;
let countdownInterval = null;
function createCountdownElement() {
if (countdownElement) return countdownElement;
const now = /* @__PURE__ */ new Date();
let targetYear = now.getFullYear();
if (now.getMonth() > 0 || now.getMonth() === 0 && now.getDate() > 1) {
targetYear++;
}
const container = document.createElement("div");
container.id = "christmas-theme-countdown";
container.innerHTML = `
<style>
#christmas-theme-countdown {
position: fixed;
bottom: 4px;
left: 120px;
background: #121212;
border: 1px solid #2a2a2a;
border-radius: 6px;
padding: 7px 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #e0e0e0;
z-index: 50;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
#christmas-theme-countdown .countdown-title {
font-size: 10px;
color: #b0b0b0;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
}
#christmas-theme-countdown .countdown-title svg {
width: 12px;
height: 12px;
fill: none;
stroke: #ffd700;
stroke-width: 2;
stroke-linecap: round;
vertical-align: middle;
flex-shrink: 0;
position: relative;
top: -1px;
}
#christmas-theme-countdown .countdown-grid {
display: flex;
align-items: flex-start;
gap: 2px;
margin-left: 5px;
}
#christmas-theme-countdown .countdown-segment {
display: flex;
flex-direction: column;
align-items: center;
min-width: 22px;
}
#christmas-theme-countdown .countdown-segment span:first-child {
font-size: 14px;
font-weight: 600;
color: #ffffff;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
#christmas-theme-countdown .countdown-segment .label {
font-size: 8px;
color: #aaa;
margin-top: 1px;
}
#christmas-theme-countdown .countdown-sep {
font-size: 14px;
font-weight: 600;
color: #666;
line-height: 1.1;
}
#christmas-theme-countdown.celebration {
animation: celebrate 0.3s ease-in-out infinite;
border-color: #ffd700;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
}
@keyframes celebrate {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
</style>
<div class="countdown-title">
<svg viewBox="0 0 24 24"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.64 5.64l2.83 2.83M15.54 15.54l2.83 2.83M5.64 18.36l2.83-2.83M15.54 8.46l2.83-2.83"/><circle cx="12" cy="12" r="2"/></svg>
New Year ${targetYear}
<svg viewBox="0 0 24 24"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.64 5.64l2.83 2.83M15.54 15.54l2.83 2.83M5.64 18.36l2.83-2.83M15.54 8.46l2.83-2.83"/><circle cx="12" cy="12" r="2"/></svg>
</div>
<div class="countdown-grid">
<div class="countdown-segment"><span id="countdown-days">00</span><span class="label">Days</span></div>
<div class="countdown-sep">:</div>
<div class="countdown-segment"><span id="countdown-hours">00</span><span class="label">Hrs</span></div>
<div class="countdown-sep">:</div>
<div class="countdown-segment"><span id="countdown-mins">00</span><span class="label">Min</span></div>
<div class="countdown-sep">:</div>
<div class="countdown-segment"><span id="countdown-secs">00</span><span class="label">Sec</span></div>
</div>
`;
document.body.appendChild(container);
countdownElement = container;
return container;
}
let finaleActive = false;
let finaleStartTime = 0;
let finaleGracePeriod = false;
const FINALE_DURATION = 22e3;
let finaleConfetti = [];
let finaleStars = [];
function triggerFinale() {
if (finaleActive) return;
finaleActive = true;
finaleStartTime = performance.now();
console.log("🎆 FINALE TRIGGERED!");
createScreenFlash();
createYearText();
for (let i = 0; i < 3; i++) {
setTimeout(() => createGoldenRing(), i * 500);
}
createConfettiShower();
triggerCascadeFireworks();
}
function createScreenFlash() {
const flash = document.createElement("div");
flash.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: radial-gradient(ellipse at center, rgba(255,255,255,0.9), rgba(255,215,0,0.6));
pointer-events: none;
z-index: 9999;
animation: finaleFlash 0.8s ease-out forwards;
`;
const style = document.createElement("style");
style.textContent = `
@keyframes finaleFlash {
0% { opacity: 1; }
100% { opacity: 0; }
}
`;
document.head.appendChild(style);
document.body.appendChild(flash);
setTimeout(() => {
flash.remove();
style.remove();
}, 1e3);
}
function createYearText() {
const now = /* @__PURE__ */ new Date();
const year = now.getFullYear() + (now.getMonth() === 0 && now.getDate() === 1 ? 0 : 1);
const text = document.createElement("div");
text.innerHTML = `
<div style="font-size: 120px; font-weight: 900; text-shadow: 0 0 30px #ffd700, 0 0 60px #ff8c00;">${year}</div>
<div style="font-size: 36px; font-weight: 600; margin-top: 10px;">Happy New Year!</div>
`;
text.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ffd700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-align: center;
pointer-events: none;
z-index: 9998;
animation: yearTextAnim 5s ease-out forwards;
`;
const style = document.createElement("style");
style.textContent = `
@keyframes yearTextAnim {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
30% { transform: translate(-50%, -50%) scale(1); }
80% { opacity: 1; }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.2); }
}
`;
document.head.appendChild(style);
document.body.appendChild(text);
setTimeout(() => {
text.remove();
style.remove();
}, 5500);
}
function createGoldenRing() {
const ring = document.createElement("div");
ring.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
width: 10px;
height: 10px;
border: 4px solid #ffd700;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9997;
box-shadow: 0 0 20px #ffd700, 0 0 40px #ff8c00;
animation: ringExpand 2s ease-out forwards;
`;
const style = document.createElement("style");
style.textContent = `
@keyframes ringExpand {
0% { width: 10px; height: 10px; opacity: 1; border-width: 4px; }
100% { width: 200vmax; height: 200vmax; opacity: 0; border-width: 2px; }
}
`;
document.head.appendChild(style);
document.body.appendChild(ring);
setTimeout(() => {
ring.remove();
style.remove();
}, 2500);
}
function createConfettiShower() {
const colors = ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#9d4edd", "#ff85a1", "#ffd700", "#00ffff"];
const container = document.createElement("div");
container.id = "finale-confetti-container";
container.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9996;
overflow: hidden;
`;
document.body.appendChild(container);
const style = document.createElement("style");
style.id = "confetti-style";
style.textContent = `
@keyframes confettiFall {
0% { transform: translateY(-20px) rotate(0deg); opacity: 1; }
85% { opacity: 0.8; }
100% { transform: translateY(120vh) rotate(720deg); opacity: 0; }
}
.finale-confetti {
position: absolute;
animation: confettiFall linear forwards;
}
`;
document.head.appendChild(style);
for (let i = 0; i < 150; i++) {
setTimeout(() => {
const confetti = document.createElement("div");
confetti.className = "finale-confetti";
const size = 8 + Math.random() * 12;
const x = Math.random() * 100;
const duration = 3 + Math.random() * 4;
const delay = Math.random() * 0.5;
confetti.style.cssText = `
left: ${x}%;
top: -20px;
width: ${size}px;
height: ${size * 0.4}px;
background: ${colors[Math.floor(Math.random() * colors.length)]};
animation-duration: ${duration}s;
animation-delay: ${delay}s;
border-radius: 2px;
`;
container.appendChild(confetti);
}, Math.random() * 2e3);
}
setTimeout(() => {
container.remove();
style.remove();
}, 1e4);
}
function triggerCascadeFireworks() {
const canvas = document.getElementById("christmas-background");
if (!canvas) return;
for (let i = 0; i < 25; i++) {
setTimeout(() => {
const x = canvas.width * 0.1 + Math.random() * canvas.width * 0.8;
fireworkRockets.push(createFireworkRocket(x, canvas.height));
}, i * 160);
}
for (let i = 0; i < 15; i++) {
setTimeout(() => {
const x = canvas.width * 0.15 + Math.random() * canvas.width * 0.7;
fireworkRockets.push(createFireworkRocket(x, canvas.height));
}, 4e3 + i * 400);
}
for (let i = 0; i < 10; i++) {
setTimeout(() => {
const x = canvas.width * 0.2 + Math.random() * canvas.width * 0.6;
fireworkRockets.push(createFireworkRocket(x, canvas.height));
}, 1e4 + i * 800);
}
for (let i = 0; i < 4; i++) {
setTimeout(() => {
const x = canvas.width * 0.25 + Math.random() * canvas.width * 0.5;
fireworkRockets.push(createFireworkRocket(x, canvas.height));
}, 18e3 + i * 1e3);
}
}
function updateFinaleParticles(ctx, canvas) {
for (let i = finaleConfetti.length - 1; i >= 0; i--) {
const c = finaleConfetti[i];
c.x += c.vx;
c.y += c.vy;
c.vy += 0.05;
c.vx *= 0.99;
c.rotation += c.rotationSpeed;
c.alpha -= c.decay;
const maxHeight = c.screenHeight || window.innerHeight;
if (c.alpha <= 0 || c.y > maxHeight + 50) {
finaleConfetti.splice(i, 1);
}
}
for (let i = finaleStars.length - 1; i >= 0; i--) {
const s = finaleStars[i];
s.trail.push({ x: s.x, y: s.y });
if (s.trail.length > 15) s.trail.shift();
s.y += s.vy;
s.alpha -= 0.01;
const maxHeight = s.screenHeight || window.innerHeight;
if (s.alpha <= 0 || s.y > maxHeight + 50) {
finaleStars.splice(i, 1);
}
}
}
function drawFinaleParticles(ctx) {
for (const c of finaleConfetti) {
ctx.save();
ctx.translate(c.x, c.y);
ctx.rotate(c.rotation);
ctx.globalAlpha = c.alpha;
ctx.fillStyle = c.color;
ctx.fillRect(-c.size / 2, -c.size / 4, c.size, c.size / 2);
ctx.restore();
}
for (const s of finaleStars) {
for (let t = 0; t < s.trail.length; t++) {
const tp = s.trail[t];
ctx.globalAlpha = t / s.trail.length * s.alpha * 0.5;
ctx.fillStyle = s.color;
ctx.beginPath();
ctx.arc(tp.x, tp.y, s.size * 0.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = s.alpha;
ctx.shadowBlur = 15;
ctx.shadowColor = s.color;
ctx.fillStyle = s.color;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.globalAlpha = 1;
}
function updateCountdown() {
const daysEl = document.getElementById("countdown-days");
const hoursEl = document.getElementById("countdown-hours");
const minsEl = document.getElementById("countdown-mins");
const secsEl = document.getElementById("countdown-secs");
if (!daysEl || !hoursEl || !minsEl || !secsEl) return;
const now = /* @__PURE__ */ new Date();
let targetYear = now.getFullYear();
if (now.getMonth() > 0 || now.getMonth() === 0 && now.getDate() > 1) {
targetYear++;
}
const newYear = new Date(targetYear, 0, 1, 0, 0, 0);
const diff = newYear.getTime() - now.getTime();
if (diff <= 0) {
daysEl.textContent = "00";
hoursEl.textContent = "00";
minsEl.textContent = "00";
secsEl.textContent = "00";
if (countdownElement) {
countdownElement.classList.add("celebration");
}
showFinaleButton();
return;
}
const days = Math.floor(diff / (1e3 * 60 * 60 * 24));
const hours = Math.floor(diff % (1e3 * 60 * 60 * 24) / (1e3 * 60 * 60));
const minutes = Math.floor(diff % (1e3 * 60 * 60) / (1e3 * 60));
const seconds = Math.floor(diff % (1e3 * 60) / 1e3);
daysEl.textContent = days.toString().padStart(2, "0");
hoursEl.textContent = hours.toString().padStart(2, "0");
minsEl.textContent = minutes.toString().padStart(2, "0");
secsEl.textContent = seconds.toString().padStart(2, "0");
if (!getSetting("ChristmasTheme.Background.ShowFinaleButton")) {
hideFinaleButton();
}
}
function toggleCountdownDisplay(enabled) {
if (enabled) {
createCountdownElement();
countdownElement.style.display = "block";
updateCountdown();
if (!countdownInterval) {
countdownInterval = setInterval(updateCountdown, 1e3);
}
if (getSetting("ChristmasTheme.Background.ShowFinaleButton")) {
showFinaleButton();
}
} else {
if (countdownElement) {
countdownElement.style.display = "none";
}
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
hideFinaleButton();
}
}
let finaleButtonElement = null;
let finaleButtonShown = false;
function showFinaleButton() {
if (finaleButtonShown || finaleActive) return;
finaleButtonShown = true;
if (finaleButtonElement) {
finaleButtonElement.style.display = "block";
return;
}
const btn = document.createElement("button");
btn.id = "finale-trigger-btn";
btn.innerHTML = `Happy New Year!`;
btn.style.cssText = `
position: fixed;
bottom: 61px;
right: 277px;
padding: 6px 12px;
background: rgba(35, 35, 35, 0.95);
border: 1px solid #ffd700;
border-radius: 6px;
color: #ffd700;
font-weight: 600;
cursor: pointer;
z-index: 10000;
font-size: 11px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
display: flex;
align-items: center;
`;
btn.onmouseenter = () => {
btn.style.background = "rgba(50, 50, 50, 0.95)";
btn.style.boxShadow = "0 0 15px rgba(255, 215, 0, 0.4)";
};
btn.onmouseleave = () => {
btn.style.background = "rgba(35, 35, 35, 0.95)";
btn.style.boxShadow = "0 2px 8px rgba(0, 0, 0, 0.3)";
};
btn.onclick = () => {
finaleActive = false;
triggerFinale();
};
document.body.appendChild(btn);
finaleButtonElement = btn;
}
function hideFinaleButton() {
finaleButtonShown = false;
if (finaleButtonElement) {
finaleButtonElement.style.display = "none";
}
}
let mouseParticles = [];
let mouseX = 0;
let mouseY = 0;
let lastMouseX = 0;
let lastMouseY = 0;
let mouseEffectInitialized = false;
const MOUSE_EFFECTS = {
none: null,
sparkler: {
colors: ["#ffd700", "#ffec99", "#fff9db", "#ffffff"],
gravity: 0.08,
decay: 0.015,
size: [1, 3],
spread: 4,
friction: 0.98,
shape: "circle",
glow: 10,
hasTrail: true,
twinkle: true,
spawnMode: "burst"
// explodes outward from cursor
},
snowflake: {
colors: ["#ffffff", "#e0f7ff", "#b8e6ff", "#d4f1f9"],
gravity: 0.015,
decay: 6e-3,
size: [3, 6],
spread: 0.5,
friction: 0.99,
shape: "snowflake",
glow: 8,
hasTrail: false,
drift: true,
rotation: true,
spawnMode: "drop"
// drops gently from cursor position
},
confetti: {
colors: ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#9d4edd", "#ff85a1"],
gravity: 0.05,
decay: 8e-3,
size: [3, 5],
spread: 6,
friction: 0.97,
shape: "rectangle",
glow: 0,
spin: true,
spawnMode: "pop"
// pops upward then falls
},
stardust: {
colors: ["#fffacd", "#fff8dc", "#fffaf0", "#ffffff"],
gravity: 5e-3,
decay: 0.01,
size: [1, 2],
spread: 2,
friction: 0.995,
shape: "star",
glow: 12,
twinkle: true,
spawnMode: "float"
// floats gently in place
},
comet: {
colors: ["#87ceeb", "#add8e6", "#ffffff"],
gravity: 0,
decay: 0.025,
size: [2, 4],
spread: 0.2,
friction: 0.92,
shape: "circle",
glow: 15,
hasTrail: true,
trailLength: 15,
spawnMode: "follow"
// tight follow, long trail
},
aurora: {
colors: ["#00ff88", "#00ffcc", "#00ccff", "#8844ff"],
gravity: -0.02,
decay: 6e-3,
size: [2, 4],
spread: 1,
friction: 0.99,
shape: "circle",
glow: 20,
wave: true,
spawnMode: "rise"
// floats upward with wave
},
ribbon: {
colors: ["#ff69b4", "#ff1493", "#db7093", "#ffb6c1"],
gravity: 0,
decay: 0.02,
size: [2, 3],
spread: 0.1,
friction: 0.85,
shape: "circle",
glow: 5,
hasTrail: true,
trailLength: 10,
spawnMode: "follow"
// smooth following ribbon
},
crystal: {
colors: ["#e0ffff", "#afeeee", "#b0e0e6", "#ffffff"],
gravity: 0.03,
decay: 0.01,
size: [3, 5],
spread: 2,
friction: 0.96,
shape: "diamond",
glow: 15,
shimmer: true,
spawnMode: "scatter"
// scatters in random directions
},
petals: {
colors: ["#ffb7c5", "#ffc0cb", "#ff69b4", "#fff0f5"],
gravity: 0.02,
decay: 8e-3,
size: [4, 6],
spread: 3,
friction: 0.98,
shape: "petal",
glow: 5,
flutter: true,
rotation: true,
spawnMode: "flutter"
// drifts with flutter motion
},
gifts: {
colors: ["#ff6b6b", "#4ade80", "#ffd700", "#60a5fa"],
gravity: 0.06,
decay: 8e-3,
size: [5, 8],
spread: 3,
friction: 0.96,
shape: "box",
glow: 0,
rotation: true,
spawnMode: "toss"
// tossed upward then tumbles
},
candy: {
colors: ["#ff0000", "#ffffff"],
gravity: 0.035,
decay: 0.01,
size: [3, 5],
spread: 4,
friction: 0.97,
shape: "candy",
glow: 5,
spin: true,
spawnMode: "bounce"
// bouncy scatter
},
orb: {
colors: ["#9d4edd", "#7b2cbf", "#c77dff", "#e0aaff"],
gravity: 0,
decay: 0.015,
size: [3, 5],
spread: 0.5,
friction: 0.95,
shape: "circle",
glow: 25,
orbit: true,
pulseGlow: true,
spawnMode: "orbit"
// orbits around spawn point
},
magic: {
colors: ["#ffd700", "#ff69b4", "#00ffff", "#ff6b6b", "#9d4edd"],
gravity: 0,
decay: 0.015,
size: [1, 2],
spread: 0.5,
friction: 0.98,
shape: "star",
glow: 15,
spiral: true,
twinkle: true,
spawnMode: "spiral"
// spirals outward from cursor
},
nova: {
colors: ["#ffffff", "#fffacd", "#ffd700", "#ff8c00"],
gravity: 0,
decay: 0.02,
size: [2, 4],
spread: 0,
friction: 0.96,
shape: "star",
glow: 20,
twinkle: true,
spawnMode: "burst"
// expanding starburst
},
bubbles: {
colors: ["#87ceeb", "#b0e0e6", "#e0ffff", "#ffffff"],
gravity: -0.03,
decay: 8e-3,
size: [4, 8],
spread: 2,
friction: 0.99,
shape: "bubble",
glow: 8,
shimmer: true,
spawnMode: "rise"
// floating upward
},
embers: {
colors: ["#ff4500", "#ff6347", "#ffa500", "#ffd700", "#ffec99"],
gravity: -0.02,
decay: 0.012,
size: [1, 3],
spread: 2,
friction: 0.98,
shape: "circle",
glow: 12,
twinkle: true,
spawnMode: "rise"
// drifting upward like fire
},
lightning: {
colors: ["#00ffff", "#87ceeb", "#ffffff", "#e0ffff"],
gravity: 0,
decay: 0.04,
size: [1, 2],
spread: 3,
friction: 0.9,
shape: "circle",
glow: 20,
hasTrail: true,
trailLength: 6,
spawnMode: "scatter"
// quick electric sparks
},
leaves: {
colors: ["#8b4513", "#d2691e", "#cd853f", "#f4a460", "#daa520"],
gravity: 0.025,
decay: 8e-3,
size: [4, 7],
spread: 3,
friction: 0.98,
shape: "leaf",
glow: 0,
flutter: true,
rotation: true,
spawnMode: "flutter"
// drifting down with spin
},
wishes: {
colors: ["#ffffff", "#fffacd", "#ffd700"],
gravity: 0,
decay: 0.03,
size: [1, 2],
spread: 0.5,
friction: 0.85,
shape: "circle",
glow: 15,
hasTrail: true,
trailLength: 20,
spawnMode: "follow"
// shooting star streaks
},
notes: {
colors: ["#ff69b4", "#9d4edd", "#4d96ff", "#ffd700"],
gravity: -0.015,
decay: 0.01,
size: [6, 10],
spread: 3,
friction: 0.98,
shape: "note",
glow: 5,
sway: true,
spawnMode: "rise"
// floating musical notes
},
hearts: {
colors: ["#ff69b4", "#ff1493", "#ff6b6b", "#ff85a1"],
gravity: -0.01,
decay: 0.01,
size: [4, 7],
spread: 2,
friction: 0.98,
shape: "heart",
glow: 8,
sway: true,
spawnMode: "rise"
// floating hearts
}
};
const PARTICLE_SHAPES = {
circle: (ctx, p) => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
},
star: (ctx, p) => {
const spikes = 5, outerR = p.size, innerR = p.size * 0.5;
ctx.beginPath();
for (let i = 0; i < spikes * 2; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const angle = i * Math.PI / spikes - Math.PI / 2 + (p.rotation || 0);
const x = p.x + Math.cos(angle) * r;
const y = p.y + Math.sin(angle) * r;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
},
diamond: (ctx, p) => {
const s = p.size;
ctx.beginPath();
ctx.moveTo(p.x, p.y - s);
ctx.lineTo(p.x + s * 0.6, p.y);
ctx.lineTo(p.x, p.y + s);
ctx.lineTo(p.x - s * 0.6, p.y);
ctx.closePath();
ctx.fill();
},
snowflake: (ctx, p) => {
const s = p.size, arms = 6;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation || 0);
ctx.lineWidth = 1;
ctx.strokeStyle = ctx.fillStyle;
for (let i = 0; i < arms; i++) {
ctx.rotate(Math.PI / 3);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -s);
ctx.moveTo(0, -s * 0.5);
ctx.lineTo(-s * 0.3, -s * 0.7);
ctx.moveTo(0, -s * 0.5);
ctx.lineTo(s * 0.3, -s * 0.7);
ctx.stroke();
}
ctx.restore();
},
rectangle: (ctx, p) => {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation || 0);
ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
ctx.restore();
},
petal: (ctx, p) => {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation || 0);
ctx.beginPath();
ctx.ellipse(0, 0, p.size * 0.4, p.size, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
},
bell: (ctx, p) => {
const s = p.size;
ctx.beginPath();
ctx.arc(p.x, p.y - s * 0.3, s * 0.7, Math.PI, 0);
ctx.quadraticCurveTo(p.x + s * 0.8, p.y + s * 0.5, p.x, p.y + s * 0.6);
ctx.quadraticCurveTo(p.x - s * 0.8, p.y + s * 0.5, p.x - s * 0.7, p.y - s * 0.3);
ctx.fill();
ctx.beginPath();
ctx.arc(p.x, p.y + s * 0.7, s * 0.15, 0, Math.PI * 2);
ctx.fill();
},
box: (ctx, p) => {
const s = p.size;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation || 0);
ctx.fillRect(-s / 2, -s / 2, s, s);
ctx.fillStyle = "#ffffff";
ctx.globalAlpha *= 0.8;
ctx.fillRect(-s / 2, -s * 0.1, s, s * 0.2);
ctx.fillRect(-s * 0.1, -s / 2, s * 0.2, s);
ctx.restore();
},
candy: (ctx, p) => {
const s = p.size;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation || 0);
ctx.beginPath();
ctx.arc(0, 0, s, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffffff";
for (let i = 0; i < 3; i++) {
ctx.fillRect(-s, -s + i * s * 0.7, s * 2, s * 0.3);
}
ctx.restore();
},
bubble: (ctx, p) => {
const s = p.size;
ctx.beginPath();
ctx.arc(p.x, p.y, s, 0, Math.PI * 2);
ctx.globalAlpha *= 0.4;
ctx.fill();
ctx.globalAlpha *= 2;
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(p.x - s * 0.3, p.y - s * 0.3, s * 0.2, 0, Math.PI * 2);
ctx.fill();
},
leaf: (ctx, p) => {
const s = p.size;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation || 0);
ctx.beginPath();
ctx.moveTo(0, -s);
ctx.quadraticCurveTo(s * 0.8, -s * 0.3, 0, s);
ctx.quadraticCurveTo(-s * 0.8, -s * 0.3, 0, -s);
ctx.fill();
ctx.strokeStyle = "rgba(0,0,0,0.2)";
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, -s * 0.8);
ctx.lineTo(0, s * 0.8);
ctx.stroke();
ctx.restore();
},
note: (ctx, p) => {
const s = p.size;
ctx.save();
ctx.translate(p.x, p.y);
ctx.beginPath();
ctx.ellipse(0, 0, s * 0.5, s * 0.35, -0.3, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(s * 0.4, -s * 1.2, s * 0.12, s * 1.2);
ctx.beginPath();
ctx.moveTo(s * 0.52, -s * 1.2);
ctx.quadraticCurveTo(s * 1.2, -s * 0.8, s * 0.52, -s * 0.4);
ctx.fill();
ctx.restore();
},
heart: (ctx, p) => {
const s = p.size;
ctx.save();
ctx.translate(p.x, p.y);
ctx.beginPath();
ctx.moveTo(0, s * 0.3);
ctx.bezierCurveTo(-s * 0.5, -s * 0.3, -s, s * 0.1, 0, s);
ctx.bezierCurveTo(s, s * 0.1, s * 0.5, -s * 0.3, 0, s * 0.3);
ctx.fill();
ctx.restore();
}
};
function initMouseEffect() {
if (mouseEffectInitialized) return;
mouseEffectInitialized = true;
document.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
console.log("✨ Mouse effects system initialized");
}
function createMouseParticle(x, y, vx, vy, effect) {
const config = MOUSE_EFFECTS[effect];
if (!config) return null;
const sizeRange = config.size;
const size = sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]);
let pVx, pVy;
const spread = config.spread;
const angle = Math.random() * Math.PI * 2;
switch (config.spawnMode) {
case "burst":
pVx = Math.cos(angle) * (2 + Math.random() * 3);
pVy = Math.sin(angle) * (2 + Math.random() * 3);
break;
case "drop":
pVx = (Math.random() - 0.5) * spread;
pVy = Math.random() * 0.5;
break;
case "pop":
pVx = (Math.random() - 0.5) * spread;
pVy = -2 - Math.random() * 3;
break;
case "float":
pVx = (Math.random() - 0.5) * 0.5;
pVy = (Math.random() - 0.5) * 0.5;
break;
case "follow":
pVx = vx * 0.3 + (Math.random() - 0.5) * spread;
pVy = vy * 0.3 + (Math.random() - 0.5) * spread;
break;
case "rise":
pVx = (Math.random() - 0.5) * spread;
pVy = -0.5 - Math.random();
break;
case "scatter":
pVx = (Math.random() - 0.5) * 4;
pVy = (Math.random() - 0.5) * 4;
break;
case "flutter":
pVx = (Math.random() - 0.5) * 2;
pVy = Math.random() * 0.5;
break;
case "toss":
pVx = (Math.random() - 0.5) * spread + vx * 0.2;
pVy = -1.5 - Math.random() * 2;
break;
case "bounce":
pVx = (Math.random() - 0.5) * spread;
pVy = -1 - Math.random();
break;
case "orbit":
pVx = Math.cos(angle) * 2;
pVy = Math.sin(angle) * 2;
break;
case "spiral":
pVx = Math.cos(angle) * 1.5;
pVy = Math.sin(angle) * 1.5;
break;
default:
pVx = vx + (Math.random() - 0.5) * spread;
pVy = vy + (Math.random() - 0.5) * spread;
}
return {
x,
y,
vx: pVx,
vy: pVy,
alpha: 1,
size,
color: config.colors[Math.floor(Math.random() * config.colors.length)],
gravity: config.gravity + (Math.random() - 0.5) * 0.01,
decay: config.decay + Math.random() * 3e-3,
friction: config.friction || 0.98,
shape: config.shape,
glow: config.glow,
twinkle: config.twinkle ? Math.random() * Math.PI * 2 : 0,
rotation: config.rotation ? Math.random() * Math.PI * 2 : 0,
rotationSpeed: config.spin ? (Math.random() - 0.5) * 0.3 : config.rotation ? 0.02 : 0,
trail: config.hasTrail ? [] : null,
trailLength: config.trailLength || 5,
wave: config.wave ? Math.random() * Math.PI * 2 : 0,
orbitAngle: config.orbit ? angle : null,
orbitRadius: config.orbit ? 20 + Math.random() * 30 : 0,
originX: x,
originY: y,
spiralAngle: config.spiral ? angle : null,
spiralRadius: config.spiral ? 5 : 0,
drift: config.drift ? (Math.random() - 0.5) * 1.5 : 0,
flutter: config.flutter ? Math.random() * Math.PI * 2 : 0,
sway: config.sway ? Math.random() * Math.PI * 2 : 0,
shimmer: config.shimmer ? Math.random() * Math.PI * 2 : 0,
pulseGlow: config.pulseGlow || false,
age: 0,
effect
};
}
function updateMouseParticles(deltaTime) {
const effectType = getSetting("ChristmasTheme.Background.MouseEffect");
if (!effectType || effectType === "none") {
mouseParticles = [];
return;
}
if (!mouseEffectInitialized) {
initMouseEffect();
}
const config = MOUSE_EFFECTS[effectType];
if (!config) return;
const dx = mouseX - lastMouseX;
const dy = mouseY - lastMouseY;
const speed = Math.sqrt(dx * dx + dy * dy);
if (speed > 2) {
const particleCount = Math.min(Math.floor(speed / 3), 5);
for (let i = 0; i < particleCount; i++) {
const t = i / particleCount;
const px = lastMouseX + dx * t;
const py = lastMouseY + dy * t;
const particle = createMouseParticle(px, py, dx * 0.1, dy * 0.1, effectType);
if (particle) mouseParticles.push(particle);
}
}
lastMouseX = mouseX;
lastMouseY = mouseY;
for (let i = mouseParticles.length - 1; i >= 0; i--) {
const p = mouseParticles[i];
if (p.trail && p.trail.length < p.trailLength) {
p.trail.push({ x: p.x, y: p.y, alpha: p.alpha });
}
p.vy += p.gravity;
p.x += p.vx + (p.drift || 0) * Math.sin(p.flutter || 0);
p.y += p.vy;
p.vx *= p.friction;
p.vy *= p.friction;
p.alpha -= p.decay;
if (p.twinkle) p.twinkle += 0.2;
if (p.rotationSpeed) p.rotation += p.rotationSpeed;
if (p.wave) {
p.wave += 0.1;
p.x += Math.sin(p.wave) * 2;
}
if (p.flutter) p.flutter += 0.15;
if (p.sway) {
p.sway += 0.1;
p.x += Math.sin(p.sway) * 1.5;
}
if (p.shimmer) p.shimmer += 0.3;
if (p.spiralAngle !== null) {
p.spiralAngle += 0.15;
p.spiralRadius += 0.5;
p.x = p.originX + Math.cos(p.spiralAngle) * p.spiralRadius;
p.y = p.originY + Math.sin(p.spiralAngle) * p.spiralRadius;
}
if (p.orbitAngle !== null) {
p.orbitAngle += 0.08;
p.x = p.originX + Math.cos(p.orbitAngle) * p.orbitRadius;
p.y = p.originY + Math.sin(p.orbitAngle) * p.orbitRadius;
}
if (p.alpha <= 0) {
mouseParticles.splice(i, 1);
}
}
if (mouseParticles.length > 200) {
mouseParticles.splice(0, mouseParticles.length - 200);
}
}
function drawMouseParticles(ctx) {
const effectType = getSetting("ChristmasTheme.Background.MouseEffect");
if (!effectType || effectType === "none" || mouseParticles.length === 0) return;
ctx.save();
for (const p of mouseParticles) {
if (p.trail && p.trail.length > 0) {
for (let t = 0; t < p.trail.length; t++) {
const tp = p.trail[t];
ctx.globalAlpha = t / p.trail.length * p.alpha * 0.4;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(tp.x, tp.y, p.size * 0.5, 0, Math.PI * 2);
ctx.fill();
}
}
let finalAlpha = p.alpha;
if (p.twinkle) finalAlpha *= 0.7 + Math.sin(p.twinkle) * 0.3;
if (p.shimmer) finalAlpha *= 0.8 + Math.sin(p.shimmer) * 0.2;
ctx.shadowBlur = p.pulseGlow ? p.glow * (0.7 + Math.sin(p.twinkle || 0) * 0.3) : p.glow;
ctx.shadowColor = p.color;
ctx.globalAlpha = finalAlpha;
ctx.fillStyle = p.color;
const shapeRenderer = PARTICLE_SHAPES[p.shape] || PARTICLE_SHAPES.circle;
shapeRenderer(ctx, p);
}
ctx.shadowBlur = 0;
ctx.restore();
}
let cachedGradient = null;
let cachedTheme = null;
let cachedHeight = 0;
let frameCount = 0;
let lastFpsCheck = performance.now();
let currentFps = 60;
let lowPerfMode = false;
let lastAnimationTime = 0;
let animationTime = 0;
let lastShootingStarTime = 0;
function getStarColor() {
const roll = Math.random();
if (roll < 0.15) return "#aaccff";
if (roll < 0.5) return "#ffffff";
if (roll < 0.75) return "#fffef0";
if (roll < 0.9) return "#fff4e0";
return "#ffe4c0";
}
function initStars(width, height) {
const area = width * height;
const density = 3e-4;
const count = Math.min(Math.floor(area * density), 800);
starEntities = [];
for (let i = 0; i < count; i++) {
const layerRoll = Math.random();
let layer, size, baseOpacity, twinkleSpeed, hasGlow, hasSpikes;
if (layerRoll < 0.6) {
layer = "distant";
size = 0.3 + Math.random() * 0.5;
baseOpacity = 0.2 + Math.random() * 0.3;
twinkleSpeed = 0.08 + Math.random() * 0.15;
hasGlow = false;
hasSpikes = false;
} else if (layerRoll < 0.92) {
layer = "normal";
size = 0.5 + Math.random() * 0.8;
baseOpacity = 0.4 + Math.random() * 0.4;
twinkleSpeed = 0.15 + Math.random() * 0.3;
hasGlow = false;
hasSpikes = false;
} else {
layer = "bright";
size = 0.8 + Math.random() * 0.7;
baseOpacity = 0.7 + Math.random() * 0.3;
twinkleSpeed = 0.2 + Math.random() * 0.4;
hasGlow = true;
hasSpikes = Math.random() < 0.4;
}
starEntities.push({
x: Math.random() * width,
y: Math.random() * height,
size,
// Use much larger offset range + secondary offset to prevent sync
twinkleOffset: Math.random() * Math.PI * 20,
// Larger range
twinkleOffset2: Math.random() * Math.PI * 15,
// Secondary offset
twinkleSpeed,
// Add slight speed variation per star
twinkleSpeedMod: 0.85 + Math.random() * 0.3,
baseOpacity,
layer,
hasGlow,
hasSpikes,
color: getStarColor()
});
}
nebulaEntities = [];
const nebulaCount = Math.min(Math.floor(count * 0.01), 5);
for (let i = 0; i < nebulaCount; i++) {
nebulaEntities.push({
x: Math.random() * width,
y: Math.random() * height * 0.6,
// More in upper area
radius: 100 + Math.random() * 150,
// Larger but more diffuse
hue: Math.random() * 60 - 30,
opacity: 0.015 + Math.random() * 0.015,
// Much more subtle
pulseSpeed: 5e-3 + Math.random() * 0.01,
// Very slow
pulseOffset: Math.random() * Math.PI * 10
// Large offset to prevent sync
});
}
starInitialized = true;
console.log(`⭐ Created ${starEntities.length} stars, ${nebulaEntities.length} nebulae`);
}
function createShootingStar(width, height) {
const angle = Math.PI * 0.2 + Math.random() * Math.PI * 0.3;
const speed = 400 + Math.random() * 500;
const isLong = Math.random() < 0.3;
return {
x: Math.random() * width * 0.8,
// Start more to the left
y: Math.random() * height * 0.3,
// Start in upper portion
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
length: isLong ? 60 + Math.random() * 80 : 25 + Math.random() * 40,
life: 1,
decay: isLong ? 0.4 + Math.random() * 0.3 : 0.7 + Math.random() * 0.6,
brightness: 0.8 + Math.random() * 0.2,
// Trail fragments for realism
fragments: [],
lastFragmentTime: 0,
fragmentInterval: 30 + Math.random() * 20,
// Terminal flare (brightens before dying)
willFlare: Math.random() < 0.4,
flareIntensity: 1.5 + Math.random() * 1
};
}
function getGradient(ctx, height) {
const colorTheme = getSetting("ChristmasTheme.Background.ColorTheme") || "classic";
if (cachedGradient && cachedTheme === colorTheme && cachedHeight === height) {
return cachedGradient;
}
const theme = BACKGROUND_THEMES[colorTheme] || BACKGROUND_THEMES.classic;
if (!theme) {
console.warn("No theme found for", colorTheme);
return null;
}
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, theme.top || "#05004c");
gradient.addColorStop(0.5, theme.bottom || "#110E19");
gradient.addColorStop(1, theme.bottom || "#110E19");
cachedGradient = gradient;
cachedTheme = colorTheme;
cachedHeight = height;
return gradient;
}
function getBgSnowflakeColor() {
const colorScheme = getSetting("ChristmasTheme.Snowflake.ColorScheme");
const christmasColors = getSetting("ChristmasTheme.ChristmasEffects.ColorScheme");
switch (colorScheme) {
case "blue":
const blueVariants = ["#d4f1f9", "#c8e8f0", "#b8dce8"];
return blueVariants[Math.floor(Math.random() * blueVariants.length)];
case "rainbow":
const rainbowPalette = ["#ffb3ba", "#bae1ff", "#baffc9", "#ffffba", "#ffdfba"];
return rainbowPalette[Math.floor(Math.random() * rainbowPalette.length)];
case "match":
const selectedPalette = COLOR_SCHEMES[christmasColors] || COLOR_SCHEMES.traditional;
return selectedPalette[Math.floor(Math.random() * selectedPalette.length)];
case "newyear":
return COLOR_SCHEMES.newyear[Math.floor(Math.random() * 5)];
default:
const whiteVariants = ["#ffffff", "#f8f9fa", "#f1f3f5"];
return whiteVariants[Math.floor(Math.random() * whiteVariants.length)];
}
}
function initBgSnowflakes(width, height) {
bgSnowflakeEntities = [];
const count = 45;
for (let i = 0; i < count; i++) {
const sizeRoll = Math.random();
let size, opacity;
if (sizeRoll < 0.5) {
size = 2 + Math.random() * 3;
opacity = 0.25 + Math.random() * 0.2;
} else if (sizeRoll < 0.85) {
size = 4 + Math.random() * 3;
opacity = 0.35 + Math.random() * 0.25;
} else {
size = 6 + Math.random() * 3;
opacity = 0.4 + Math.random() * 0.2;
}
bgSnowflakeEntities.push({
x: Math.random() * width,
y: Math.random() * height,
// Distribute across full height initially
size,
opacity,
color: getBgSnowflakeColor(),
// Match color scheme
speed: 8 + Math.random() * 10,
// Pixels per second (slower)
drift: (Math.random() - 0.5) * 25,
// Horizontal drift amplitude
driftSpeed: 0.1 + Math.random() * 0.15,
// Drift oscillation speed
driftOffset: Math.random() * Math.PI * 2,
// Phase offset
rotation: Math.random() * Math.PI * 2,
// Initial rotation
rotationSpeed: (Math.random() - 0.5) * 0.3,
// Slow rotation
flakeType: ["branched", "minimal", "stellar", "emoji1", "emoji2", "emoji3", "dendrite", "ornate"][Math.floor(Math.random() * 8)]
// Random type
});
}
bgSnowflakesInitialized = true;
console.log(`❄️ Created ${count} background canvas snowflakes`);
}
function drawSnowflake(ctx, x, y, size, rotation, glowAmount = 0, color = null) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
if (glowAmount > 0 && color) {
ctx.shadowBlur = glowAmount;
ctx.shadowColor = color;
}
for (let i = 0; i < 6; i++) {
ctx.rotate(Math.PI / 3);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size);
ctx.stroke();
if (size > 4) {
const branchLen = size * 0.35;
const branchPos = size * 0.55;
ctx.beginPath();
ctx.moveTo(0, -branchPos);
ctx.lineTo(-branchLen * 0.5, -branchPos - branchLen * 0.5);
ctx.moveTo(0, -branchPos);
ctx.lineTo(branchLen * 0.5, -branchPos - branchLen * 0.5);
ctx.stroke();
}
}
ctx.beginPath();
ctx.arc(0, 0, size * 0.15, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawCrystalSnowflake(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(Math.PI / 3 * i);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size);
ctx.stroke();
if (size > 3) {
const branchY = -size * 0.55;
const branchLen = size * 0.35;
const branchAngle = Math.PI / 5;
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(-Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
}
ctx.restore();
}
ctx.beginPath();
ctx.arc(0, 0, size * 0.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawStellarSnowflake(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(Math.PI / 3 * i);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size);
ctx.stroke();
const positions = [0.4, 0.7];
for (const pos of positions) {
const branchY = -size * pos;
const branchLen = size * (0.45 - pos * 0.3);
const branchAngle = Math.PI / 4;
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(-Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
}
if (size > 4) {
ctx.beginPath();
ctx.arc(0, -size, size * 0.08, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
ctx.beginPath();
ctx.arc(0, 0, size * 0.12, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawEmoji2744(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(Math.PI / 3 * i);
ctx.beginPath();
ctx.moveTo(0, -size * 0.1);
ctx.lineTo(0, -size);
ctx.stroke();
const tipSize = size * 0.15;
ctx.beginPath();
ctx.moveTo(-tipSize, -size + tipSize * 0.7);
ctx.lineTo(0, -size);
ctx.lineTo(tipSize, -size + tipSize * 0.7);
ctx.stroke();
const branchY = -size * 0.6;
const branchLen = size * 0.3;
const angle = Math.PI / 4;
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(-Math.sin(angle) * branchLen, branchY - Math.cos(angle) * branchLen);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(Math.sin(angle) * branchLen, branchY - Math.cos(angle) * branchLen);
ctx.stroke();
ctx.restore();
}
ctx.beginPath();
ctx.arc(0, 0, size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawEmoji2745(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.beginPath();
ctx.arc(0, 0, size * 0.18, 0, Math.PI * 2);
ctx.stroke();
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i - Math.PI / 2;
const startR = size * 0.18;
const endR = size;
ctx.beginPath();
ctx.moveTo(Math.cos(angle) * startR, Math.sin(angle) * startR);
ctx.lineTo(Math.cos(angle) * endR, Math.sin(angle) * endR);
ctx.stroke();
const crossPos = size * 0.65;
const crossLen = size * 0.12;
const px = Math.cos(angle) * crossPos;
const py = Math.sin(angle) * crossPos;
const perpAngle = angle + Math.PI / 2;
ctx.beginPath();
ctx.moveTo(px - Math.cos(perpAngle) * crossLen, py - Math.sin(perpAngle) * crossLen);
ctx.lineTo(px + Math.cos(perpAngle) * crossLen, py + Math.sin(perpAngle) * crossLen);
ctx.stroke();
ctx.beginPath();
ctx.arc(Math.cos(angle) * endR, Math.sin(angle) * endR, size * 0.04, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawEmoji2746(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
const origWidth = ctx.lineWidth;
ctx.lineWidth = origWidth * 1.3;
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(Math.PI / 3 * i);
ctx.beginPath();
ctx.moveTo(0, -size * 0.12);
ctx.lineTo(0, -size);
ctx.stroke();
const chevY = -size * 0.5;
const chevLen = size * 0.28;
const chevAngle = Math.PI / 5;
ctx.beginPath();
ctx.moveTo(-Math.sin(chevAngle) * chevLen, chevY + Math.cos(chevAngle) * chevLen * 0.5);
ctx.lineTo(0, chevY);
ctx.lineTo(Math.sin(chevAngle) * chevLen, chevY + Math.cos(chevAngle) * chevLen * 0.5);
ctx.stroke();
const tipY = -size;
const tipLen = size * 0.12;
ctx.beginPath();
ctx.moveTo(-tipLen * 0.6, tipY + tipLen);
ctx.lineTo(0, tipY);
ctx.lineTo(tipLen * 0.6, tipY + tipLen);
ctx.stroke();
ctx.restore();
}
ctx.lineWidth = origWidth;
ctx.beginPath();
ctx.arc(0, 0, size * 0.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawDendriteSnowflake(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(Math.PI / 3 * i);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size);
ctx.stroke();
const positions = [0.4, 0.6, 0.8];
positions.forEach((pos, idx) => {
const branchY = -size * pos;
const branchLen = size * (0.3 - idx * 0.06);
const branchAngle = Math.PI / 4;
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(-Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
});
ctx.restore();
}
ctx.beginPath();
ctx.arc(0, 0, size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawOrnateSnowflake(ctx, x, y, size, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(Math.PI / 3 * i);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size * 0.9);
ctx.stroke();
const tipY = -size * 0.9;
const dSize = size * 0.1;
ctx.beginPath();
ctx.moveTo(0, tipY - dSize);
ctx.lineTo(dSize * 0.6, tipY);
ctx.lineTo(0, tipY + dSize);
ctx.lineTo(-dSize * 0.6, tipY);
ctx.closePath();
ctx.fill();
const branchY = -size * 0.5;
const branchLen = size * 0.3;
const branchAngle = Math.PI / 3;
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(-Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, branchY);
ctx.lineTo(Math.sin(branchAngle) * branchLen, branchY - Math.cos(branchAngle) * branchLen);
ctx.stroke();
ctx.restore();
}
ctx.beginPath();
ctx.arc(0, 0, size * 0.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawEnhancedBackground(ctx, width, height) {
if (!isPageVisible) return;
if (!getSetting("ChristmasTheme.Background.Enabled")) return;
const now = performance.now();
if (!isExecuting && now - lastAnimationTime > 1e3) {
lastAnimationTime = now;
}
const deltaTime = isExecuting ? 0 : (now - lastAnimationTime) / 1e3;
if (!isExecuting) {
lastAnimationTime = now;
animationTime += deltaTime;
}
if (!isExecuting) {
frameCount++;
if (now - lastFpsCheck >= 1e3) {
currentFps = frameCount;
frameCount = 0;
lastFpsCheck = now;
if (currentFps < 28) lowPerfMode = true;
if (currentFps > 32) lowPerfMode = false;
}
}
ctx.shadowBlur = 0;
const inCooldown = now - executionEndTime < 500;
if (!isExecuting && !inCooldown && !starInitialized) {
cachedHeight = height;
initStars(width, height);
}
const snowEnabled = getSetting("ChristmasTheme.Snowflake.Enabled");
if (!isExecuting && !inCooldown && snowEnabled && !bgSnowflakesInitialized) {
initBgSnowflakes(width, height);
}
if (snowEnabled && bgSnowflakeEntities.length > 0) {
const currentSnowflakeColorScheme = getSetting("ChristmasTheme.Snowflake.ColorScheme");
const currentChristmasColorScheme = getSetting("ChristmasTheme.ChristmasEffects.ColorScheme");
if (currentSnowflakeColorScheme !== lastBgSnowflakeColorScheme || currentSnowflakeColorScheme === "match" && currentChristmasColorScheme !== lastBgColorScheme) {
updateBgSnowflakeColors();
lastBgSnowflakeColorScheme = currentSnowflakeColorScheme;
lastBgColorScheme = currentChristmasColorScheme;
}
}
ctx.save();
const gradient = getGradient(ctx, height);
if (gradient) {
ctx.fillStyle = gradient;
ctx.globalAlpha = 0.3;
ctx.fillRect(0, 0, width, height);
}
const starsEnabled = getSetting("ChristmasTheme.Background.Stars");
const partyMode = getSetting("ChristmasTheme.Background.PartyMode");
const colorTheme = getSetting("ChristmasTheme.Background.ColorTheme") || "classic";
BACKGROUND_THEMES[colorTheme] || BACKGROUND_THEMES.classic;
const time = animationTime;
const partyColors = ["#ff0080", "#00ff80", "#8000ff", "#ff8000", "#00ffff", "#ff00ff", "#ffff00", "#00ff00"];
if (starsEnabled) {
for (let starIdx = 0; starIdx < starEntities.length; starIdx++) {
const star = starEntities[starIdx];
if (lowPerfMode) {
if (star.layer === "distant" && starIdx % 3 !== 0) continue;
if (star.layer === "normal" && starIdx % 2 !== 0) continue;
}
let starSpeed = star.twinkleSpeed * star.twinkleSpeedMod;
let opacity, starColor;
if (partyMode) {
starSpeed *= 8;
const fastTwinkle = Math.sin(time * starSpeed + star.twinkleOffset);
const colorIndex = Math.floor((time * 3 + star.twinkleOffset) % partyColors.length);
starColor = partyColors[colorIndex];
opacity = star.baseOpacity * (0.4 + Math.abs(fastTwinkle) * 0.6);
} else {
const twinkle = Math.sin(time * starSpeed + star.twinkleOffset);
const twinkle2 = Math.sin(time * starSpeed * 0.67 + star.twinkleOffset2);
const combinedTwinkle = twinkle * 0.6 + twinkle2 * 0.4;
opacity = star.baseOpacity * (0.8 + combinedTwinkle * 0.2);
starColor = star.color;
}
ctx.fillStyle = starColor;
if (star.hasGlow && !lowPerfMode) {
ctx.shadowBlur = partyMode ? star.size * 10 : star.size * 5;
ctx.shadowColor = starColor;
ctx.globalAlpha = opacity * (partyMode ? 0.9 : 0.7);
ctx.beginPath();
ctx.arc(star.x, star.y, star.size * (partyMode ? 1.3 : 1), 0, Math.PI * 2);
ctx.fill();
if (star.hasSpikes && !partyMode) {
const combinedTwinkle = Math.sin(time * starSpeed + star.twinkleOffset);
ctx.shadowBlur = 0;
const spikeLength = star.size * 4 * (0.6 + combinedTwinkle * 0.4);
ctx.strokeStyle = starColor;
ctx.lineWidth = 0.3;
ctx.globalAlpha = opacity * 0.25;
ctx.beginPath();
ctx.moveTo(star.x - spikeLength, star.y);
ctx.lineTo(star.x + spikeLength, star.y);
ctx.moveTo(star.x, star.y - spikeLength);
ctx.lineTo(star.x, star.y + spikeLength);
ctx.stroke();
}
ctx.shadowBlur = 0;
} else {
ctx.globalAlpha = opacity * (partyMode ? 0.7 : 0.5);
ctx.beginPath();
ctx.arc(star.x, star.y, star.size * (partyMode ? 1.2 : 1), 0, Math.PI * 2);
ctx.fill();
}
}
}
if (!lowPerfMode) {
const horizonGlow = ctx.createLinearGradient(0, height * 0.7, 0, height);
horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
horizonGlow.addColorStop(0.5, "rgba(20, 30, 60, 0.05)");
horizonGlow.addColorStop(1, "rgba(40, 50, 80, 0.1)");
ctx.globalAlpha = 1;
ctx.fillStyle = horizonGlow;
ctx.fillRect(0, height * 0.7, width, height * 0.3);
}
if (!lowPerfMode && nebulaEntities.length > 0) {
for (const nebula of nebulaEntities) {
const nebulaOpacity = nebula.opacity;
const nebulaGradient = ctx.createRadialGradient(
nebula.x,
nebula.y,
0,
nebula.x,
nebula.y,
nebula.radius
);
nebulaGradient.addColorStop(0, `rgba(80, 120, 200, ${nebulaOpacity * 0.6})`);
nebulaGradient.addColorStop(0.4, `rgba(60, 100, 180, ${nebulaOpacity * 0.3})`);
nebulaGradient.addColorStop(1, "rgba(0, 0, 0, 0)");
ctx.globalAlpha = 1;
ctx.fillStyle = nebulaGradient;
ctx.beginPath();
ctx.arc(nebula.x, nebula.y, nebula.radius, 0, Math.PI * 2);
ctx.fill();
}
}
if (snowEnabled && bgSnowflakeEntities.length > 0) {
ctx.lineCap = "round";
const glowIntensity = getSetting("ChristmasTheme.Snowflake.Glow") || 10;
for (const flake of bgSnowflakeEntities) {
flake.y += flake.speed * deltaTime;
flake.rotation += flake.rotationSpeed * deltaTime;
const driftX = Math.sin(time * flake.driftSpeed + flake.driftOffset) * flake.drift;
if (!isExecuting && flake.y > height + 20) {
flake.y = -20;
flake.x = Math.random() * width;
flake.color = getBgSnowflakeColor();
flake.flakeType = ["branched", "minimal", "stellar", "emoji1", "emoji2", "emoji3", "dendrite", "ornate"][Math.floor(Math.random() * 8)];
}
ctx.globalAlpha = flake.opacity;
ctx.strokeStyle = flake.color;
ctx.fillStyle = flake.color;
ctx.lineWidth = Math.max(0.5, flake.size * 0.12);
const glowAmount = Math.min(glowIntensity * 0.4, 8);
if (glowAmount > 0.5) {
const hexToRgba = (hex, alpha) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const glowRadius = flake.size * 0.6 + glowAmount;
const gradient2 = ctx.createRadialGradient(
flake.x + driftX,
flake.y,
flake.size * 0.1,
flake.x + driftX,
flake.y,
glowRadius
);
gradient2.addColorStop(0, hexToRgba(flake.color, 0.4));
gradient2.addColorStop(0.5, hexToRgba(flake.color, 0.15));
gradient2.addColorStop(1, hexToRgba(flake.color, 0));
ctx.globalAlpha = flake.opacity;
ctx.fillStyle = gradient2;
ctx.beginPath();
ctx.arc(flake.x + driftX, flake.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = flake.color;
}
const snowflakeType = getSetting("ChristmasTheme.Snowflake.Type");
if (snowflakeType === "custom" || snowflakeType === "mix_custom") {
const snowSrc = getSetting("ChristmasTheme.Snowflake.CustomImage");
if (snowSrc && snowSrc !== customSnowImageSrc) {
customSnowImageSrc = snowSrc;
customSnowImage = new Image();
customSnowImage.onload = () => {
if (app.canvas) app.canvas.setDirty(true, true);
};
customSnowImage.src = snowSrc;
}
}
let effectiveType = snowflakeType;
if (snowflakeType === "mix_custom") {
if (Math.floor(flake.x) % 2 === 0) {
effectiveType = "custom";
}
}
if (effectiveType === "custom") {
if (customSnowImage && customSnowImage.complete && customSnowImage.naturalWidth > 0) {
ctx.save();
ctx.translate(flake.x + driftX, flake.y);
ctx.rotate(flake.rotation);
const size = flake.size * 2;
ctx.drawImage(customSnowImage, -size / 2, -size / 2, size, size);
ctx.restore();
} else {
drawCrystalSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
}
} else if (effectiveType && effectiveType !== "random" && effectiveType !== "mix_custom") {
if (snowflakeType === "classic") drawCrystalSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
else if (snowflakeType === "simple") drawEmoji2745(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
else if (snowflakeType === "bold") drawEmoji2746(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
else drawCrystalSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else {
if (flake.flakeType === "minimal") {
drawCrystalSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else if (flake.flakeType === "stellar") {
drawStellarSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else if (flake.flakeType === "emoji1") {
drawEmoji2744(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else if (flake.flakeType === "emoji2") {
drawEmoji2745(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else if (flake.flakeType === "emoji3") {
drawEmoji2746(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else if (flake.flakeType === "dendrite") {
drawDendriteSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else if (flake.flakeType === "ornate") {
drawOrnateSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
} else {
drawSnowflake(ctx, flake.x + driftX, flake.y, flake.size, flake.rotation);
}
}
}
}
const shootingStarsEnabled = getSetting("ChristmasTheme.Background.ShootingStars");
if (!lowPerfMode && shootingStarsEnabled) {
if (!isExecuting && now - lastShootingStarTime > 1e4 && Math.random() < 0.02) {
shootingStars.push(createShootingStar(width, height));
lastShootingStarTime = now;
}
ctx.lineCap = "round";
for (let i = shootingStars.length - 1; i >= 0; i--) {
const star = shootingStars[i];
star.x += star.vx * deltaTime;
star.y += star.vy * deltaTime;
star.life -= star.decay * deltaTime;
if (star.life <= 0 || star.x > width + 50 || star.y > height + 50) {
shootingStars.splice(i, 1);
continue;
}
const speed = Math.sqrt(star.vx * star.vx + star.vy * star.vy);
const dirX = star.vx / speed;
const dirY = star.vy / speed;
let intensityMod = 1;
if (star.willFlare && star.life < 0.3) {
intensityMod = star.flareIntensity * (1 - star.life / 0.3) * star.life * 3;
}
if (now - star.lastFragmentTime > star.fragmentInterval && star.fragments.length < 8) {
star.fragments.push({
x: star.x - dirX * 3,
y: star.y - dirY * 3,
life: 0.5,
size: 0.3 + Math.random() * 0.4
});
star.lastFragmentTime = now;
}
for (let j = star.fragments.length - 1; j >= 0; j--) {
const frag = star.fragments[j];
frag.life -= deltaTime * 2;
if (frag.life <= 0) {
star.fragments.splice(j, 1);
continue;
}
ctx.fillStyle = `rgba(255, 200, 150, ${frag.life * 0.4})`;
ctx.globalAlpha = frag.life;
ctx.beginPath();
ctx.arc(frag.x, frag.y, frag.size, 0, Math.PI * 2);
ctx.fill();
}
const tailX = star.x - dirX * star.length * star.life;
const tailY = star.y - dirY * star.length * star.life;
const trailGradient = ctx.createLinearGradient(tailX, tailY, star.x, star.y);
trailGradient.addColorStop(0, "rgba(255, 180, 100, 0)");
trailGradient.addColorStop(0.4, `rgba(255, 200, 150, ${star.life * star.brightness * 0.2 * intensityMod})`);
trailGradient.addColorStop(0.8, `rgba(255, 240, 220, ${star.life * star.brightness * 0.5 * intensityMod})`);
trailGradient.addColorStop(1, `rgba(255, 255, 255, ${star.life * star.brightness * 0.9 * intensityMod})`);
ctx.strokeStyle = trailGradient;
ctx.lineWidth = 1 + star.life * 1.5;
ctx.globalAlpha = star.life;
ctx.beginPath();
ctx.moveTo(tailX, tailY);
ctx.lineTo(star.x, star.y);
ctx.stroke();
const headSize = (1.2 + star.life) * intensityMod;
ctx.shadowBlur = 10 * intensityMod;
ctx.shadowColor = "#ffffcc";
ctx.fillStyle = "#ffffff";
ctx.globalAlpha = star.life * star.brightness * intensityMod;
ctx.beginPath();
ctx.arc(star.x, star.y, headSize, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
}
const hasExistingParticles = fireworkRockets.length > 0 || fireworkParticles.length > 0 || fireworkSparks.length > 0;
const fireworksEnabled = getSetting("ChristmasTheme.Background.Fireworks") || finaleActive || finaleGracePeriod && hasExistingParticles;
if (fireworksEnabled) {
if (finaleActive && now - finaleStartTime > FINALE_DURATION) {
finaleActive = false;
finaleGracePeriod = true;
console.log("🎆 Finale ended, entering grace period");
}
if (finaleGracePeriod && !hasExistingParticles) {
finaleGracePeriod = false;
console.log("🎆 Grace period complete");
}
const shouldSpawn = !isExecuting && (finaleActive || getSetting("ChristmasTheme.Background.Fireworks") && !finaleGracePeriod);
if (shouldSpawn) {
const spawnInterval = finaleActive ? 100 + Math.random() * 150 : 2e3 + Math.random() * 2e3;
if (now - lastFireworkTime > spawnInterval) {
const spawnCount = finaleActive ? 2 + Math.floor(Math.random() * 3) : 1;
for (let s = 0; s < spawnCount; s++) {
const rocket = createFireworkRocket(width, height);
if (finaleActive && Math.random() < 0.3) {
rocket.palette = ["#ffd700", "#ffec99", "#fff9db"];
rocket.vy = -14 - Math.random() * 6;
}
fireworkRockets.push(rocket);
}
lastFireworkTime = now;
}
}
for (let i = fireworkRockets.length - 1; i >= 0; i--) {
const rocket = fireworkRockets[i];
rocket.vy += 0.12;
rocket.x += rocket.vx;
rocket.y += rocket.vy;
rocket.age += deltaTime;
rocket.trailTimer += deltaTime;
if (rocket.trailTimer > 0.02) {
rocket.trail.push({ x: rocket.x, y: rocket.y, alpha: 1, size: rocket.size });
rocket.trailTimer = 0;
}
if (rocket.trail.length > 20) rocket.trail.shift();
if (rocket.vy > -2 && !rocket.exploded) {
rocket.exploded = true;
fireworkParticles.push(...createExplosionParticles(rocket.x, rocket.y, rocket.palette, rocket.explosionType));
fireworkRockets.splice(i, 1);
continue;
}
if (rocket.age > 5 && !rocket.exploded) {
rocket.exploded = true;
const simpleCount = 15 + Math.floor(Math.random() * 10);
for (let i2 = 0; i2 < simpleCount; i2++) {
const angle = Math.PI * 2 * i2 / simpleCount;
const speed = 2 + Math.random() * 2;
fireworkParticles.push({
x: rocket.x,
y: rocket.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: rocket.palette[Math.floor(Math.random() * rocket.palette.length)],
alpha: 1,
size: 1.5 + Math.random(),
decay: 0.025,
// Faster fade than normal
trail: [],
hasTrail: false,
// No trails for performance
gravity: 0.05
});
}
fireworkRockets.splice(i, 1);
continue;
}
const primaryColor = rocket.palette[0];
for (let j = 0; j < rocket.trail.length; j++) {
const point = rocket.trail[j];
const progress = j / rocket.trail.length;
const alpha = progress * 0.8;
const size = point.size * progress * 0.8;
ctx.globalAlpha = alpha;
ctx.fillStyle = primaryColor;
ctx.beginPath();
ctx.arc(point.x, point.y, size, 0, Math.PI * 2);
ctx.fill();
}
ctx.shadowBlur = 15;
ctx.shadowColor = primaryColor;
ctx.fillStyle = "#ffffff";
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(rocket.x, rocket.y, rocket.size + 1, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = primaryColor;
ctx.beginPath();
ctx.arc(rocket.x, rocket.y, rocket.size * 0.6, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
for (let i = fireworkParticles.length - 1; i >= 0; i--) {
const p = fireworkParticles[i];
if (p.hasTrail && p.trail) {
p.trail.push({ x: p.x, y: p.y, alpha: p.alpha });
if (p.trail.length > 8) p.trail.shift();
}
p.vy += p.gravity || 0.04;
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.985;
p.vy *= 0.985;
p.alpha -= p.decay;
if (p.crackle && p.crackleTime !== void 0) {
p.crackleTime -= deltaTime;
if (p.crackleTime <= 0 && p.alpha > 0.3) {
for (let c = 0; c < 5; c++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 2;
fireworkSparks.push({
x: p.x,
y: p.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
alpha: 0.8,
size: 0.8,
decay: 0.05,
twinkle: Math.random() * Math.PI * 2
});
}
p.crackle = false;
}
}
if (p.alpha <= 0) {
fireworkParticles.splice(i, 1);
continue;
}
if (p.hasTrail && p.trail && p.trail.length > 0) {
for (let t = 0; t < p.trail.length; t++) {
const tp = p.trail[t];
const trailAlpha = t / p.trail.length * p.alpha * 0.5;
const trailSize = p.size * (t / p.trail.length) * 0.7;
ctx.globalAlpha = trailAlpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(tp.x, tp.y, trailSize, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.shadowBlur = 8;
ctx.shadowColor = p.color;
ctx.fillStyle = p.color;
ctx.globalAlpha = p.alpha;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
for (let i = fireworkSparks.length - 1; i >= 0; i--) {
const s = fireworkSparks[i];
s.vy += 0.03;
s.x += s.vx;
s.y += s.vy;
s.alpha -= s.decay;
s.twinkle += 0.3;
if (s.alpha <= 0) {
fireworkSparks.splice(i, 1);
continue;
}
const twinkleAlpha = s.alpha * (0.5 + Math.sin(s.twinkle) * 0.5);
ctx.fillStyle = "#ffffff";
ctx.globalAlpha = twinkleAlpha;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
}
updateMouseParticles();
drawMouseParticles(ctx);
if (finaleActive || finaleConfetti.length > 0 || finaleStars.length > 0) {
updateFinaleParticles(ctx, ctx.canvas);
drawFinaleParticles(ctx);
}
ctx.restore();
}
function startAnimationLoop() {
if (animationLoopId !== null) return;
function loop() {
if (isPageVisible && getSetting("ChristmasTheme.Background.Enabled")) {
const pauseDuringRender = getSetting("ChristmasTheme.PauseDuringRender");
const shouldRender = !isExecuting || !pauseDuringRender;
if (shouldRender && app.canvas) {
app.canvas.setDirty(true, true);
}
}
animationLoopId = requestAnimationFrame(loop);
}
animationLoopId = requestAnimationFrame(loop);
}
function stopAnimationLoop() {
if (animationLoopId !== null) {
cancelAnimationFrame(animationLoopId);
animationLoopId = null;
}
}
function installBackgroundHook() {
if (!app.canvas) {
console.log("Waiting for app.canvas to install background hook...");
setTimeout(installBackgroundHook, 100);
return;
}
const canvas = app.canvas;
if (!originalDrawBackCanvas) {
originalDrawBackCanvas = canvas.constructor.prototype.drawBackCanvas;
}
canvas.constructor.prototype.drawBackCanvas = function() {
if (originalDrawBackCanvas) {
originalDrawBackCanvas.apply(this, [...arguments]);
}
try {
if (getSetting("ChristmasTheme.Background.Enabled") && this.bgctx) {
drawEnhancedBackground(this.bgctx, this.bgcanvas.width, this.bgcanvas.height);
}
} catch (e) {
console.error("Enhanced background error:", e);
}
};
canvas.setDirty(true, true);
startAnimationLoop();
console.log("✨ Background hook installed successfully");
}
function removeBackgroundHook() {
stopAnimationLoop();
if (originalDrawBackCanvas && app.canvas) {
app.canvas.constructor.prototype.drawBackCanvas = originalDrawBackCanvas;
app.canvas.setDirty(true, true);
}
starEntities = [];
starInitialized = false;
cachedGradient = null;
}
app.registerExtension({
name: "Comfy.EnhancedBackground",
async setup() {
if (extensionSetupComplete) {
return;
}
extensionSetupComplete = true;
console.log("🎨 Setting up Enhanced Background extension...");
initSettingsCache();
app.api.addEventListener("execution_start", () => {
isExecuting = true;
});
app.api.addEventListener("execution_cached", () => {
isExecuting = true;
});
app.api.addEventListener("executed", () => {
isExecuting = false;
executionEndTime = performance.now();
});
app.api.addEventListener("execution_error", () => {
isExecuting = false;
executionEndTime = performance.now();
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.Enabled",
name: "🌟 Background Effect",
type: "combo",
options: [
{ value: true, text: "✨ On" },
{ value: false, text: "⭘ Off" }
],
defaultValue: true,
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.Enabled", value);
if (isInitialSetup) return;
if (value) {
installBackgroundHook();
} else {
removeBackgroundHook();
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.ColorTheme",
name: "🎨 Color Theme",
type: "combo",
options: [
{ value: "classic", text: "🌌 Classic Night" },
{ value: "christmas", text: "🎄 Christmas Forest" },
{ value: "candycane", text: "🍬 Candy Cane Red" },
{ value: "frostnight", text: "❄️ Frost Night" },
{ value: "gingerbread", text: "🍪 Gingerbread" },
{ value: "darknight", text: "🌑 Dark Night" }
],
defaultValue: "frostnight",
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.ColorTheme", value);
if (isInitialSetup) return;
cachedGradient = null;
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.ShootingStars",
name: "☄️ Shooting Stars",
type: "combo",
options: [
{ value: true, text: "✨ On" },
{ value: false, text: "⭘ Off" }
],
defaultValue: true,
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.ShootingStars", value);
if (isInitialSetup) return;
if (!value) {
shootingStars = [];
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.Stars",
name: "⭐ Background Stars",
type: "combo",
options: [
{ value: true, text: "✨ On" },
{ value: false, text: "⭘ Off" }
],
defaultValue: true,
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.Stars", value);
if (isInitialSetup) return;
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.PartyMode",
name: "🪩 Party Mode (Rave Stars)",
type: "combo",
options: [
{ value: true, text: "🎉 On" },
{ value: false, text: "⭘ Off" }
],
defaultValue: false,
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.PartyMode", value);
if (isInitialSetup) return;
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.Fireworks",
name: "🎆 Fireworks",
type: "combo",
options: [
{ value: true, text: "🎇 On" },
{ value: false, text: "⭘ Off" }
],
defaultValue: false,
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.Fireworks", value);
if (isInitialSetup) return;
if (!value) {
fireworkRockets = [];
fireworkParticles = [];
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.MouseEffect",
name: "✨ Mouse Trail Effect",
type: "combo",
options: [
{ value: "none", text: "⭘ Off" },
{ value: "sparkler", text: "✨ Sparkler" },
{ value: "snowflake", text: "❄️ Snowflake" },
{ value: "confetti", text: "🎊 Confetti" },
{ value: "stardust", text: "⭐ Stardust" },
{ value: "comet", text: "☄️ Comet" },
{ value: "aurora", text: "🌌 Aurora" },
{ value: "ribbon", text: "🎀 Ribbon" },
{ value: "crystal", text: "💎 Crystal" },
{ value: "petals", text: "🌸 Petals" },
{ value: "gifts", text: "🎁 Gifts" },
{ value: "candy", text: "🍬 Candy" },
{ value: "orb", text: "🔮 Magic Orb" },
{ value: "magic", text: "✨ Magic Wand" },
{ value: "nova", text: "🌟 Nova" },
{ value: "bubbles", text: "💧 Bubbles" },
{ value: "embers", text: "🔥 Embers" },
{ value: "lightning", text: "⚡ Lightning" },
{ value: "leaves", text: "🍂 Leaves" },
{ value: "wishes", text: "💫 Wishes" },
{ value: "notes", text: "🎵 Notes" },
{ value: "hearts", text: "💖 Hearts" }
],
defaultValue: "none",
section: "Background Theme",
tooltip: "Choose a mouse trail effect",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.MouseEffect", value);
if (isInitialSetup) return;
if (!value || value === "none") {
mouseParticles = [];
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
app.ui.settings.addSetting({
id: "ChristmasTheme.Background.Countdown",
name: "🎊 New Year Countdown",
type: "combo",
options: [
{ value: true, text: "🕐 On" },
{ value: false, text: "⭘ Off" }
],
defaultValue: true,
section: "Background Theme",
onChange: async (value) => {
updateCache("ChristmasTheme.Background.Countdown", value);
if (isInitialSetup) return;
toggleCountdownDisplay(value);
}
});
loadSettingFromStorage("ChristmasTheme.Background.Enabled");
loadSettingFromStorage("ChristmasTheme.Background.ColorTheme");
loadSettingFromStorage("ChristmasTheme.Background.Stars");
loadSettingFromStorage("ChristmasTheme.Background.PartyMode");
loadSettingFromStorage("ChristmasTheme.Background.ShootingStars");
loadSettingFromStorage("ChristmasTheme.Background.Fireworks");
loadSettingFromStorage("ChristmasTheme.Background.MouseEffect");
loadSettingFromStorage("ChristmasTheme.Background.Countdown");
isInitialSetup = false;
if (getSetting("ChristmasTheme.Background.Enabled")) {
installBackgroundHook();
}
if (getSetting("ChristmasTheme.Background.Countdown")) {
toggleCountdownDisplay(true);
}
return () => {
removeBackgroundHook();
if (countdownInterval) {
clearInterval(countdownInterval);
}
if (countdownElement) {
countdownElement.remove();
}
};
}
});
export {
isExecuting,
isPageVisible
};
//# sourceMappingURL=background-themes.js.map