Files
AEmotionStudio-ComfyUI-Chri…/js/link_animations.js
T
AEmotionStudio 4ba82a7565 perf: optimize candy cane effect and refactor link path tracing
- Introduce  to  for reusable path drawing.
- Refactor Candy Cane effect to use native  for smoother performance and cleaner code.
- Align Candy Cane animation flow and speed with other effects.
2026-01-11 15:06:32 -08:00

480 lines
18 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { initSettingsCache, getSetting, COLOR_SCHEMES } from "./settings-cache.js";
import { isPageVisible } from "./background-themes.js";
app.registerExtension({
name: "Christmas.Theme.LightSwitch",
async setup() {
initSettingsCache();
const PerformanceMonitor = {
frameTimeHistory: new Float32Array(60),
currentIndex: 0,
sum: 0,
warningThreshold: 8,
// reduced threshold
criticalThreshold: 16,
_lastMode: "normal",
adaptiveSettings: {
normal: { lightSpacing: 15, skipCaps: false, reducedGlow: false },
warning: { lightSpacing: 25, skipCaps: true, reducedGlow: false },
critical: { lightSpacing: 40, skipCaps: true, reducedGlow: true }
},
addFrameTime(time) {
this.sum -= this.frameTimeHistory[this.currentIndex];
this.frameTimeHistory[this.currentIndex] = time;
this.sum += time;
this.currentIndex = (this.currentIndex + 1) % 60;
const avg = this.sum / 60;
let mode = "normal";
if (avg > this.criticalThreshold) mode = "critical";
else if (avg > this.warningThreshold) mode = "warning";
if (mode !== this._lastMode) {
console.log(`🎄 Christmas Lights: Switching to ${mode} mode (avg frame time: ${avg.toFixed(2)}ms)`);
this._lastMode = mode;
}
return mode;
},
getSettings() {
return this.adaptiveSettings[this._lastMode];
}
};
const ArrayPool = {
pool: [],
init() {
for (let i = 0; i < 1e3; i++) this.pool.push(new Float32Array(2));
},
get() {
return this.pool.pop() || new Float32Array(2);
},
release(arr) {
if (this.pool.length < 2e3) this.pool.push(arr);
}
};
ArrayPool.init();
const State = {
isRunning: true,
phase: 0,
lastFrame: 0,
animationFrame: null,
performanceMode: "normal",
isRendering: false,
linkDataCache: new Array(1e3).fill(null).map(() => ({
start: new Float32Array(2),
end: new Float32Array(2),
color: null
})),
linkDataIndex: 0
};
const LinkRenderers = {
spline: {
getLength(start, end) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dist = Math.sqrt(dx * dx + dy * dy);
return dist * 1.15;
},
getPoint(start, end, t, out) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dist = Math.sqrt(dx * dx + dy * dy);
const bendDistance = Math.min(dist * 0.5, 100);
const p0x = start[0], p0y = start[1];
const p1x = start[0] + bendDistance, p1y = start[1];
const p2x = end[0] - bendDistance, p2y = end[1];
const p3x = end[0], p3y = end[1];
const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;
out[0] = mt3 * p0x + 3 * mt2 * t * p1x + 3 * mt * t2 * p2x + t3 * p3x;
out[1] = mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y;
},
tracePath(ctx, start, end) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dist = Math.sqrt(dx * dx + dy * dy);
const bendDistance = Math.min(dist * 0.5, 100);
ctx.moveTo(start[0], start[1]);
ctx.bezierCurveTo(start[0] + bendDistance, start[1], end[0] - bendDistance, end[1], end[0], end[1]);
},
draw(ctx, start, end, color, thickness) {
ctx.beginPath();
this.tracePath(ctx, start, end);
ctx.strokeStyle = color;
ctx.lineWidth = thickness * 0.8;
ctx.stroke();
}
},
straight: {
getLength(start, end) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
return Math.sqrt(dx * dx + dy * dy);
},
getPoint(start, end, t, out) {
out[0] = start[0] + (end[0] - start[0]) * t;
out[1] = start[1] + (end[1] - start[1]) * t;
},
tracePath(ctx, start, end) {
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
},
draw(ctx, start, end, color, thickness) {
ctx.beginPath();
this.tracePath(ctx, start, end);
ctx.strokeStyle = color;
ctx.lineWidth = thickness * 0.8;
ctx.stroke();
}
},
linear: {
getLength(start, end) {
const midX = (start[0] + end[0]) / 2;
return Math.abs(midX - start[0]) + Math.abs(end[1] - start[1]) + Math.abs(end[0] - midX);
},
getPoint(start, end, t, out) {
const midX = (start[0] + end[0]) / 2;
if (t <= 0.33) {
const segmentT = t / 0.33;
out[0] = start[0] + (midX - start[0]) * segmentT;
out[1] = start[1];
} else if (t <= 0.67) {
const segmentT = (t - 0.33) / 0.34;
out[0] = midX;
out[1] = start[1] + (end[1] - start[1]) * segmentT;
} else {
const segmentT = (t - 0.67) / 0.33;
out[0] = midX + (end[0] - midX) * segmentT;
out[1] = end[1];
}
},
tracePath(ctx, start, end) {
const midX = (start[0] + end[0]) / 2;
ctx.moveTo(start[0], start[1]);
ctx.lineTo(midX, start[1]);
ctx.lineTo(midX, end[1]);
ctx.lineTo(end[0], end[1]);
},
draw(ctx, start, end, color, thickness) {
ctx.beginPath();
this.tracePath(ctx, start, end);
ctx.strokeStyle = color;
ctx.lineWidth = thickness * 0.8;
ctx.stroke();
}
},
hidden: {
getLength(start, end) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
return Math.sqrt(dx * dx + dy * dy);
},
getPoint(start, end, t, out) {
out[0] = start[0] + (end[0] - start[0]) * t;
out[1] = start[1] + (end[1] - start[1]) * t;
},
tracePath(ctx, start, end) {
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
},
draw() {
}
}
};
const SIN_TABLE_SIZE = 1024;
const SIN_TABLE = new Float32Array(SIN_TABLE_SIZE);
for (let i = 0; i < SIN_TABLE_SIZE; i++) {
SIN_TABLE[i] = Math.sin(i / SIN_TABLE_SIZE * Math.PI * 2);
}
function fastSin(angle) {
const index = Math.floor(angle / (Math.PI * 2) * SIN_TABLE_SIZE) % SIN_TABLE_SIZE;
return SIN_TABLE[index >= 0 ? index : index + SIN_TABLE_SIZE];
}
const TimingManager = {
lastTime: 0,
update() {
const now = performance.now();
if (!this.lastTime) this.lastTime = now;
const delta = (now - this.lastTime) / 1e3;
this.lastTime = now;
return Math.min(delta, 0.05);
}
};
const AnimationState = {
phase: 0,
update(delta) {
const speed = getSetting("ChristmasTheme.ChristmasEffects.AnimationSpeed") || 1;
this.phase += delta * speed;
return this.phase;
}
};
let retryCount = 0;
const installChristmasLights = () => {
if (!app.canvas || !app.graph) {
if (retryCount < 20) {
retryCount++;
setTimeout(installChristmasLights, 200);
}
return;
}
const LGraphCanvas = app.canvas.constructor;
const origDrawConnections = LGraphCanvas.prototype.drawConnections;
console.log("🎄 Installing Christmas Lights (Optimized)...");
LGraphCanvas.prototype.renderChristmasLights = function(ctx, items, phase) {
getSetting("ChristmasTheme.ChristmasEffects.LightSwitch");
const currentSchemeName = getSetting("ChristmasTheme.ChristmasEffects.ColorScheme");
const christmasColors = COLOR_SCHEMES[currentSchemeName] || COLOR_SCHEMES.traditional;
const bulbShape = getSetting("ChristmasTheme.ChristmasEffects.BulbShape");
const settings = PerformanceMonitor.getSettings();
const baseSpacing = settings.lightSpacing;
const skipCaps = settings.skipCaps;
const reducedGlow = settings.reducedGlow;
const glowIntensity = getSetting("ChristmasTheme.ChristmasEffects.GlowIntensity");
const colorCount = christmasColors.length;
const linkStyle = getSetting("ChristmasTheme.LinkRenderMode") || "spline";
const renderer = LinkRenderers[linkStyle] || LinkRenderers.spline;
const tempPoint = new Float32Array(2);
const Thickness = getSetting("ChristmasTheme.ChristmasEffects.Thickness") || 3;
const Direction = -1;
const effectMode = getSetting("ChristmasTheme.ChristmasEffects.Twinkle");
const candyCaneMode = effectMode === "candycane";
const frostMode = effectMode === "frost";
const auroraMode = effectMode === "aurora";
if (candyCaneMode || frostMode || auroraMode) {
for (let itemIdx = 0; itemIdx < items.length; itemIdx++) {
const { start, end, color } = items[itemIdx];
const totalLength = renderer.getLength(start, end);
if (candyCaneMode) {
const stripeWidth = 15;
const speed = 45;
ctx.beginPath();
renderer.tracePath(ctx, start, end);
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = Thickness * 2;
ctx.lineCap = "round";
ctx.stroke();
ctx.beginPath();
renderer.tracePath(ctx, start, end);
ctx.strokeStyle = "#ff0000";
ctx.lineWidth = Thickness * 2;
ctx.lineCap = "round";
ctx.setLineDash([stripeWidth, stripeWidth]);
ctx.lineDashOffset = -phase * speed;
ctx.globalAlpha = 0.9;
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
} else if (frostMode) {
const numCrystals = Math.floor(totalLength / baseSpacing);
const frostColors = ["#e0ffff", "#b0e0e6", "#87ceeb", "#add8e6", "#ffffff"];
for (let i = 0; i <= numCrystals; i++) {
const t = i / numCrystals;
renderer.getPoint(start, end, t, tempPoint);
const shimmer = 0.6 + fastSin(-phase * 4 + i * 2) * 0.4;
const crystalColor = frostColors[i % frostColors.length];
ctx.shadowBlur = 15 * shimmer;
ctx.shadowColor = "#87ceeb";
ctx.fillStyle = crystalColor;
ctx.globalAlpha = shimmer * 0.8;
const size = Thickness * (1 + shimmer * 0.5);
ctx.beginPath();
for (let p = 0; p < 6; p++) {
const angle = p / 6 * Math.PI * 2 - Math.PI / 2;
const px = tempPoint[0] + Math.cos(angle) * size;
const py = tempPoint[1] + Math.sin(angle) * size;
if (p === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.arc(tempPoint[0], tempPoint[1], size * 0.3, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.globalAlpha = shimmer;
ctx.fill();
}
} else if (auroraMode) {
const numPoints = Math.floor(totalLength / 5);
const auroraColors = ["#00ff88", "#00ffcc", "#00ccff", "#0088ff", "#8800ff", "#ff00ff"];
for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints;
renderer.getPoint(start, end, t, tempPoint);
const waveOffset = fastSin(t * Math.PI * 3 - phase * 6) * 8;
const x = tempPoint[0];
const y = tempPoint[1] + waveOffset;
const colorT = ((t - phase * 1.5) % 1 + 1) % 1;
const colorIndex = Math.floor(colorT * auroraColors.length) % auroraColors.length;
const auroraColor = auroraColors[colorIndex];
const pulse = 0.5 + fastSin(-phase * 6 + t * Math.PI * 2) * 0.5;
ctx.shadowBlur = 20 * pulse;
ctx.shadowColor = auroraColor;
ctx.fillStyle = auroraColor;
ctx.globalAlpha = pulse * 0.7;
ctx.beginPath();
ctx.arc(x, y, Thickness * (1 + pulse * 0.5), 0, Math.PI * 2);
ctx.fill();
}
}
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
}
return;
}
const drawIcicleBulb = (ctx2, x, y, size) => {
const bulbWidth = size * 1.2;
const bulbHeight = size * 3;
ctx2.beginPath();
ctx2.moveTo(x, y - size * 0.5);
ctx2.bezierCurveTo(
x - bulbWidth,
y,
x - bulbWidth * 0.6,
y + bulbHeight * 0.5,
x,
y + bulbHeight
// Pointed tip at bottom
);
ctx2.bezierCurveTo(
x + bulbWidth * 0.6,
y + bulbHeight * 0.5,
x + bulbWidth,
y,
x,
y - size * 0.5
);
ctx2.closePath();
};
for (let itemIdx = 0; itemIdx < items.length; itemIdx++) {
const { start, end, color } = items[itemIdx];
if (linkStyle !== "hidden") {
ctx.globalAlpha = 0.8;
ctx.shadowBlur = 0;
renderer.draw(ctx, start, end, color || "#888", Thickness);
ctx.globalAlpha = 1;
}
if (linkStyle === "hidden" && !getSetting("ChristmasTheme.ChristmasEffects.LightSwitch")) {
continue;
}
const totalLength = renderer.getLength(start, end);
const numLights = Math.floor(totalLength / baseSpacing);
if (numLights < 1) continue;
const effectiveGlow = reducedGlow ? glowIntensity * 0.5 : glowIntensity;
for (let i = 0; i <= numLights; i++) {
const t = i / numLights;
renderer.getPoint(start, end, t, tempPoint);
const wobble = fastSin(t * Math.PI * 4) * 5;
const x = tempPoint[0];
const y = tempPoint[1] + wobble;
const colorIndex = ((i - Math.floor(phase * 2 * Direction)) % colorCount + colorCount) % colorCount;
const lightColor = christmasColors[colorIndex];
let flicker;
if (effectMode === "steady") {
flicker = 1;
} else if (effectMode === "sparkle") {
flicker = 0.7 + fastSin(-phase * 8 + i * 5) * 0.3 * Math.random();
} else {
flicker = 0.85 + fastSin(-phase * 5 + i * 3) * 0.15;
}
ctx.shadowBlur = effectiveGlow * 1.5 * flicker;
ctx.fillStyle = lightColor;
ctx.shadowColor = lightColor;
ctx.globalAlpha = flicker;
if (bulbShape === "icicle") {
drawIcicleBulb(ctx, x, y, Thickness);
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(x, y, Thickness * 1.5, 0, Math.PI * 2);
ctx.fill();
}
if (!skipCaps) {
ctx.beginPath();
ctx.shadowBlur = 0;
const capY = bulbShape === "icicle" ? y - Thickness * 0.8 : y - Thickness;
ctx.arc(x, capY, Thickness * 0.5, 0, Math.PI * 2);
ctx.fillStyle = "#c0c0c0";
ctx.globalAlpha = 1;
ctx.fill();
}
}
ctx.globalAlpha = 1;
}
};
LGraphCanvas.prototype.drawConnections = function(ctx) {
try {
if (!isPageVisible) {
return;
}
const startTime = performance.now();
const animStyle = getSetting("ChristmasTheme.ChristmasEffects.LightSwitch");
if (animStyle === 0) {
origDrawConnections.call(this, ctx);
return;
}
const delta = TimingManager.update();
const phase = AnimationState.update(delta);
ctx.save();
State.linkDataIndex = 0;
for (const linkId in this.graph.links) {
const linkData = this.graph.links[linkId];
if (!linkData) continue;
const originNode = this.graph._nodes_by_id[linkData.origin_id];
const targetNode = this.graph._nodes_by_id[linkData.target_id];
if (!originNode || !targetNode || originNode.flags.collapsed || targetNode.flags.collapsed) continue;
let data = State.linkDataCache[State.linkDataIndex];
if (!data) {
data = {
start: ArrayPool.get(),
end: ArrayPool.get(),
color: null
};
State.linkDataCache[State.linkDataIndex] = data;
}
originNode.getConnectionPos(false, linkData.origin_slot, data.start);
targetNode.getConnectionPos(true, linkData.target_slot, data.end);
data.color = linkData.type ? LGraphCanvas.link_type_colors[linkData.type] : this.default_connection_color;
State.linkDataIndex++;
}
if (State.linkDataIndex > 0) {
const linksToRender = State.linkDataCache.slice(0, State.linkDataIndex);
this.renderChristmasLights(ctx, linksToRender, phase);
}
ctx.restore();
const frameTime = performance.now() - startTime;
State.performanceMode = PerformanceMonitor.addFrameTime(frameTime);
} catch (error) {
console.error("Error in drawConnections:", error);
origDrawConnections.call(this, ctx);
}
};
let linkAnimLoopId = null;
function startLinkLoop() {
if (linkAnimLoopId) return;
const loop = () => {
const lightsOn = getSetting("ChristmasTheme.ChristmasEffects.LightSwitch") !== 0;
if (isPageVisible && lightsOn && app.canvas) {
app.canvas.setDirty(true, true);
}
linkAnimLoopId = requestAnimationFrame(loop);
};
linkAnimLoopId = requestAnimationFrame(loop);
}
startLinkLoop();
};
api.addEventListener("execution_start", () => {
if (getSetting("ChristmasTheme.PauseDuringRender")) {
State.isRendering = true;
}
});
api.addEventListener("execution_end", () => {
State.isRendering = false;
if (app.canvas) {
app.canvas.setDirty(true, true);
}
});
installChristmasLights();
}
});
//# sourceMappingURL=link_animations.js.map