Files
AEmotionStudio-ComfyUI-Chri…/js/aether-snow.js
T
AEmotionStudio 4c2482b33d perf: optimize custom assets and enhance link animations
- Add image optimization for custom snowflake uploads (resize + WebP conversion).
- Implement caching for custom snowflake images to improve rendering performance.
- Refactor link renderers to support multiple styles (spline, straight, linear).
- Add/restore special link effects: Candy Cane, Frost, and Aurora.
- Increase custom image upload size limit to 5MB.
2026-01-11 14:40:59 -08:00

418 lines
18 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { getSetting, COLOR_SCHEMES } from "./settings-cache.js";
import { isPageVisible, isExecuting } from "./background-themes.js";
const SNOWFLAKE_CONFIG = {
MIN_SIZE: 8,
MAX_SIZE: 18,
FLAKE_COUNTS: {
high: 50,
medium: 35,
low: 20
},
BATCH_SIZE: 5
};
const SNOWFLAKE_TYPES = {
// Branched - 6 arms with side branches
branched: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
d += `M ${size / 2} ${size / 2} L ${size / 2 + sin * size * 0.45} ${size / 2 - cos * size * 0.45} `;
const bx = size / 2 + sin * size * 0.25, by = size / 2 - cos * size * 0.25;
const bLen = size * 0.15;
const bAngle = Math.PI / 5;
d += `M ${bx} ${by} L ${bx + Math.sin(angle - bAngle) * bLen} ${by - Math.cos(angle - bAngle) * bLen} `;
d += `M ${bx} ${by} L ${bx + Math.sin(angle + bAngle) * bLen} ${by - Math.cos(angle + bAngle) * bLen} `;
}
return d;
},
minimal: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
d += `M ${size / 2} ${size / 2} L ${size / 2 + sin * size * 0.45} ${size / 2 - cos * size * 0.45} `;
const bx = size / 2 + sin * size * 0.25, by = size / 2 - cos * size * 0.25;
const bLen = size * 0.12;
d += `M ${bx} ${by} L ${bx + Math.sin(angle - Math.PI / 4) * bLen} ${by - Math.cos(angle - Math.PI / 4) * bLen} `;
d += `M ${bx} ${by} L ${bx + Math.sin(angle + Math.PI / 4) * bLen} ${by - Math.cos(angle + Math.PI / 4) * bLen} `;
}
return d;
},
stellar: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
d += `M ${size / 2} ${size / 2} L ${size / 2 + sin * size * 0.45} ${size / 2 - cos * size * 0.45} `;
[0.2, 0.32].forEach((pos) => {
const bx = size / 2 + sin * size * pos, by = size / 2 - cos * size * pos;
const bLen = size * (0.18 - pos * 0.3);
d += `M ${bx} ${by} L ${bx + Math.sin(angle - Math.PI / 4) * bLen} ${by - Math.cos(angle - Math.PI / 4) * bLen} `;
d += `M ${bx} ${by} L ${bx + Math.sin(angle + Math.PI / 4) * bLen} ${by - Math.cos(angle + Math.PI / 4) * bLen} `;
});
}
return d;
},
emoji1: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
const tipX = size / 2 + sin * size * 0.45, tipY = size / 2 - cos * size * 0.45;
d += `M ${size / 2} ${size / 2} L ${tipX} ${tipY} `;
const tip = size * 0.06;
d += `M ${tipX - cos * tip} ${tipY - sin * tip} L ${tipX} ${tipY} L ${tipX + cos * tip} ${tipY + sin * tip} `;
const bx = size / 2 + sin * size * 0.27, by = size / 2 - cos * size * 0.27;
const bLen = size * 0.13;
d += `M ${bx} ${by} L ${bx + Math.sin(angle - Math.PI / 4) * bLen} ${by - Math.cos(angle - Math.PI / 4) * bLen} `;
d += `M ${bx} ${by} L ${bx + Math.sin(angle + Math.PI / 4) * bLen} ${by - Math.cos(angle + Math.PI / 4) * bLen} `;
}
return d;
},
emoji2: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
d += `M ${size / 2 + sin * size * 0.08} ${size / 2 - cos * size * 0.08} L ${size / 2 + sin * size * 0.45} ${size / 2 - cos * size * 0.45} `;
const cx = size / 2 + sin * size * 0.3, cy = size / 2 - cos * size * 0.3;
const cLen = size * 0.05;
d += `M ${cx - cos * cLen} ${cy - sin * cLen} L ${cx + cos * cLen} ${cy + sin * cLen} `;
}
return d;
},
emoji3: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
d += `M ${size / 2} ${size / 2} L ${size / 2 + sin * size * 0.45} ${size / 2 - cos * size * 0.45} `;
const cx = size / 2 + sin * size * 0.22, cy = size / 2 - cos * size * 0.22;
const cLen = size * 0.12;
const cAngle = Math.PI / 5;
d += `M ${cx + Math.sin(angle - cAngle) * cLen} ${cy - Math.cos(angle - cAngle) * cLen * 0.7} L ${cx} ${cy} L ${cx + Math.sin(angle + cAngle) * cLen} ${cy - Math.cos(angle + cAngle) * cLen * 0.7} `;
}
return d;
},
// Dendrite - fernlike with multiple branch levels
dendrite: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
d += `M ${size / 2} ${size / 2} L ${size / 2 + sin * size * 0.45} ${size / 2 - cos * size * 0.45} `;
[0.18, 0.28, 0.38].forEach((pos, idx) => {
const bx = size / 2 + sin * size * pos, by = size / 2 - cos * size * pos;
const bLen = size * (0.14 - idx * 0.03);
const bAngle = Math.PI / 4;
d += `M ${bx} ${by} L ${bx + Math.sin(angle - bAngle) * bLen} ${by - Math.cos(angle - bAngle) * bLen} `;
d += `M ${bx} ${by} L ${bx + Math.sin(angle + bAngle) * bLen} ${by - Math.cos(angle + bAngle) * bLen} `;
});
}
return d;
},
// Ornate - decorative with diamond tips
ornate: (size) => {
let d = "";
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i;
const cos = Math.cos(angle), sin = Math.sin(angle);
const tipX = size / 2 + sin * size * 0.42, tipY = size / 2 - cos * size * 0.42;
d += `M ${size / 2} ${size / 2} L ${tipX} ${tipY} `;
const dSize = size * 0.05;
d += `M ${tipX} ${tipY - dSize} L ${tipX + dSize * 0.7} ${tipY} L ${tipX} ${tipY + dSize} L ${tipX - dSize * 0.7} ${tipY} Z `;
const bx = size / 2 + sin * size * 0.22, by = size / 2 - cos * size * 0.22;
const bLen = size * 0.14;
d += `M ${bx} ${by} L ${bx + Math.sin(angle - Math.PI / 3) * bLen} ${by - Math.cos(angle - Math.PI / 3) * bLen} `;
d += `M ${bx} ${by} L ${bx + Math.sin(angle + Math.PI / 3) * bLen} ${by - Math.cos(angle + Math.PI / 3) * bLen} `;
}
return d;
}
};
const FLAKE_TYPE_NAMES = Object.keys(SNOWFLAKE_TYPES);
let cachedCustomImage = null;
function getPerformanceTier() {
const isLowEnd = navigator.hardwareConcurrency <= 2 || navigator.deviceMemory !== void 0 && navigator.deviceMemory <= 2 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isLowEnd) return "low";
const isHighEnd = navigator.hardwareConcurrency >= 8 && (navigator.deviceMemory === void 0 || navigator.deviceMemory >= 8);
return isHighEnd ? "high" : "medium";
}
function createSVGSnowflake(size, color, flakeType) {
const ns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(ns, "svg");
svg.setAttribute("width", String(size));
svg.setAttribute("height", String(size));
svg.setAttribute("viewBox", `0 0 ${size} ${size}`);
svg.style.overflow = "visible";
const path = document.createElementNS(ns, "path");
path.setAttribute("d", SNOWFLAKE_TYPES[flakeType](size));
path.setAttribute("stroke", color);
path.setAttribute("stroke-width", String(Math.max(1, size * 0.06)));
path.setAttribute("stroke-linecap", "round");
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("fill", "none");
const dot = document.createElementNS(ns, "circle");
dot.setAttribute("cx", String(size / 2));
dot.setAttribute("cy", String(size / 2));
dot.setAttribute("r", String(size * 0.04));
dot.setAttribute("fill", color);
svg.appendChild(path);
svg.appendChild(dot);
return svg;
}
app.registerExtension({
name: "Christmas.Theme.SnowEffect",
async setup() {
console.log("✨ Initializing Premium Snow Effect with JS animation...");
try {
const perfTier = getPerformanceTier();
const totalFlakes = SNOWFLAKE_CONFIG.FLAKE_COUNTS[perfTier];
console.log(`❄️ Performance tier: ${perfTier}, using ${totalFlakes} foreground snowflakes (8 designs)`);
const container = document.createElement("div");
container.id = "comfy-aether-snow";
Object.assign(container.style, {
position: "fixed",
top: "0",
left: "0",
width: "100%",
height: "100%",
overflow: "hidden"
});
container.style.pointerEvents = "none";
container.style.zIndex = "50";
document.body.appendChild(container);
const style = document.createElement("style");
style.id = "snowflake-styles";
style.textContent = `
.snowflake {
position: absolute;
pointer-events: none;
user-select: none;
will-change: transform, filter;
}
`;
document.head.appendChild(style);
let flakes = [];
let animationId = null;
let lastTime = performance.now();
let snowAnimTime = 0;
const getSnowflakeColor = () => {
const colorScheme = getSetting("ChristmasTheme.Snowflake.ColorScheme");
const christmasColors = getSetting("ChristmasTheme.ChristmasEffects.ColorScheme");
switch (colorScheme) {
case "blue":
return ["#d4f1f9", "#c8e8f0", "#b8dce8"][Math.floor(Math.random() * 3)];
case "rainbow":
return ["#ffb3ba", "#bae1ff", "#baffc9", "#ffffba", "#ffdfba"][Math.floor(Math.random() * 5)];
case "match":
const palette = COLOR_SCHEMES[christmasColors] || COLOR_SCHEMES.traditional;
return palette[Math.floor(Math.random() * palette.length)];
case "newyear":
return COLOR_SCHEMES.newyear[Math.floor(Math.random() * 5)];
default:
return ["#ffffff", "#f8f9fa", "#f1f3f5"][Math.floor(Math.random() * 3)];
}
};
const getGlowFilter = (color) => {
const glowIntensity = getSetting("ChristmasTheme.Snowflake.Glow") || 10;
const glowAmount = Math.min(glowIntensity * 0.4, 8);
if (glowAmount < 1) return "none";
return `drop-shadow(0 0 ${glowAmount}px ${color})`;
};
const createFlakeEntity = () => {
const size = SNOWFLAKE_CONFIG.MIN_SIZE + Math.random() * (SNOWFLAKE_CONFIG.MAX_SIZE - SNOWFLAKE_CONFIG.MIN_SIZE);
const sizeRatio = size / SNOWFLAKE_CONFIG.MAX_SIZE;
const color = getSnowflakeColor();
return {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
size,
opacity: 0.3 + Math.random() * 0.2 + sizeRatio * 0.35,
// Random base 0.3-0.5 plus size factor
color,
speed: 12 + Math.random() * 18,
// Slightly slower
drift: (Math.random() - 0.5) * 30,
driftSpeed: 0.3 + Math.random() * 0.4,
driftOffset: Math.random() * Math.PI * 2,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.5,
flakeType: (() => {
const type = getSetting("ChristmasTheme.Snowflake.Type");
if (type === "custom") return "custom";
if (type === "mix_custom") return Math.random() > 0.5 ? "custom" : FLAKE_TYPE_NAMES[Math.floor(Math.random() * FLAKE_TYPE_NAMES.length)];
if (type && type !== "random" && FLAKE_TYPE_NAMES.includes(type)) return type;
return FLAKE_TYPE_NAMES[Math.floor(Math.random() * FLAKE_TYPE_NAMES.length)];
})(),
element: null
};
};
const updateFlakeGlow = (flake) => {
if (flake.element) {
flake.element.style.filter = getGlowFilter(flake.color);
}
};
const initFlakes = () => {
container.innerHTML = "";
flakes = [];
const snowType = getSetting("ChristmasTheme.Snowflake.Type");
const snowSrc = getSetting("ChristmasTheme.Snowflake.CustomImage");
const needsCustomImage = (snowType === "custom" || snowType === "mix_custom") && snowSrc;
if (needsCustomImage && (!cachedCustomImage || cachedCustomImage.src !== snowSrc)) {
cachedCustomImage = new Image();
cachedCustomImage.src = snowSrc;
console.log("❄️ Caching custom snowflake image");
}
for (let i = 0; i < totalFlakes; i++) {
const flakeData = createFlakeEntity();
const flake = document.createElement("div");
flake.className = "snowflake";
if (flakeData.flakeType === "custom" && cachedCustomImage) {
const img = cachedCustomImage.cloneNode();
img.style.width = String(flakeData.size * 2) + "px";
img.style.height = String(flakeData.size * 2) + "px";
img.style.objectFit = "contain";
img.draggable = false;
flake.appendChild(img);
} else if (flakeData.flakeType === "custom") {
const svg = createSVGSnowflake(flakeData.size, flakeData.color, "minimal");
flake.appendChild(svg);
} else {
const svg = createSVGSnowflake(flakeData.size, flakeData.color, flakeData.flakeType);
flake.appendChild(svg);
}
flake.style.opacity = String(flakeData.opacity);
if (flakeData.flakeType !== "custom") {
flake.style.filter = getGlowFilter(flakeData.color);
}
container.appendChild(flake);
flakeData.element = flake;
flakes.push(flakeData);
}
};
const updateFlakeColor = (flake) => {
var _a;
flake.color = getSnowflakeColor();
const svg = (_a = flake.element) == null ? void 0 : _a.querySelector("svg");
if (svg) {
const path = svg.querySelector("path");
const dot = svg.querySelector("circle");
if (path) path.setAttribute("stroke", flake.color);
if (dot) dot.setAttribute("fill", flake.color);
}
updateFlakeGlow(flake);
};
const animate = (currentTime) => {
if (!isPageVisible) {
animationId = requestAnimationFrame(animate);
return;
}
if (!isExecuting && currentTime - lastTime > 1e3) {
lastTime = currentTime;
}
const deltaTime = isExecuting ? 0 : (currentTime - lastTime) / 1e3;
if (!isExecuting) {
lastTime = currentTime;
snowAnimTime += deltaTime;
}
const height = window.innerHeight;
const width = window.innerWidth;
for (const flake of flakes) {
if (!isExecuting) {
flake.y += flake.speed * deltaTime;
flake.rotation += flake.rotationSpeed * deltaTime;
if (flake.y > height + 30) {
flake.y = -30;
flake.x = Math.random() * width;
const type = getSetting("ChristmasTheme.Snowflake.Type");
if (type === "custom") flake.flakeType = "custom";
else if (type === "mix_custom") flake.flakeType = Math.random() > 0.5 ? "custom" : FLAKE_TYPE_NAMES[Math.floor(Math.random() * FLAKE_TYPE_NAMES.length)];
else if (type && type !== "random" && FLAKE_TYPE_NAMES.includes(type)) flake.flakeType = type;
else flake.flakeType = FLAKE_TYPE_NAMES[Math.floor(Math.random() * FLAKE_TYPE_NAMES.length)];
if (flake.element) {
flake.element.innerHTML = "";
if (flake.flakeType === "custom" && cachedCustomImage) {
const img = cachedCustomImage.cloneNode();
img.style.width = String(flake.size * 2) + "px";
img.style.height = String(flake.size * 2) + "px";
img.style.objectFit = "contain";
img.draggable = false;
flake.element.appendChild(img);
} else if (flake.flakeType === "custom") {
const svg = createSVGSnowflake(flake.size, flake.color, "minimal");
flake.element.appendChild(svg);
} else {
const svg = createSVGSnowflake(flake.size, flake.color, flake.flakeType);
flake.element.appendChild(svg);
}
}
updateFlakeColor(flake);
}
}
const driftX = Math.sin(snowAnimTime * flake.driftSpeed + flake.driftOffset) * flake.drift;
if (flake.element) {
flake.element.style.transform = `translate(${flake.x + driftX}px, ${flake.y}px) rotate(${flake.rotation}rad)`;
}
}
animationId = requestAnimationFrame(animate);
};
const isEnabled = getSetting("ChristmasTheme.Snowflake.Enabled");
container.style.display = isEnabled ? "block" : "none";
if (isEnabled) {
initFlakes();
animationId = requestAnimationFrame(animate);
}
const handleVisibility = () => {
if (document.visibilityState !== "hidden") {
lastTime = performance.now();
}
};
document.addEventListener("visibilitychange", handleVisibility);
let lastColorScheme = getSetting("ChristmasTheme.Snowflake.ColorScheme");
let lastGlow = getSetting("ChristmasTheme.Snowflake.Glow");
let lastEnabled = getSetting("ChristmasTheme.Snowflake.Enabled");
const checkSettings = setInterval(() => {
const currentEnabled = getSetting("ChristmasTheme.Snowflake.Enabled");
const currentColorScheme = getSetting("ChristmasTheme.Snowflake.ColorScheme");
const currentGlow = getSetting("ChristmasTheme.Snowflake.Glow");
if (currentEnabled !== lastEnabled) {
if (currentEnabled === 1 || currentEnabled === true) {
container.style.display = "block";
if (flakes.length === 0) {
initFlakes();
if (!animationId) animationId = requestAnimationFrame(animate);
}
} else {
container.style.display = "none";
}
lastEnabled = currentEnabled;
}
if (currentColorScheme !== lastColorScheme) {
flakes.forEach(updateFlakeColor);
lastColorScheme = currentColorScheme;
}
if (currentGlow !== lastGlow) {
flakes.forEach(updateFlakeGlow);
lastGlow = currentGlow;
}
if (!currentEnabled && flakes.length > 0) {
flakes = [];
container.innerHTML = "";
}
}, 500);
return () => {
clearInterval(checkSettings);
document.removeEventListener("visibilitychange", handleVisibility);
if (animationId) cancelAnimationFrame(animationId);
container.remove();
style.remove();
};
} catch (error) {
console.error("❌ Failed to initialize Snow Effect:", error);
}
}
});
//# sourceMappingURL=aether-snow.js.map