/** * 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 }; }