Files
PozzettiAndrea 4888e4886e Vendor viewer JS, drop comfy-3d-viewers
Vendor the viewer JavaScript into javascript/ and drop the
comfy-3d-viewers dependency. The viewers were previously copied into web/ at
every startup by prestartup_script.py, which made the served frontend depend on
a second package resolving at runtime.

Bundles are vendored as .mjs so ComfyUI's **/*.js auto-import glob skips them:
they are iframe-only and must never enter the shared main browser realm. Only
the widget entry points stay .js.

Also replace comfy_env.copy_files with a local helper. Its two extra features
(frame-derived source dir, silent no-op on a missing source) were unused here,
and the asset seeding now resolves the input directory through
folder_paths.get_input_directory() so it follows ComfyUI Desktop and
--input-directory instead of writing into the code tree.

comfy-test.toml moves to the current schema: [test.workflows] cuda (there is no
"gpu" key -- the accelerator is named by backend, and the old spelling parsed to
an empty list, so CUDA runners silently ran the full unfiltered set), an
explicit [test.platforms] platforms allowlist (per-platform booleans are no
longer supported), and the javascript level.

Namespace every registerExtension name under sam3. and inline the spinner
styles: the stylesheet used to be injected into document.head, which is shared
state every other pack inherits and nobody can clean up. It is now inline on the
element with the rotation driven by the Web Animations API.
2026-08-22 19:44:03 +02:00

645 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SAM3 Multi-Region Collector Widget
* Combines point and box collection with multiple prompt regions
* Version: 2025-01-20-v1-MULTIREGION
*/
import { app } from "../../scripts/app.js";
console.log("[SAM3] ===== MULTI-REGION COLLECTOR VERSION 1 =====");
// Color palette for different prompt regions (max 8)
const PROMPT_COLORS = [
{ name: "cyan", primary: "#00FFFF", dim: "#006666" },
{ name: "yellow", primary: "#FFFF00", dim: "#666600" },
{ name: "magenta", primary: "#FF00FF", dim: "#660066" },
{ name: "lime", primary: "#00FF00", dim: "#006600" },
{ name: "orange", primary: "#FF8000", dim: "#663300" },
{ name: "pink", primary: "#FF69B4", dim: "#662944" },
{ name: "blue", primary: "#4169E1", dim: "#1a2a5c" },
{ name: "teal", primary: "#20B2AA", dim: "#0d4744" },
];
const MAX_PROMPTS = PROMPT_COLORS.length;
// Helper function to hide widgets
function hideWidgetForGood(node, widget, suffix = '') {
if (!widget) return;
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.computeSize = () => [0, -4];
widget.type = "converted-widget" + suffix;
widget.hidden = true;
if (widget.element) {
widget.element.style.display = "none";
widget.element.style.visibility = "hidden";
}
}
app.registerExtension({
name: "sam3.multiregioncollector",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "SAM3MultiRegionCollector") return;
console.log("[SAM3] Registering SAM3MultiRegionCollector node");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// Create main container
const container = document.createElement("div");
container.style.cssText = "position: relative; width: 100%; background: #222; overflow: hidden; box-sizing: border-box; display: flex; flex-direction: column;";
// Create info bar (top)
const infoBar = document.createElement("div");
infoBar.style.cssText = "position: absolute; top: 5px; left: 5px; right: 5px; z-index: 10; display: flex; justify-content: space-between; align-items: center;";
container.appendChild(infoBar);
// Counter display
const counter = document.createElement("div");
counter.style.cssText = "padding: 5px 10px; background: rgba(0,0,0,0.7); color: #fff; border-radius: 3px; font-size: 12px; font-family: monospace;";
counter.textContent = "Prompt 1: 0 pts, 0 boxes";
infoBar.appendChild(counter);
// Button container
const buttonContainer = document.createElement("div");
buttonContainer.style.cssText = "display: flex; gap: 5px;";
infoBar.appendChild(buttonContainer);
// Clear Prompt button
const clearPromptBtn = document.createElement("button");
clearPromptBtn.textContent = "Clear Prompt";
clearPromptBtn.style.cssText = "padding: 5px 10px; background: #a50; color: #fff; border: 1px solid #830; border-radius: 3px; cursor: pointer; font-size: 11px;";
clearPromptBtn.onmouseover = () => clearPromptBtn.style.background = "#c60";
clearPromptBtn.onmouseout = () => clearPromptBtn.style.background = "#a50";
buttonContainer.appendChild(clearPromptBtn);
// Clear All button
const clearAllBtn = document.createElement("button");
clearAllBtn.textContent = "Clear All";
clearAllBtn.style.cssText = "padding: 5px 10px; background: #d44; color: #fff; border: 1px solid #a22; border-radius: 3px; cursor: pointer; font-size: 11px;";
clearAllBtn.onmouseover = () => clearAllBtn.style.background = "#e55";
clearAllBtn.onmouseout = () => clearAllBtn.style.background = "#d44";
buttonContainer.appendChild(clearAllBtn);
// Canvas wrapper (for centering)
const canvasWrapper = document.createElement("div");
canvasWrapper.style.cssText = "flex: 1; display: flex; align-items: center; justify-content: center; min-height: 200px;";
container.appendChild(canvasWrapper);
// Canvas
const canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 512;
canvas.style.cssText = "display: block; max-width: 100%; max-height: 100%; object-fit: contain; cursor: crosshair;";
canvasWrapper.appendChild(canvas);
const ctx = canvas.getContext("2d");
// Tab bar (bottom)
const tabBar = document.createElement("div");
tabBar.style.cssText = "display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; background: #1a1a1a; border-top: 1px solid #333;";
container.appendChild(tabBar);
// Store state
this.canvasWidget = {
canvas: canvas,
ctx: ctx,
container: container,
canvasWrapper: canvasWrapper,
image: null,
// Multi-prompt state
prompts: [{
positive_points: [],
negative_points: [],
positive_boxes: [],
negative_boxes: []
}],
activePromptIndex: 0,
// Drawing state
currentBox: null,
isDrawingBox: false,
hoveredItem: null,
// UI elements
tabBar: tabBar,
counter: counter
};
// Add as DOM widget
const widget = this.addDOMWidget("canvas", "customCanvas", container);
this.canvasWidget.domWidget = widget;
widget.computeSize = (width) => {
const nodeHeight = this.size ? this.size[1] : 520;
const widgetHeight = Math.max(250, nodeHeight - 80);
return [width, widgetHeight];
};
// Build initial tab bar
this.rebuildTabBar();
// Clear Prompt button handler
clearPromptBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.clearActivePrompt();
});
// Clear All button handler
clearAllBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.clearAllPrompts();
});
// Hide storage widget
const storeWidget = this.widgets.find(w => w.name === "multi_prompts_store");
if (storeWidget) {
storeWidget.value = storeWidget.value || "[]";
this._hiddenWidgets = { multi_prompts_store: storeWidget };
hideWidgetForGood(this, storeWidget);
}
// Override onDrawForeground to hide converted widgets
const originalDrawForeground = this.onDrawForeground;
this.onDrawForeground = function(ctx) {
const hiddenWidgets = this.widgets.filter(w => w.type?.includes("converted-widget"));
const originalTypes = hiddenWidgets.map(w => w.type);
hiddenWidgets.forEach(w => w.type = null);
if (originalDrawForeground) originalDrawForeground.apply(this, arguments);
hiddenWidgets.forEach((w, i) => w.type = originalTypes[i]);
// Update container height
const containerHeight = Math.max(250, this.size[1] - 80);
if (container.style.height !== containerHeight + "px") {
container.style.height = containerHeight + "px";
}
};
// Mouse event handlers
canvas.addEventListener("mousedown", (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
const activePrompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
const isNegative = e.button === 2;
// Shift key = box mode
if (e.shiftKey) {
this.canvasWidget.currentBox = { x1: x, y1: y, x2: x, y2: y, isNegative };
this.canvasWidget.isDrawingBox = true;
return;
}
// Add point (left-click = positive, right-click = negative)
const pointList = isNegative ? activePrompt.negative_points : activePrompt.positive_points;
pointList.push({ x, y });
this.updateStorage();
this.redrawCanvas();
});
canvas.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
if (this.canvasWidget.isDrawingBox && this.canvasWidget.currentBox) {
this.canvasWidget.currentBox.x2 = x;
this.canvasWidget.currentBox.y2 = y;
this.redrawCanvas();
} else {
// Check hover
const hovered = this.findItemAt(x, y);
if (hovered !== this.canvasWidget.hoveredItem) {
this.canvasWidget.hoveredItem = hovered;
this.redrawCanvas();
}
}
});
canvas.addEventListener("mouseup", (e) => {
if (this.canvasWidget.isDrawingBox && this.canvasWidget.currentBox) {
const box = this.canvasWidget.currentBox;
const width = Math.abs(box.x2 - box.x1);
const height = Math.abs(box.y2 - box.y1);
if (width > 5 && height > 5) {
const normalizedBox = {
x1: Math.min(box.x1, box.x2),
y1: Math.min(box.y1, box.y2),
x2: Math.max(box.x1, box.x2),
y2: Math.max(box.y1, box.y2)
};
const activePrompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
const boxList = box.isNegative ? activePrompt.negative_boxes : activePrompt.positive_boxes;
boxList.push(normalizedBox);
this.updateStorage();
}
this.canvasWidget.currentBox = null;
this.canvasWidget.isDrawingBox = false;
this.redrawCanvas();
}
});
canvas.addEventListener("contextmenu", (e) => {
e.preventDefault();
canvas.dispatchEvent(new MouseEvent('mousedown', {
button: 2,
clientX: e.clientX,
clientY: e.clientY,
shiftKey: e.shiftKey
}));
});
// Handle image loading
this.onExecuted = (message) => {
if (message.bg_image && message.bg_image[0]) {
const img = new Image();
img.onload = () => {
this.canvasWidget.image = img;
canvas.width = img.width;
canvas.height = img.height;
this.redrawCanvas();
};
img.src = "data:image/jpeg;base64," + message.bg_image[0];
}
};
// Handle node resize
const originalOnResize = this.onResize;
this.onResize = function(size) {
if (originalOnResize) originalOnResize.apply(this, arguments);
const containerHeight = Math.max(250, size[1] - 80);
container.style.height = containerHeight + "px";
};
// Initial draw
this.redrawCanvas();
// Set initial size
this.setSize([400, 520]);
container.style.height = "440px";
return result;
};
// Restore state when workflow is loaded
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
onConfigure?.apply(this, arguments);
// Restore prompts from stored widget value
const storeWidget = this._hiddenWidgets?.multi_prompts_store;
if (storeWidget && storeWidget.value) {
try {
const stored = JSON.parse(storeWidget.value);
if (Array.isArray(stored) && stored.length > 0) {
this.canvasWidget.prompts = stored;
this.canvasWidget.activePromptIndex = 0;
this.rebuildTabBar();
this.redrawCanvas();
console.log("[SAM3] Restored", stored.length, "prompts from saved state");
}
} catch (e) {
console.log("[SAM3] Failed to restore prompts:", e);
}
}
};
// Rebuild tab bar
nodeType.prototype.rebuildTabBar = function() {
const tabBar = this.canvasWidget.tabBar;
tabBar.innerHTML = "";
// Create tabs for each prompt
this.canvasWidget.prompts.forEach((prompt, idx) => {
const tab = document.createElement("div");
const color = PROMPT_COLORS[idx % PROMPT_COLORS.length];
const isActive = idx === this.canvasWidget.activePromptIndex;
tab.style.cssText = `
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; background: ${isActive ? '#333' : '#2a2a2a'};
border: 1px solid ${isActive ? color.primary : '#444'};
border-radius: 4px; cursor: pointer; font-size: 11px;
color: ${isActive ? '#fff' : '#aaa'};
`;
tab.dataset.promptIndex = idx;
// Color indicator
const colorDot = document.createElement("span");
colorDot.style.cssText = `width: 10px; height: 10px; border-radius: 2px; background: ${color.primary};`;
tab.appendChild(colorDot);
// Label
const label = document.createElement("span");
label.textContent = `Prompt ${idx + 1}`;
tab.appendChild(label);
// Delete button (only if more than 1 prompt)
if (this.canvasWidget.prompts.length > 1) {
const deleteBtn = document.createElement("span");
deleteBtn.textContent = "×";
deleteBtn.style.cssText = "color: #888; cursor: pointer; font-size: 14px; padding: 0 2px; margin-left: 2px;";
deleteBtn.onmouseover = () => deleteBtn.style.color = "#f00";
deleteBtn.onmouseout = () => deleteBtn.style.color = "#888";
deleteBtn.onclick = (e) => {
e.stopPropagation();
this.deletePrompt(idx);
};
tab.appendChild(deleteBtn);
}
// Tab click handler
tab.onclick = () => this.setActivePrompt(idx);
tab.onmouseover = () => { if (!isActive) tab.style.background = '#3a3a3a'; };
tab.onmouseout = () => { if (!isActive) tab.style.background = '#2a2a2a'; };
tabBar.appendChild(tab);
});
// Add prompt button
if (this.canvasWidget.prompts.length < MAX_PROMPTS) {
const addBtn = document.createElement("button");
addBtn.textContent = "+";
addBtn.style.cssText = `
padding: 4px 12px; background: #2a5a2a; border: 1px solid #3a7a3a;
border-radius: 4px; color: #8f8; cursor: pointer; font-size: 14px; font-weight: bold;
`;
addBtn.onmouseover = () => addBtn.style.background = "#3a6a3a";
addBtn.onmouseout = () => addBtn.style.background = "#2a5a2a";
addBtn.onclick = () => this.addNewPrompt();
tabBar.appendChild(addBtn);
}
this.updateCounter();
};
// Set active prompt
nodeType.prototype.setActivePrompt = function(index) {
this.canvasWidget.activePromptIndex = index;
this.rebuildTabBar();
this.redrawCanvas();
};
// Add new prompt
nodeType.prototype.addNewPrompt = function() {
if (this.canvasWidget.prompts.length >= MAX_PROMPTS) return;
this.canvasWidget.prompts.push({
positive_points: [],
negative_points: [],
positive_boxes: [],
negative_boxes: []
});
this.canvasWidget.activePromptIndex = this.canvasWidget.prompts.length - 1;
this.rebuildTabBar();
this.updateStorage();
this.redrawCanvas();
};
// Delete prompt
nodeType.prototype.deletePrompt = function(index) {
if (this.canvasWidget.prompts.length <= 1) {
this.clearActivePrompt();
return;
}
this.canvasWidget.prompts.splice(index, 1);
if (this.canvasWidget.activePromptIndex >= this.canvasWidget.prompts.length) {
this.canvasWidget.activePromptIndex = this.canvasWidget.prompts.length - 1;
}
this.rebuildTabBar();
this.updateStorage();
this.redrawCanvas();
};
// Clear active prompt
nodeType.prototype.clearActivePrompt = function() {
const prompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
prompt.positive_points = [];
prompt.negative_points = [];
prompt.positive_boxes = [];
prompt.negative_boxes = [];
this.updateStorage();
this.redrawCanvas();
};
// Clear all prompts
nodeType.prototype.clearAllPrompts = function() {
this.canvasWidget.prompts = [{
positive_points: [],
negative_points: [],
positive_boxes: [],
negative_boxes: []
}];
this.canvasWidget.activePromptIndex = 0;
this.rebuildTabBar();
this.updateStorage();
this.redrawCanvas();
};
// Find item at coordinates (only checks active prompt)
nodeType.prototype.findItemAt = function(x, y) {
const threshold = 10;
const pIdx = this.canvasWidget.activePromptIndex;
const prompt = this.canvasWidget.prompts[pIdx];
// Check points
for (let i = 0; i < prompt.positive_points.length; i++) {
const pt = prompt.positive_points[i];
if (Math.abs(pt.x - x) < threshold && Math.abs(pt.y - y) < threshold) {
return { type: "point", index: i, promptIndex: pIdx, isNegative: false };
}
}
for (let i = 0; i < prompt.negative_points.length; i++) {
const pt = prompt.negative_points[i];
if (Math.abs(pt.x - x) < threshold && Math.abs(pt.y - y) < threshold) {
return { type: "point", index: i, promptIndex: pIdx, isNegative: true };
}
}
// Check boxes
for (let i = 0; i < prompt.positive_boxes.length; i++) {
const box = prompt.positive_boxes[i];
if (x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2) {
return { type: "box", index: i, promptIndex: pIdx, isNegative: false };
}
}
for (let i = 0; i < prompt.negative_boxes.length; i++) {
const box = prompt.negative_boxes[i];
if (x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2) {
return { type: "box", index: i, promptIndex: pIdx, isNegative: true };
}
}
return null;
};
// Update storage widget
nodeType.prototype.updateStorage = function() {
const widget = this._hiddenWidgets?.multi_prompts_store;
if (widget) {
widget.value = JSON.stringify(this.canvasWidget.prompts);
}
this.updateCounter();
};
// Update counter display
nodeType.prototype.updateCounter = function() {
const prompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
const pts = prompt.positive_points.length + prompt.negative_points.length;
const boxes = prompt.positive_boxes.length + prompt.negative_boxes.length;
this.canvasWidget.counter.textContent = `Prompt ${this.canvasWidget.activePromptIndex + 1}: ${pts} pts, ${boxes} boxes`;
};
// Redraw canvas
nodeType.prototype.redrawCanvas = function() {
const { canvas, ctx, image, prompts, activePromptIndex, currentBox, hoveredItem } = this.canvasWidget;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw image or placeholder
if (image) {
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
} else {
ctx.fillStyle = "#333";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#666";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Click: Positive point | Right-click: Negative point", canvas.width / 2, canvas.height / 2 - 10);
ctx.fillText("Shift+Drag: Box | Shift+Right-drag: Negative box", canvas.width / 2, canvas.height / 2 + 15);
}
// Only draw the active prompt
const prompt = prompts[activePromptIndex];
const color = PROMPT_COLORS[activePromptIndex % PROMPT_COLORS.length];
// Draw boxes first
this.drawBoxes(ctx, prompt.positive_boxes, color.primary, 1.0, false, activePromptIndex, hoveredItem);
this.drawBoxes(ctx, prompt.negative_boxes, color.primary, 1.0, true, activePromptIndex, hoveredItem);
// Draw points
this.drawPoints(ctx, prompt.positive_points, color.primary, 1.0, false, activePromptIndex, hoveredItem);
this.drawPoints(ctx, prompt.negative_points, color.primary, 1.0, true, activePromptIndex, hoveredItem);
// Draw current box being drawn
if (currentBox) {
const color = PROMPT_COLORS[activePromptIndex % PROMPT_COLORS.length];
ctx.setLineDash([5, 5]);
ctx.strokeStyle = currentBox.isNegative ? "#f80" : color.primary;
ctx.lineWidth = 2;
const w = currentBox.x2 - currentBox.x1;
const h = currentBox.y2 - currentBox.y1;
ctx.strokeRect(currentBox.x1, currentBox.y1, w, h);
ctx.setLineDash([]);
ctx.fillStyle = currentBox.isNegative ? "rgba(255,128,0,0.1)" : this.colorWithAlpha(color.primary, 0.1);
ctx.fillRect(currentBox.x1, currentBox.y1, w, h);
}
// Draw image dimensions
if (image) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(5, canvas.height - 25, 150, 20);
ctx.fillStyle = "#0f0";
ctx.font = "12px monospace";
ctx.textAlign = "left";
ctx.fillText(`Image: ${canvas.width}x${canvas.height}`, 10, canvas.height - 10);
}
};
// Draw points
nodeType.prototype.drawPoints = function(ctx, points, color, alpha, isNegative, promptIndex, hoveredItem) {
// Scale point size relative to image height (1080p = baseline)
const canvas = this.canvasWidget.canvas;
const scaleFactor = Math.max(0.5, canvas.height / 1080);
const baseRadius = 6 * scaleFactor;
const hoverRadius = 8 * scaleFactor;
for (let i = 0; i < points.length; i++) {
const pt = points[i];
const isHovered = hoveredItem?.type === "point" &&
hoveredItem?.promptIndex === promptIndex &&
hoveredItem?.index === i &&
hoveredItem?.isNegative === isNegative;
const radius = isHovered ? hoverRadius : baseRadius;
ctx.beginPath();
ctx.arc(pt.x, pt.y, radius, 0, 2 * Math.PI);
// Fill
if (isNegative) {
ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 0.8})`;
} else {
ctx.fillStyle = this.colorWithAlpha(color, alpha * 0.8);
}
ctx.fill();
// Stroke
ctx.strokeStyle = isHovered ? "#fff" : this.colorWithAlpha(color, alpha);
ctx.lineWidth = (isHovered ? 3 : 2) * scaleFactor;
ctx.stroke();
// X marker for negative
if (isNegative) {
const xSize = 3 * scaleFactor;
ctx.strokeStyle = "#fff";
ctx.lineWidth = 2 * scaleFactor;
ctx.beginPath();
ctx.moveTo(pt.x - xSize, pt.y - xSize);
ctx.lineTo(pt.x + xSize, pt.y + xSize);
ctx.moveTo(pt.x + xSize, pt.y - xSize);
ctx.lineTo(pt.x - xSize, pt.y + xSize);
ctx.stroke();
}
}
};
// Draw boxes
nodeType.prototype.drawBoxes = function(ctx, boxes, color, alpha, isNegative, promptIndex, hoveredItem) {
for (let i = 0; i < boxes.length; i++) {
const box = boxes[i];
const w = box.x2 - box.x1;
const h = box.y2 - box.y1;
const isHovered = hoveredItem?.type === "box" &&
hoveredItem?.promptIndex === promptIndex &&
hoveredItem?.index === i &&
hoveredItem?.isNegative === isNegative;
// Fill
if (isNegative) {
ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 0.15})`;
} else {
ctx.fillStyle = this.colorWithAlpha(color, alpha * 0.15);
}
ctx.fillRect(box.x1, box.y1, w, h);
// Stroke
ctx.strokeStyle = isHovered ? "#fff" : (isNegative ? `rgba(255,0,0,${alpha})` : this.colorWithAlpha(color, alpha));
ctx.lineWidth = isHovered ? 3 : 2;
if (isNegative) {
ctx.setLineDash([4, 4]);
}
ctx.strokeRect(box.x1, box.y1, w, h);
ctx.setLineDash([]);
}
};
// Helper: color with alpha
nodeType.prototype.colorWithAlpha = function(hexColor, alpha) {
const r = parseInt(hexColor.slice(1, 3), 16);
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
}
});