mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary
Change the CURVE widget value from CurvePoint[] to CurveData ({ points,
interpolation }) to support multiple interpolation types. Add a Select
dropdown in the widget UI for switching between Smooth (monotone cubic)
and Linear interpolation, with the SVG preview updating accordingly.
- Add CurveData type with CURVE_INTERPOLATIONS const enum
- Add createLinearInterpolator with piecewise linear + binary search
- Add createInterpolator factory dispatching by interpolation type
- Add isCurveData type guard in curveUtils
- Update ICurveWidget value type to CurveData
- Add interpolation prop to CurveEditor and useCurveEditor composable
- Linear mode generates direct M...L... SVG path (no sampling)
- Add i18n entries for interpolation labels
- Add unit tests for createLinearInterpolator
BE change is https://github.com/Comfy-Org/ComfyUI/pull/12757
## Screenshots (if applicable)
<img width="1437" height="670" alt="image"
src="https://github.com/user-attachments/assets/550aedec-e5da-425b-8233-86a4f28067fa"
/>
<img width="1445" height="648" alt="image"
src="https://github.com/user-attachments/assets/0a8dc654-3f92-4ca2-9fa2-c1fef3be6d66"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10118-feat-add-linear-interpolation-type-to-CURVE-widget-3256d73d36508185a86edf73bb555c51)
by [Unito](https://www.unito.io)
189 lines
4.8 KiB
TypeScript
189 lines
4.8 KiB
TypeScript
import { CURVE_INTERPOLATIONS } from './types'
|
|
import type { CurveData, CurveInterpolation, CurvePoint } from './types'
|
|
|
|
export function isCurveData(value: unknown): value is CurveData {
|
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
return false
|
|
const v = value as Record<string, unknown>
|
|
return (
|
|
Array.isArray(v.points) &&
|
|
v.points.every(
|
|
(p: unknown) =>
|
|
Array.isArray(p) &&
|
|
p.length === 2 &&
|
|
typeof p[0] === 'number' &&
|
|
typeof p[1] === 'number'
|
|
) &&
|
|
typeof v.interpolation === 'string' &&
|
|
CURVE_INTERPOLATIONS.includes(v.interpolation as CurveInterpolation)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Piecewise linear interpolation through sorted control points.
|
|
* Returns a function that evaluates y for any x in [0, 1].
|
|
*/
|
|
export function createLinearInterpolator(
|
|
points: CurvePoint[]
|
|
): (x: number) => number {
|
|
if (points.length === 0) return () => 0
|
|
if (points.length === 1) return () => points[0][1]
|
|
|
|
const sorted = [...points].sort((a, b) => a[0] - b[0])
|
|
const n = sorted.length
|
|
const xs = sorted.map((p) => p[0])
|
|
const ys = sorted.map((p) => p[1])
|
|
|
|
return (x: number): number => {
|
|
if (x <= xs[0]) return ys[0]
|
|
if (x >= xs[n - 1]) return ys[n - 1]
|
|
|
|
let lo = 0
|
|
let hi = n - 1
|
|
while (lo < hi - 1) {
|
|
const mid = (lo + hi) >> 1
|
|
if (xs[mid] <= x) lo = mid
|
|
else hi = mid
|
|
}
|
|
|
|
const dx = xs[hi] - xs[lo]
|
|
if (dx === 0) return ys[lo]
|
|
const t = (x - xs[lo]) / dx
|
|
return ys[lo] + t * (ys[hi] - ys[lo])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Factory that dispatches to the correct interpolator based on type.
|
|
*/
|
|
export function createInterpolator(
|
|
points: CurvePoint[],
|
|
interpolation: CurveInterpolation
|
|
): (x: number) => number {
|
|
return interpolation === 'linear'
|
|
? createLinearInterpolator(points)
|
|
: createMonotoneInterpolator(points)
|
|
}
|
|
|
|
/**
|
|
* Monotone cubic Hermite interpolation.
|
|
* Produces a smooth curve that passes through all control points
|
|
* without overshooting (monotone property).
|
|
*
|
|
* Returns a function that evaluates y for any x in [0, 1].
|
|
*/
|
|
export function createMonotoneInterpolator(
|
|
points: CurvePoint[]
|
|
): (x: number) => number {
|
|
if (points.length === 0) return () => 0
|
|
if (points.length === 1) return () => points[0][1]
|
|
|
|
const sorted = [...points].sort((a, b) => a[0] - b[0])
|
|
const n = sorted.length
|
|
const xs = sorted.map((p) => p[0])
|
|
const ys = sorted.map((p) => p[1])
|
|
|
|
const deltas: number[] = []
|
|
const slopes: number[] = []
|
|
for (let i = 0; i < n - 1; i++) {
|
|
const dx = xs[i + 1] - xs[i]
|
|
deltas.push(dx === 0 ? 0 : (ys[i + 1] - ys[i]) / dx)
|
|
}
|
|
|
|
slopes.push(deltas[0] ?? 0)
|
|
for (let i = 1; i < n - 1; i++) {
|
|
if (deltas[i - 1] * deltas[i] <= 0) {
|
|
slopes.push(0)
|
|
} else {
|
|
slopes.push((deltas[i - 1] + deltas[i]) / 2)
|
|
}
|
|
}
|
|
slopes.push(deltas[n - 2] ?? 0)
|
|
|
|
for (let i = 0; i < n - 1; i++) {
|
|
if (deltas[i] === 0) {
|
|
slopes[i] = 0
|
|
slopes[i + 1] = 0
|
|
} else {
|
|
const alpha = slopes[i] / deltas[i]
|
|
const beta = slopes[i + 1] / deltas[i]
|
|
const s = alpha * alpha + beta * beta
|
|
if (s > 9) {
|
|
const t = 3 / Math.sqrt(s)
|
|
slopes[i] = t * alpha * deltas[i]
|
|
slopes[i + 1] = t * beta * deltas[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return (x: number): number => {
|
|
if (x <= xs[0]) return ys[0]
|
|
if (x >= xs[n - 1]) return ys[n - 1]
|
|
|
|
let lo = 0
|
|
let hi = n - 1
|
|
while (lo < hi - 1) {
|
|
const mid = (lo + hi) >> 1
|
|
if (xs[mid] <= x) lo = mid
|
|
else hi = mid
|
|
}
|
|
|
|
const dx = xs[hi] - xs[lo]
|
|
if (dx === 0) return ys[lo]
|
|
|
|
const t = (x - xs[lo]) / dx
|
|
const t2 = t * t
|
|
const t3 = t2 * t
|
|
|
|
const h00 = 2 * t3 - 3 * t2 + 1
|
|
const h10 = t3 - 2 * t2 + t
|
|
const h01 = -2 * t3 + 3 * t2
|
|
const h11 = t3 - t2
|
|
|
|
return (
|
|
h00 * ys[lo] +
|
|
h10 * dx * slopes[lo] +
|
|
h01 * ys[hi] +
|
|
h11 * dx * slopes[hi]
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a 256-bin histogram into an SVG path string.
|
|
* Normalizes using the 99.5th percentile to avoid outlier spikes.
|
|
*/
|
|
export function histogramToPath(histogram: Uint32Array): string {
|
|
if (!histogram.length) return ''
|
|
|
|
const sorted = Array.from(histogram).sort((a, b) => a - b)
|
|
const max = sorted[Math.floor(255 * 0.995)]
|
|
if (max === 0) return ''
|
|
|
|
const invMax = 1 / max
|
|
const parts: string[] = ['M0,1']
|
|
for (let i = 0; i < 256; i++) {
|
|
const x = i / 255
|
|
const y = 1 - Math.min(1, histogram[i] * invMax)
|
|
parts.push(`L${x},${y}`)
|
|
}
|
|
parts.push('L1,1 Z')
|
|
return parts.join(' ')
|
|
}
|
|
|
|
export function curvesToLUT(
|
|
points: CurvePoint[],
|
|
interpolation: CurveInterpolation = 'monotone_cubic'
|
|
): Uint8Array {
|
|
const lut = new Uint8Array(256)
|
|
const interpolate = createInterpolator(points, interpolation)
|
|
|
|
for (let i = 0; i < 256; i++) {
|
|
const x = i / 255
|
|
const y = interpolate(x)
|
|
lut[i] = Math.max(0, Math.min(255, Math.round(y * 255)))
|
|
}
|
|
|
|
return lut
|
|
}
|