From b2d36e5d2a1bb889a88cf2b5cc9207fc9536f57d Mon Sep 17 00:00:00 2001 From: ryanontheinstide Date: Mon, 20 Jan 2025 11:27:24 -0500 Subject: [PATCH] drawable feature templates --- web/js/drawable_feature.js | 131 ++++++++++++++++------- web/js/feature_templates.js | 202 ++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+), 35 deletions(-) create mode 100644 web/js/feature_templates.js diff --git a/web/js/drawable_feature.js b/web/js/drawable_feature.js index 9c93f52..32ae987 100644 --- a/web/js/drawable_feature.js +++ b/web/js/drawable_feature.js @@ -1,5 +1,6 @@ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; +import { FeatureTemplates } from "./feature_templates.js"; //TODO: add handling for maximum frame count change @@ -257,13 +258,61 @@ app.registerExtension({ // console.error("Failed to restore points:", e); } + + // Add template dropdown and controls + const templateWidget = this.addWidget("combo", "Template", "selected_template", (v) => {}, { + values: [ + "none", + // Basic waveforms + "sine", + "square", + "triangle", + "sawtooth", + // Animation curves + "easeInOut", + "easeIn", + "easeOut", + // Special patterns + "bounce", + "pulse", + "random", + "smoothRandom", + "heartbeat", + "steps" + ], + value: "none" + }); + templateWidget.name = "template"; + + // Add cycles control + const cyclesWidget = this.addWidget("number", "Template Cycles", "template_cycles", function(v) { + return Math.max(0.1, Math.min(200, v)); + }, { + value: 1.0, + min: 0.1, + max: 200, + step: 0.1, + precision: 2 + }); + cyclesWidget.name = "template_cycles"; + cyclesWidget.value = 1.0; // Explicitly set initial value + + // Add load template button + const loadButton = this.addWidget("button", "Load Template", "load", () => { + const templateWidget = this.widgets.find(w => w.name === "template"); + if (templateWidget && templateWidget.value && templateWidget.value !== "none") { + this.loadTemplate(templateWidget.value); + } + }); + + // Add clear button widget this.addWidget("button", "Clear Graph", "clear", () => { this.points = []; this.updatePointsValue(); this.setDirtyCanvas(true, true); }); - + // Add handlers for min/max value changes const minValueWidget = this.widgets.find(w => w.name === "min_value"); const maxValueWidget = this.widgets.find(w => w.name === "max_value"); @@ -596,8 +645,17 @@ app.registerExtension({ const pointIndex = this.findNearPoint(x, y); if (pointIndex !== null) { - this.selectedPoint = pointIndex; - this.dragStartPos = [...pos]; + if (this.isDragging && this.selectedPoint !== null) { + // If already dragging and clicked, drop the point + this.isDragging = false; + this.selectedPoint = null; + this.dragStartPos = null; + } else { + // Start dragging + this.selectedPoint = pointIndex; + this.dragStartPos = [...pos]; + this.isDragging = true; + } } else { // Not on a point - add new point const [frame, value] = this.coordsToGraphValues(x, y); @@ -636,30 +694,22 @@ app.registerExtension({ return false; } - if (this.dragStartPos && this.selectedPoint !== null) { - // If we've moved more than 5 pixels, start dragging - const dragDist = Math.sqrt( - Math.pow(pos[0] - this.dragStartPos[0], 2) + - Math.pow(pos[1] - this.dragStartPos[1], 2) - ); - - if (dragDist > 5) { - this.isDragging = true; - const [frame, value] = this.coordsToGraphValues(x, y); - const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30; - const maxFrame = frameCount - 1; - // Keep strict < frameCount for dragging to prevent edge issues - if (frame >= 0 && frame <= maxFrame) { - // Check if there's already a point at the target frame (except selected point) - const existingIndex = this.points.findIndex((p, i) => - i !== this.selectedPoint && p[0] === frame); - - if (existingIndex === -1) { - this.points[this.selectedPoint] = [frame, value]; - this.points.sort((a, b) => a[0] - b[0]); - this.selectedPoint = this.points.findIndex(p => p[0] === frame); - this.updatePointsValue(); - } + if (this.isDragging && this.selectedPoint !== null) { + const [frame, value] = this.coordsToGraphValues(x, y); + const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30; + const maxFrame = frameCount - 1; + // Keep strict < frameCount for dragging to prevent edge issues + if (frame >= 0 && frame <= maxFrame) { + // Check if there's already a point at the target frame (except selected point) + const existingIndex = this.points.findIndex((p, i) => + i !== this.selectedPoint && p[0] === frame); + + if (existingIndex === -1) { + this.points[this.selectedPoint] = [frame, value]; + this.points.sort((a, b) => a[0] - b[0]); + this.selectedPoint = this.points.findIndex(p => + p[0] === frame && p[1] === value); + this.updatePointsValue(); } } } else { @@ -673,15 +723,11 @@ app.registerExtension({ }; nodeType.prototype.onMouseUp = function(e, pos) { - // If we have a selected point but never started dragging, it's a click - delete the point - if (this.selectedPoint !== null && !this.isDragging) { - this.points.splice(this.selectedPoint, 1); - this.updatePointsValue(); + // Only clear states if we're not in dragging mode + if (!this.isDragging) { + this.selectedPoint = null; + this.dragStartPos = null; } - - this.isDragging = false; - this.selectedPoint = null; - this.dragStartPos = null; this.mouseDownTime = null; this.setDirtyCanvas(true, true); return false; @@ -735,6 +781,21 @@ app.registerExtension({ this.setDirtyCanvas(true, true); return false; }; + + // Template loading method + nodeType.prototype.loadTemplate = function(templateName) { + const frameCount = this.getCurrentFrameCount(); + const minValue = this.widgets.find(w => w.name === "min_value")?.value ?? 0; + const maxValue = this.widgets.find(w => w.name === "max_value")?.value ?? 1; + const cyclesWidget = this.widgets.find(w => w.name === "template_cycles"); + const cycles = cyclesWidget?.value ?? 1; + + if (templateName in FeatureTemplates) { + this.points = FeatureTemplates[templateName](frameCount, minValue, maxValue, cycles); + this.updatePointsValue(); + this.setDirtyCanvas(true, true); + } + }; } } }); \ No newline at end of file diff --git a/web/js/feature_templates.js b/web/js/feature_templates.js new file mode 100644 index 0000000..b25bb8b --- /dev/null +++ b/web/js/feature_templates.js @@ -0,0 +1,202 @@ +// Feature template generation functions +export const FeatureTemplates = { + // Basic waveforms + sine: (frameCount, minValue, maxValue, cycles = 1) => { + const amplitude = (maxValue - minValue) / 2; + const center = minValue + amplitude; + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const value = center + amplitude * Math.sin((i / (numPoints - 1)) * Math.PI * 2 * cycles); + points.push([frame, value]); + } + return points; + }, + + square: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const phase = (i / (numPoints - 1)) * cycles; + const value = Math.floor(phase % 1 * 2) ? minValue : maxValue; + points.push([frame, value]); + } + return points; + }, + + triangle: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const phase = (i / (numPoints - 1)) * cycles % 1; + const triangleValue = phase < 0.5 + ? 2 * phase + : 2 * (1 - phase); + const value = minValue + (maxValue - minValue) * triangleValue; + points.push([frame, value]); + } + return points; + }, + + sawtooth: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const phase = (i / (numPoints - 1)) * cycles % 1; + const value = minValue + (maxValue - minValue) * phase; + points.push([frame, value]); + } + return points; + }, + + // Animation curves + easeInOut: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + // Cubic ease in-out + const easeValue = t < 0.5 + ? 4 * t * t * t + : 1 - Math.pow(-2 * t + 2, 3) / 2; + const value = minValue + (maxValue - minValue) * easeValue; + points.push([frame, value]); + } + return points; + }, + + easeIn: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + // Cubic ease in + const easeValue = t * t * t; + const value = minValue + (maxValue - minValue) * easeValue; + points.push([frame, value]); + } + return points; + }, + + easeOut: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + // Cubic ease out + const easeValue = 1 - Math.pow(1 - t, 3); + const value = minValue + (maxValue - minValue) * easeValue; + points.push([frame, value]); + } + return points; + }, + + // Bounce patterns + bounce: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + // Bounce calculation with bounds check + const bounceValue = Math.abs(Math.sin(t * Math.PI * 3) * (1 - t)); + const value = minValue + (maxValue - minValue) * (1 - bounceValue); + points.push([frame, Math.max(minValue, Math.min(maxValue, value))]); + } + return points; + }, + + // Pulse patterns + pulse: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + // Gaussian pulse + const pulseValue = Math.exp(-Math.pow((Math.sin(t * Math.PI * 2) * 2), 2)); + const value = minValue + (maxValue - minValue) * pulseValue; + points.push([frame, Math.max(minValue, Math.min(maxValue, value))]); + } + return points; + }, + + // Random patterns + random: (frameCount, minValue, maxValue) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const value = minValue + (maxValue - minValue) * Math.random(); + points.push([frame, value]); + } + return points; + }, + + smoothRandom: (frameCount, minValue, maxValue) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + let lastValue = minValue + (maxValue - minValue) * Math.random(); + const maxStep = (maxValue - minValue) * 0.2; // Maximum change per step + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const randomStep = (Math.random() * 2 - 1) * maxStep; + lastValue = Math.max(minValue, Math.min(maxValue, lastValue + randomStep)); + points.push([frame, lastValue]); + } + return points; + }, + + // Special patterns + heartbeat: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + // Double-pulse heartbeat pattern with bounds check + const x = t * 4; + const heartbeatValue = Math.min(1, Math.max(0, + Math.pow(Math.E, -Math.pow(x % 1 * 6 - 1, 2)) * 0.8 + + Math.pow(Math.E, -Math.pow(x % 1 * 6 - 2, 2)) + )); + const value = minValue + (maxValue - minValue) * heartbeatValue; + points.push([frame, value]); + } + return points; + }, + + steps: (frameCount, minValue, maxValue, cycles = 1) => { + const points = []; + const numPoints = Math.min(frameCount, 30); + const numSteps = Math.max(2, Math.round(cycles * 5)); // 5 steps per cycle + + for (let i = 0; i < numPoints; i++) { + const frame = Math.round((i / (numPoints - 1)) * (frameCount - 1)); + const t = (i / (numPoints - 1)) * cycles % 1; + const step = Math.floor(t * numSteps) / (numSteps - 1); + const value = minValue + (maxValue - minValue) * Math.min(1, Math.max(0, step)); + points.push([frame, value]); + } + return points; + } +}; \ No newline at end of file