228 lines
9.1 KiB
JavaScript
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
|
|
};
|
|
}
|