Files
2026-01-28 19:33:35 +13:00

228 lines
9.1 KiB
JavaScript

/**
* Curve list component creation and management
*/
/**
* Create the curve list panel
* @param {Object} state - Editor state
* @param {Object} callbacks - Callback functions
* @param {Function} callbacks.getActiveShape - Get active shape
* @param {Function} callbacks.setFrame - Set current frame
* @param {Function} callbacks.updateNodeInput - Update node input
* @param {Function} callbacks.redraw - Redraw canvas
* @returns {Object} Curve list elements and update function
*/
export function createCurveList(state, callbacks) {
const curveListContainer = document.createElement("div");
curveListContainer.style.display = "flex";
curveListContainer.style.flexDirection = "column";
curveListContainer.style.gap = "4px";
curveListContainer.style.height = "120px"; // Fixed height to fit ~3 shapes
curveListContainer.style.minHeight = "120px"; // Prevent shrinking
curveListContainer.style.maxHeight = "120px"; // Prevent growing into canvas space
curveListContainer.style.overflow = "auto";
curveListContainer.style.flexShrink = "0"; // Don't shrink when container is tight
// Title
const curveListHeader = document.createElement("div");
curveListHeader.textContent = "Curves";
curveListHeader.style.fontSize = "13px";
curveListHeader.style.fontWeight = "600";
curveListHeader.style.color = "var(--fg-color)";
// Count
const curveListCount = document.createElement("div");
curveListCount.style.fontSize = "12px";
curveListCount.style.opacity = "0.7";
curveListCount.style.marginBottom = "4px";
const curveList = document.createElement("ul");
curveList.style.listStyle = "none";
curveList.style.padding = "0";
curveList.style.margin = "0";
curveListContainer.appendChild(curveListHeader);
curveListContainer.appendChild(curveListCount);
curveListContainer.appendChild(curveList);
function updateCurveList() {
const shape = callbacks.getActiveShape();
const frameKey = String(state.currentFrame);
const currentCurves = shape.frames[frameKey] || [];
const curveMap = new Map();
Object.entries(shape.frames || {}).forEach(([frame, curves]) => {
(curves || []).forEach(curve => {
if (!curve || !curve.id) return;
if (!curveMap.has(curve.id)) {
curveMap.set(curve.id, new Set());
}
curveMap.get(curve.id).add(frame);
});
});
curveList.innerHTML = "";
const ids = Array.from(curveMap.keys());
curveListCount.textContent = `${ids.length} curve(s)`;
// Create a map of curve IDs to display names (Shape 1, Shape 2, etc.)
const curveDisplayNames = new Map();
ids.forEach((id, index) => {
curveDisplayNames.set(id, `Shape ${index + 1}`);
});
ids.forEach(id => {
const item = document.createElement("li");
item.style.display = "flex";
item.style.justifyContent = "space-between";
item.style.alignItems = "center";
item.style.padding = "2px 0";
item.style.gap = "4px";
const label = document.createElement("span");
label.textContent = curveDisplayNames.get(id);
label.style.cursor = "pointer";
const frames = curveMap.get(id);
const frameNumbers = frames ? Array.from(frames).map(f => parseInt(f)).sort((a, b) => a - b) : [];
const hasKeyOnCurrentFrame = frames && frames.has(frameKey);
const dot = document.createElement("span");
dot.style.display = "inline-block";
dot.style.width = "6px";
dot.style.height = "6px";
dot.style.borderRadius = "50%";
dot.style.marginRight = "6px";
dot.style.backgroundColor = hasKeyOnCurrentFrame ? "#ff4d4d" : "transparent";
const left = document.createElement("div");
left.style.display = "flex";
left.style.alignItems = "center";
left.appendChild(dot);
left.appendChild(label);
// Keyframe navigation buttons
const navContainer = document.createElement("div");
navContainer.style.display = "flex";
navContainer.style.gap = "2px";
navContainer.style.alignItems = "center";
// Previous keyframe button
const prevKeyBtn = document.createElement("button");
prevKeyBtn.textContent = "◀";
prevKeyBtn.className = "comfy-button";
prevKeyBtn.type = "button";
prevKeyBtn.style.padding = "0 4px";
prevKeyBtn.style.minWidth = "20px";
prevKeyBtn.style.fontSize = "10px";
prevKeyBtn.title = "Previous keyframe";
prevKeyBtn.onclick = (e) => {
e.stopPropagation();
const currentFrameNum = state.currentFrame;
const prevFrames = frameNumbers.filter(f => f < currentFrameNum);
if (prevFrames.length > 0) {
const prevFrame = prevFrames[prevFrames.length - 1];
callbacks.setFrame(prevFrame);
}
};
// Next keyframe button
const nextKeyBtn = document.createElement("button");
nextKeyBtn.textContent = "▶";
nextKeyBtn.className = "comfy-button";
nextKeyBtn.type = "button";
nextKeyBtn.style.padding = "0 4px";
nextKeyBtn.style.minWidth = "20px";
nextKeyBtn.style.fontSize = "10px";
nextKeyBtn.title = "Next keyframe";
nextKeyBtn.onclick = (e) => {
e.stopPropagation();
const currentFrameNum = state.currentFrame;
const nextFrames = frameNumbers.filter(f => f > currentFrameNum);
if (nextFrames.length > 0) {
const nextFrame = nextFrames[0];
callbacks.setFrame(nextFrame);
}
};
// Delete keyframe button (only for current frame)
const deleteKeyBtn = document.createElement("button");
deleteKeyBtn.textContent = "🔑✕";
deleteKeyBtn.className = "comfy-button";
deleteKeyBtn.type = "button";
deleteKeyBtn.style.padding = "0 4px";
deleteKeyBtn.style.minWidth = "28px";
deleteKeyBtn.style.fontSize = "10px";
deleteKeyBtn.title = "Delete keyframe for this curve";
deleteKeyBtn.disabled = !hasKeyOnCurrentFrame;
deleteKeyBtn.style.opacity = hasKeyOnCurrentFrame ? "1" : "0.4";
deleteKeyBtn.onclick = (e) => {
e.stopPropagation();
if (!hasKeyOnCurrentFrame) return;
// Remove this curve from current frame only
if (shape.frames[frameKey]) {
shape.frames[frameKey] = shape.frames[frameKey].filter(c => c.id !== id);
if (shape.frames[frameKey].length === 0) {
delete shape.frames[frameKey];
}
}
state.selectedCurves.delete(id);
state.selectedPoints.clear();
callbacks.updateNodeInput();
callbacks.redraw();
};
// Delete curve entirely button
const deleteCurveBtn = document.createElement("button");
deleteCurveBtn.textContent = "✕";
deleteCurveBtn.className = "comfy-button";
deleteCurveBtn.type = "button";
deleteCurveBtn.style.padding = "0 6px";
deleteCurveBtn.style.minWidth = "24px";
deleteCurveBtn.title = "Delete curve from all frames";
deleteCurveBtn.onclick = (e) => {
e.stopPropagation();
Object.keys(shape.frames || {}).forEach(frame => {
shape.frames[frame] = (shape.frames[frame] || []).filter(c => c.id !== id);
if (shape.frames[frame].length === 0) {
delete shape.frames[frame];
}
});
state.selectedCurves.delete(id);
state.selectedPoints.clear();
callbacks.updateNodeInput();
callbacks.redraw();
};
navContainer.appendChild(prevKeyBtn);
navContainer.appendChild(nextKeyBtn);
navContainer.appendChild(deleteKeyBtn);
navContainer.appendChild(deleteCurveBtn);
if (state.selectedCurves.has(id)) {
label.style.color = "#4a9eff";
}
label.onclick = (e) => {
e.stopPropagation();
const curve = currentCurves.find(c => c.id === id);
state.selectedCurves.clear();
state.selectedPoints.clear();
if (curve) {
state.selectedCurves.add(id);
}
callbacks.redraw();
};
item.appendChild(left);
item.appendChild(navContainer);
curveList.appendChild(item);
});
}
return {
curveListContainer,
updateCurveList
};
}