/** * Toolbar component creation and management */ import { uploadImageFile } from "../utils/fileUpload.js"; /** * Create the toolbar with all controls * @param {Object} state - Editor state * @param {Object} callbacks - Callback functions * @param {Function} callbacks.updateNodeInput - Update node input * @param {Function} callbacks.setFrame - Set current frame * @param {Function} callbacks.redraw - Redraw canvas * @param {Function} callbacks.setMode - Set editor mode * @param {Function} callbacks.removeDocumentListeners - Remove document listeners * @param {Function} callbacks.updateModeButtons - Update mode button styles * @param {Function} callbacks.getFrameImageForIndex - Get frame image * @param {Function} callbacks.resizeCanvasToImage - Resize canvas to image * @param {HTMLCanvasElement} canvas - Canvas element * @param {Object} node - ComfyUI node * @returns {Object} Toolbar elements and controls */ export function createToolbar(state, callbacks, canvas, node) { // Section 1: Load Frames (full width at top) const loadFramesSection = document.createElement("div"); loadFramesSection.style.display = "flex"; loadFramesSection.style.width = "100%"; loadFramesSection.style.gap = "8px"; // Feather amount slider const featherLabel = document.createElement("span"); featherLabel.textContent = "Feather Amount:"; featherLabel.style.minWidth = "120px"; featherLabel.style.fontSize = "12px"; const featherSlider = document.createElement("input"); featherSlider.type = "range"; featherSlider.min = "0"; featherSlider.max = "100"; featherSlider.step = "0.1"; featherSlider.value = "0"; featherSlider.style.flex = "1 1 auto"; featherSlider.style.minWidth = "100px"; const featherValueLabel = document.createElement("span"); featherValueLabel.textContent = "0.0"; featherValueLabel.style.minWidth = "40px"; featherValueLabel.style.fontSize = "12px"; featherValueLabel.style.textAlign = "right"; featherSlider.oninput = (e) => { const value = Math.max(0, Math.min(100, parseFloat(e.target.value) || 0)); state.featherAmount = value; featherSlider.value = String(value); featherValueLabel.textContent = value.toFixed(1); callbacks.updateNodeInput(); }; const featherContainer = document.createElement("div"); featherContainer.style.display = "flex"; featherContainer.style.gap = "8px"; featherContainer.style.alignItems = "center"; featherContainer.style.flex = "1 1 auto"; featherContainer.style.minWidth = "200px"; featherContainer.appendChild(featherLabel); featherContainer.appendChild(featherSlider); featherContainer.appendChild(featherValueLabel); // Background color picker const bgColorLabel = document.createElement("span"); bgColorLabel.textContent = "Background color:"; bgColorLabel.style.minWidth = "90px"; bgColorLabel.style.fontSize = "12px"; const bgColorInput = document.createElement("input"); bgColorInput.type = "color"; bgColorInput.value = "#000000"; bgColorInput.style.width = "40px"; bgColorInput.style.height = "24px"; bgColorInput.style.cursor = "pointer"; bgColorInput.style.border = "1px solid var(--border-color)"; bgColorInput.style.borderRadius = "2px"; bgColorInput.onchange = (e) => { state.backgroundColor = e.target.value; callbacks.updateNodeInput(); }; const bgColorContainer = document.createElement("div"); bgColorContainer.style.display = "flex"; bgColorContainer.style.gap = "8px"; bgColorContainer.style.alignItems = "center"; bgColorContainer.appendChild(bgColorLabel); bgColorContainer.appendChild(bgColorInput); // Section 4: Settings - created early so we can append to it const settingsSection = document.createElement("div"); settingsSection.style.display = "flex"; settingsSection.style.flexDirection = "column"; settingsSection.style.gap = "6px"; settingsSection.style.width = "100%"; // Divider at top of settings section (separates from curve list) const settingsDivider = document.createElement("div"); settingsDivider.style.width = "100%"; settingsDivider.style.height = "1px"; settingsDivider.style.backgroundColor = "var(--border-color)"; settingsDivider.style.marginBottom = "4px"; // === Mask Visualization Settings === const maskVisualizationTitle = document.createElement("div"); maskVisualizationTitle.textContent = "Mask Visualization Settings"; maskVisualizationTitle.style.fontSize = "13px"; maskVisualizationTitle.style.fontWeight = "600"; maskVisualizationTitle.style.color = "var(--fg-color)"; // Mask Color picker const maskColorLabel = document.createElement("span"); maskColorLabel.textContent = "Mask Color:"; maskColorLabel.style.fontSize = "12px"; maskColorLabel.style.minWidth = "70px"; const maskColorInput = document.createElement("input"); maskColorInput.type = "color"; maskColorInput.value = "#ffffff"; maskColorInput.style.width = "40px"; maskColorInput.style.height = "24px"; maskColorInput.style.cursor = "pointer"; maskColorInput.style.border = "1px solid var(--border-color)"; maskColorInput.style.borderRadius = "2px"; maskColorInput.onchange = (e) => { state.maskColor = e.target.value; callbacks.updateNodeInput(); callbacks.redraw(); }; const maskColorContainer = document.createElement("div"); maskColorContainer.style.display = "flex"; maskColorContainer.style.gap = "4px"; maskColorContainer.style.alignItems = "center"; maskColorContainer.appendChild(maskColorLabel); maskColorContainer.appendChild(maskColorInput); // Mask Opacity slider const maskOpacityLabel = document.createElement("span"); maskOpacityLabel.textContent = "Mask Opacity:"; maskOpacityLabel.style.fontSize = "12px"; maskOpacityLabel.style.minWidth = "90px"; const maskOpacitySlider = document.createElement("input"); maskOpacitySlider.type = "range"; maskOpacitySlider.min = "0"; maskOpacitySlider.max = "100"; maskOpacitySlider.step = "1"; maskOpacitySlider.value = "10"; maskOpacitySlider.style.flex = "1 1 auto"; maskOpacitySlider.style.minWidth = "100px"; const maskOpacityValueLabel = document.createElement("span"); maskOpacityValueLabel.textContent = "10"; maskOpacityValueLabel.style.minWidth = "30px"; maskOpacityValueLabel.style.fontSize = "12px"; maskOpacityValueLabel.style.textAlign = "right"; maskOpacitySlider.oninput = (e) => { const value = Math.max(0, Math.min(100, parseInt(e.target.value) || 0)); state.maskOpacity = value; maskOpacitySlider.value = String(value); maskOpacityValueLabel.textContent = String(value); callbacks.updateNodeInput(); callbacks.redraw(); }; const maskOpacityContainer = document.createElement("div"); maskOpacityContainer.style.display = "flex"; maskOpacityContainer.style.gap = "8px"; maskOpacityContainer.style.alignItems = "center"; maskOpacityContainer.style.flex = "1 1 auto"; maskOpacityContainer.style.minWidth = "200px"; maskOpacityContainer.appendChild(maskOpacityLabel); maskOpacityContainer.appendChild(maskOpacitySlider); maskOpacityContainer.appendChild(maskOpacityValueLabel); // Mask controls row (color and opacity on same line) const maskControlsRow = document.createElement("div"); maskControlsRow.style.display = "flex"; maskControlsRow.style.gap = "16px"; maskControlsRow.style.alignItems = "center"; maskControlsRow.style.flexWrap = "wrap"; maskControlsRow.appendChild(maskColorContainer); maskControlsRow.appendChild(maskOpacityContainer); // Divider between Mask Visualization Settings and Image Output Settings const maskImageDivider = document.createElement("div"); maskImageDivider.style.width = "100%"; maskImageDivider.style.height = "1px"; maskImageDivider.style.backgroundColor = "var(--border-color)"; maskImageDivider.style.marginTop = "4px"; maskImageDivider.style.marginBottom = "4px"; // === Image Output Settings === const imageOutputTitle = document.createElement("div"); imageOutputTitle.textContent = "Image Output Settings"; imageOutputTitle.style.fontSize = "13px"; imageOutputTitle.style.fontWeight = "600"; imageOutputTitle.style.color = "var(--fg-color)"; // Divider before Output Frame Range const frameRangeDivider = document.createElement("div"); frameRangeDivider.style.width = "100%"; frameRangeDivider.style.height = "1px"; frameRangeDivider.style.backgroundColor = "var(--border-color)"; frameRangeDivider.style.marginTop = "4px"; // === Output Frame Range === const frameRangeTitle = document.createElement("div"); frameRangeTitle.textContent = "Output Frame Range"; frameRangeTitle.style.fontSize = "13px"; frameRangeTitle.style.fontWeight = "600"; frameRangeTitle.style.color = "var(--fg-color)"; frameRangeTitle.style.marginTop = "2px"; // Frame output controls - Start Frame const startFrameLabel = document.createElement("span"); startFrameLabel.textContent = "Start Frame:"; startFrameLabel.style.fontSize = "12px"; startFrameLabel.style.minWidth = "70px"; const startFrameInput = document.createElement("input"); startFrameInput.type = "number"; startFrameInput.min = "1"; startFrameInput.value = "1"; startFrameInput.style.width = "60px"; startFrameInput.style.height = "24px"; startFrameInput.style.padding = "2px 6px"; startFrameInput.style.border = "1px solid var(--border-color, #555)"; startFrameInput.style.borderRadius = "4px"; startFrameInput.style.backgroundColor = "var(--comfy-input-bg, #333)"; startFrameInput.style.color = "var(--input-text, #fff)"; const startFrameContainer = document.createElement("div"); startFrameContainer.style.display = "flex"; startFrameContainer.style.gap = "4px"; startFrameContainer.style.alignItems = "center"; startFrameContainer.appendChild(startFrameLabel); startFrameContainer.appendChild(startFrameInput); // Frame output controls - End Frame const endFrameLabel = document.createElement("span"); endFrameLabel.textContent = "End Frame:"; endFrameLabel.style.fontSize = "12px"; endFrameLabel.style.minWidth = "70px"; const endFrameInput = document.createElement("input"); endFrameInput.type = "number"; endFrameInput.min = "1"; endFrameInput.value = "1"; endFrameInput.style.width = "60px"; endFrameInput.style.height = "24px"; endFrameInput.style.padding = "2px 6px"; endFrameInput.style.border = "1px solid var(--border-color, #555)"; endFrameInput.style.borderRadius = "4px"; endFrameInput.style.backgroundColor = "var(--comfy-input-bg, #333)"; endFrameInput.style.color = "var(--input-text, #fff)"; const endFrameContainer = document.createElement("div"); endFrameContainer.style.display = "flex"; endFrameContainer.style.gap = "4px"; endFrameContainer.style.alignItems = "center"; endFrameContainer.appendChild(endFrameLabel); endFrameContainer.appendChild(endFrameInput); // Helper to calculate frame count from start/end const getFrameCount = () => { const startFrame = parseInt(startFrameInput.value) || 1; const endFrame = parseInt(endFrameInput.value) || 1; return Math.max(1, endFrame - startFrame + 1); }; // Update frame range (called when frames are loaded) const updateFrameRangeInfo = () => { // This function is kept for compatibility but does nothing visual now }; // Handle frame output setting changes const updateFrameWidgets = () => { const startFrame = parseInt(startFrameInput.value) || 1; const endFrame = parseInt(endFrameInput.value) || 1; const frameCount = Math.max(1, endFrame - startFrame + 1); // Update frame_index widget const frameIndexWidget = (node.widgets || []).find(w => w.name === "frame_index"); if (frameIndexWidget) { frameIndexWidget.value = startFrame; if (typeof frameIndexWidget.callback === "function") { frameIndexWidget.callback(startFrame); } } // Update frame_count widget const frameCountWidget = (node.widgets || []).find(w => w.name === "frame_count"); if (frameCountWidget) { frameCountWidget.value = frameCount; if (typeof frameCountWidget.callback === "function") { frameCountWidget.callback(frameCount); } } callbacks.updateNodeInput(); }; startFrameInput.onchange = (e) => { const value = Math.max(1, parseInt(e.target.value) || 1); startFrameInput.value = String(value); // Ensure end frame is not less than start frame const endValue = parseInt(endFrameInput.value) || 1; if (endValue < value) { endFrameInput.value = String(value); } updateFrameWidgets(); }; endFrameInput.onchange = (e) => { const startValue = parseInt(startFrameInput.value) || 1; const value = Math.max(startValue, parseInt(e.target.value) || 1); endFrameInput.value = String(value); updateFrameWidgets(); }; // Frame controls row const frameControlsRow = document.createElement("div"); frameControlsRow.style.display = "flex"; frameControlsRow.style.gap = "16px"; frameControlsRow.style.alignItems = "center"; frameControlsRow.style.flexWrap = "wrap"; frameControlsRow.appendChild(startFrameContainer); frameControlsRow.appendChild(endFrameContainer); // Update feather container to be full width row featherContainer.style.width = "100%"; // Update bg color container to be full width row bgColorContainer.style.width = "100%"; // Assemble settings section in order: // 1. Divider (separates from curve list above) // 2. Mask Visualization Settings title // 3. Mask Color and Opacity controls (same line) // 4. Divider (separates Mask Visualization from Image Output) // 5. Image Output Settings title // 6. Feather amount (own line) // 7. Background colour (own line) // 8. Divider // 9. Output Frame Range title // 10. Start/End frame controls settingsSection.appendChild(settingsDivider); settingsSection.appendChild(maskVisualizationTitle); settingsSection.appendChild(maskControlsRow); settingsSection.appendChild(maskImageDivider); settingsSection.appendChild(imageOutputTitle); settingsSection.appendChild(featherContainer); settingsSection.appendChild(bgColorContainer); settingsSection.appendChild(frameRangeDivider); settingsSection.appendChild(frameRangeTitle); settingsSection.appendChild(frameControlsRow); // Load frames button + file input const loadFramesBtn = document.createElement("button"); loadFramesBtn.textContent = "Load Frames"; loadFramesBtn.className = "comfy-button"; loadFramesBtn.type = "button"; loadFramesBtn.style.width = "100%"; const framesInput = document.createElement("input"); framesInput.type = "file"; framesInput.accept = "image/*"; framesInput.multiple = true; framesInput.style.display = "none"; loadFramesBtn.onclick = () => framesInput.click(); loadFramesSection.appendChild(loadFramesBtn); loadFramesSection.appendChild(framesInput); framesInput.onchange = async (e) => { const files = Array.from(e.target.files || []); if (!files.length) return; try { state.frameImages = []; state.frameImageCache.clear(); for (const file of files) { const data = await uploadImageFile(file); const filename = data.name; const subfolder = data.subfolder || ""; const url = `/view?filename=${filename}&type=input&subfolder=${subfolder}&t=${Date.now()}`; state.frameImages.push({ filename, subfolder, url }); } if (state.frameImages.length > 0) { const first = await callbacks.getFrameImageForIndex(0); if (first) { callbacks.resizeCanvasToImage(first); } } frameSlider.max = String(Math.max(1, state.frameImages.length)); // Auto-set frame_count widget to match loaded frames const widgets = node.widgets || []; const frameCountWidget = widgets.find(w => w.name === "frame_count"); if (frameCountWidget) { frameCountWidget.value = state.frameImages.length; if (typeof frameCountWidget.callback === "function") { frameCountWidget.callback(state.frameImages.length); } } // Update the frame output controls (Start Frame = 1, End Frame = total frames) startFrameInput.value = "1"; endFrameInput.value = String(state.frameImages.length); updateFrameLabel(); // Update frame counter display callbacks.setFrame(1); callbacks.redraw(); callbacks.updateNodeInput(); // Store frame_paths in metadata } catch (error) { console.error("[ComfyUI Roto] Failed to load frames:", error); } finally { framesInput.value = ""; } }; // Section 2: Tool Buttons (Draw, Edit, Add Point) const toolButtonsSection = document.createElement("div"); toolButtonsSection.style.display = "flex"; toolButtonsSection.style.gap = "8px"; toolButtonsSection.style.width = "100%"; // Mode buttons const drawBtn = document.createElement("button"); drawBtn.textContent = "Draw (B)"; drawBtn.className = "comfy-button"; drawBtn.type = "button"; drawBtn.style.flex = "1 1 auto"; drawBtn.onclick = () => { try { state.selectedCurves.clear(); state.selectedPoints.clear(); callbacks.setMode("draw"); } catch (error) { console.error("[ComfyUI Roto] Error switching to draw mode:", error); } }; const editBtn = document.createElement("button"); editBtn.textContent = "Edit (V)"; editBtn.className = "comfy-button"; editBtn.type = "button"; editBtn.style.flex = "1 1 auto"; editBtn.onclick = () => { try { callbacks.setMode("edit"); } catch (error) { console.error("[ComfyUI Roto] Error switching to edit mode:", error); } }; // Add Point button const addPointBtn = document.createElement("button"); addPointBtn.textContent = "Add Point"; addPointBtn.className = "comfy-button"; addPointBtn.type = "button"; addPointBtn.disabled = true; addPointBtn.style.flex = "1 1 auto"; function updateAddPointButton() { // Enable/disable based on curve selection addPointBtn.disabled = state.selectedCurves.size === 0; if (state.addPointMode) { addPointBtn.textContent = "Click Edge..."; addPointBtn.style.backgroundColor = "var(--comfy-menu-button-bg-selected)"; canvas.style.cursor = "crosshair"; } else { addPointBtn.textContent = "Add Point"; addPointBtn.style.backgroundColor = ""; canvas.style.cursor = state.mode === "draw" ? "crosshair" : "default"; } } addPointBtn.onclick = () => { if (state.selectedCurves.size === 0) return; // Toggle add point mode - user must click on edge to add point state.addPointMode = !state.addPointMode; updateAddPointButton(); }; // Toggle Smooth button const toggleSmoothBtn = document.createElement("button"); toggleSmoothBtn.textContent = "Soften Point"; toggleSmoothBtn.className = "comfy-button"; toggleSmoothBtn.type = "button"; toggleSmoothBtn.disabled = true; toggleSmoothBtn.style.flex = "1 1 auto"; function updateToggleSmoothButton() { // Button enabled when curves are selected const hasCurveSelection = state.selectedCurves.size > 0; const hasPointSelection = state.selectedPoints.size > 0; toggleSmoothBtn.disabled = !hasCurveSelection || !hasPointSelection; if (!hasCurveSelection) { toggleSmoothBtn.textContent = "Soften Point"; return; } if (!hasPointSelection) { toggleSmoothBtn.textContent = "Soften Point"; return; } // Check state of selected points within selected curves const shape = callbacks.getActiveShape ? callbacks.getActiveShape() : null; if (!shape) { toggleSmoothBtn.textContent = "Soften Point"; return; } const frameKey = String(state.currentFrame); const curves = shape.frames && shape.frames[frameKey] ? shape.frames[frameKey] : (callbacks.evaluateFrame ? callbacks.evaluateFrame(shape, state.currentFrame, { hold: true }) : []); let allHard = true; let allSmooth = true; state.selectedPoints.forEach(pointKey => { const [curveId, pointIndex] = pointKey.split(":"); // Only check points in selected curves if (!state.selectedCurves.has(curveId)) { return; } const curve = curves.find(c => c.id === curveId); if (!curve) { // Curve doesn't exist - skip (selection might be stale) return; } // Get pointTypes with validation let types; if (Array.isArray(curve.pointTypes) && curve.pointTypes.length === curve.points.length) { types = curve.pointTypes; } else { types = curve.points.map(() => "hard"); } const idx = parseInt(pointIndex); if (idx >= 0 && idx < curve.points.length && idx < types.length) { const type = types[idx]; // Validate type is valid if (type === "smooth") { allHard = false; } else if (type === "hard") { allSmooth = false; } // If invalid type, treat as "hard" (defensive) } }); if (allHard) { toggleSmoothBtn.textContent = "Soften Point"; } else if (allSmooth) { toggleSmoothBtn.textContent = "Sharpen Point"; } else { toggleSmoothBtn.textContent = "Soften Point"; // Mixed: default to softening } } toggleSmoothBtn.onclick = () => { if (state.selectedCurves.size === 0 || state.selectedPoints.size === 0) return; if (callbacks.togglePointSmoothness) { callbacks.togglePointSmoothness(); } }; toolButtonsSection.appendChild(drawBtn); toolButtonsSection.appendChild(editBtn); toolButtonsSection.appendChild(addPointBtn); toolButtonsSection.appendChild(toggleSmoothBtn); // Section 3: Timeline (slider + play button) const timelineSection = document.createElement("div"); timelineSection.style.display = "flex"; timelineSection.style.gap = "8px"; timelineSection.style.alignItems = "center"; timelineSection.style.width = "100%"; timelineSection.style.padding = "8px 0"; timelineSection.style.borderTop = "1px solid var(--border-color)"; timelineSection.style.borderBottom = "1px solid var(--border-color)"; timelineSection.style.margin = "4px 0"; // Frame counter display const frameCounter = document.createElement("div"); frameCounter.style.display = "flex"; frameCounter.style.alignItems = "center"; frameCounter.style.gap = "4px"; frameCounter.style.minWidth = "80px"; frameCounter.style.fontSize = "12px"; frameCounter.style.fontWeight = "500"; frameCounter.style.color = "var(--fg-color)"; const frameNumber = document.createElement("span"); frameNumber.textContent = "1"; frameNumber.style.minWidth = "30px"; frameNumber.style.textAlign = "right"; const frameSeparator = document.createElement("span"); frameSeparator.textContent = "/"; frameSeparator.style.opacity = "0.5"; const frameMax = document.createElement("span"); frameMax.textContent = "1"; frameMax.style.minWidth = "30px"; frameMax.style.textAlign = "left"; frameMax.style.opacity = "0.7"; frameCounter.appendChild(frameNumber); frameCounter.appendChild(frameSeparator); frameCounter.appendChild(frameMax); // Divider line const divider1 = document.createElement("div"); divider1.style.width = "1px"; divider1.style.height = "24px"; divider1.style.backgroundColor = "var(--border-color)"; divider1.style.opacity = "0.5"; const frameSlider = document.createElement("input"); frameSlider.type = "range"; frameSlider.min = "1"; frameSlider.max = "1"; frameSlider.step = "1"; frameSlider.value = "1"; frameSlider.style.flex = "1 1 auto"; frameSlider.style.height = "6px"; frameSlider.style.cursor = "pointer"; // Divider line const divider2 = document.createElement("div"); divider2.style.width = "1px"; divider2.style.height = "24px"; divider2.style.backgroundColor = "var(--border-color)"; divider2.style.opacity = "0.5"; const playBtn = document.createElement("button"); playBtn.textContent = "▶"; playBtn.className = "comfy-button"; playBtn.type = "button"; playBtn.style.minWidth = "40px"; playBtn.style.width = "40px"; playBtn.style.padding = "4px 8px"; playBtn.style.fontSize = "14px"; let playing = false; let lastTick = 0; const fps = 24; function updateFrameLabel() { frameNumber.textContent = String(state.currentFrame); const maxFrame = Math.max(1, Number(frameSlider.max || 1)); frameMax.textContent = String(maxFrame); } function safeWidgetCallback(widget, value) { if (widget && typeof widget.callback === "function") { try { widget.callback.call(widget, value, null, node); } catch (error) { console.warn("[ComfyUI Roto] Frame widget callback failed:", error); } } } function setFrame(frameIndex) { const maxFrame = Math.max(1, Number(frameSlider.max || 1)); const nextFrame = Math.max(1, Math.min(maxFrame, frameIndex)); if (nextFrame === state.currentFrame) return; state.currentFrame = nextFrame; frameSlider.value = String(nextFrame); updateFrameLabel(); callbacks.redraw(); const widgets = node.widgets || []; const frameWidget = widgets.find(w => w.name === "frame_index"); if (frameWidget) { frameWidget.value = nextFrame; if (!state.updatingFrame) { state.updatingFrame = true; safeWidgetCallback(frameWidget, nextFrame); state.updatingFrame = false; } } } function tick(ts) { if (!playing) return; if (!lastTick) lastTick = ts; const interval = 1000 / fps; if (ts - lastTick >= interval) { lastTick = ts; const maxFrame = Number(frameSlider.max || 1); const next = (state.currentFrame + 1) > maxFrame ? 1 : state.currentFrame + 1; setFrame(next); } requestAnimationFrame(tick); } playBtn.onclick = () => { playing = !playing; playBtn.textContent = playing ? "⏸" : "▶"; if (playing) { lastTick = 0; requestAnimationFrame(tick); } }; frameSlider.oninput = (e) => { const value = Number(e.target.value || 0); setFrame(value); }; // Assemble timeline section timelineSection.appendChild(frameCounter); timelineSection.appendChild(divider1); timelineSection.appendChild(frameSlider); timelineSection.appendChild(divider2); timelineSection.appendChild(playBtn); return { // Sections for layout loadFramesSection, toolButtonsSection, timelineSection, settingsSection, // Individual controls (for backward compatibility) featherSlider, featherValueLabel, bgColorInput, maskColorInput, maskOpacitySlider, maskOpacityValueLabel, frameSlider, addPointBtn, toggleSmoothBtn, drawBtn, editBtn, playBtn, // Frame output controls startFrameInput, endFrameInput, // Functions updateFrameLabel, updateAddPointButton, updateToggleSmoothButton, updateFrameRangeInfo, setFrame }; }