mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 08:30:06 +00:00
Enables TypeScript rules that improve code legibility.
- Requires `override` keyword
- Prevent indexed properties from being accessed with dot notation
```ts
const obj: Record<string, unknown> = {}
// Prefer
obj["property"]
// Over
obj.property
```
248 lines
6.4 KiB
TypeScript
248 lines
6.4 KiB
TypeScript
import type { IKnobWidget, IWidgetKnobOptions } from "@/types/widgets"
|
|
|
|
import { clamp } from "@/litegraph"
|
|
import { getWidgetStep } from "@/utils/widget"
|
|
|
|
import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget"
|
|
|
|
export class KnobWidget extends BaseWidget implements IKnobWidget {
|
|
declare type: "knob"
|
|
declare value: number
|
|
declare options: IWidgetKnobOptions
|
|
|
|
constructor(widget: IKnobWidget) {
|
|
super(widget)
|
|
this.type = "knob"
|
|
this.value = widget.value
|
|
this.options = widget.options
|
|
}
|
|
|
|
computedHeight?: number
|
|
/**
|
|
* Compute the layout size of the widget.
|
|
* @returns The layout size of the widget.
|
|
*/
|
|
computeLayoutSize(): {
|
|
minHeight: number
|
|
maxHeight?: number
|
|
minWidth: number
|
|
maxWidth?: number
|
|
} {
|
|
return {
|
|
minHeight: 60,
|
|
minWidth: 20,
|
|
maxHeight: 1_000_000,
|
|
maxWidth: 1_000_000,
|
|
}
|
|
}
|
|
|
|
override get height(): number {
|
|
return this.computedHeight || super.height
|
|
}
|
|
|
|
drawWidget(
|
|
ctx: CanvasRenderingContext2D,
|
|
{
|
|
width,
|
|
showText = true,
|
|
}: DrawWidgetOptions,
|
|
): void {
|
|
// Store original context attributes
|
|
const originalTextAlign = ctx.textAlign
|
|
const originalStrokeStyle = ctx.strokeStyle
|
|
const originalFillStyle = ctx.fillStyle
|
|
|
|
const { y } = this
|
|
const { margin } = BaseWidget
|
|
|
|
const { gradient_stops = "rgb(14, 182, 201); rgb(0, 216, 72)" } = this.options
|
|
const effective_height = this.computedHeight || this.height
|
|
// Draw background
|
|
const size_modifier =
|
|
Math.min(this.computedHeight || this.height, this.width || 20) / 20 // TODO: replace magic numbers
|
|
const arc_center = { x: width / 2, y: effective_height / 2 + y }
|
|
ctx.lineWidth =
|
|
(Math.min(width, effective_height) - margin * size_modifier) / 6
|
|
const arc_size =
|
|
(Math.min(width, effective_height) -
|
|
margin * size_modifier -
|
|
ctx.lineWidth) / 2
|
|
{
|
|
const gradient = ctx.createRadialGradient(
|
|
arc_center.x,
|
|
arc_center.y,
|
|
arc_size + ctx.lineWidth,
|
|
0,
|
|
0,
|
|
arc_size + ctx.lineWidth,
|
|
)
|
|
gradient.addColorStop(0, "rgb(29, 29, 29)")
|
|
gradient.addColorStop(1, "rgb(116, 116, 116)")
|
|
ctx.fillStyle = gradient
|
|
}
|
|
ctx.beginPath()
|
|
|
|
{
|
|
ctx.arc(
|
|
arc_center.x,
|
|
arc_center.y,
|
|
arc_size + ctx.lineWidth / 2,
|
|
0,
|
|
Math.PI * 2,
|
|
false,
|
|
)
|
|
ctx.fill()
|
|
ctx.closePath()
|
|
}
|
|
|
|
// Draw knob's background
|
|
const arc = {
|
|
start_angle: Math.PI * 0.6,
|
|
end_angle: Math.PI * 2.4,
|
|
}
|
|
ctx.beginPath()
|
|
{
|
|
const gradient = ctx.createRadialGradient(
|
|
arc_center.x,
|
|
arc_center.y,
|
|
arc_size + ctx.lineWidth,
|
|
0,
|
|
0,
|
|
arc_size + ctx.lineWidth,
|
|
)
|
|
gradient.addColorStop(0, "rgb(99, 99, 99)")
|
|
gradient.addColorStop(1, "rgb(36, 36, 36)")
|
|
ctx.strokeStyle = gradient
|
|
}
|
|
ctx.arc(
|
|
arc_center.x,
|
|
arc_center.y,
|
|
arc_size,
|
|
arc.start_angle,
|
|
arc.end_angle,
|
|
false,
|
|
)
|
|
ctx.stroke()
|
|
ctx.closePath()
|
|
|
|
const range = this.options.max - this.options.min
|
|
let nvalue = (this.value - this.options.min) / range
|
|
nvalue = clamp(nvalue, 0, 1)
|
|
|
|
// Draw value
|
|
ctx.beginPath()
|
|
const gradient = ctx.createConicGradient(
|
|
arc.start_angle,
|
|
arc_center.x,
|
|
arc_center.y,
|
|
)
|
|
const gs = gradient_stops.split(";")
|
|
for (const [index, stop] of gs.entries()) {
|
|
gradient.addColorStop(index, stop.trim())
|
|
}
|
|
|
|
ctx.strokeStyle = gradient
|
|
const value_end_angle =
|
|
(arc.end_angle - arc.start_angle) * nvalue + arc.start_angle
|
|
ctx.arc(
|
|
arc_center.x,
|
|
arc_center.y,
|
|
arc_size,
|
|
arc.start_angle,
|
|
value_end_angle,
|
|
false,
|
|
)
|
|
ctx.stroke()
|
|
ctx.closePath()
|
|
|
|
// Draw outline if not disabled
|
|
if (showText && !this.computedDisabled) {
|
|
ctx.strokeStyle = this.outline_color
|
|
// Draw value
|
|
ctx.beginPath()
|
|
ctx.strokeStyle = this.outline_color
|
|
ctx.arc(
|
|
arc_center.x,
|
|
arc_center.y,
|
|
arc_size + ctx.lineWidth / 2,
|
|
0,
|
|
Math.PI * 2,
|
|
false,
|
|
)
|
|
ctx.lineWidth = 1
|
|
ctx.stroke()
|
|
ctx.closePath()
|
|
}
|
|
|
|
// Draw marker if present
|
|
// TODO: TBD later when options work
|
|
|
|
// Draw text
|
|
if (showText) {
|
|
ctx.textAlign = "center"
|
|
ctx.fillStyle = this.text_color
|
|
const fixedValue = Number(this.value).toFixed(this.options.precision ?? 3)
|
|
ctx.fillText(
|
|
`${this.label || this.name}\n${fixedValue}`,
|
|
width * 0.5,
|
|
y + effective_height * 0.5,
|
|
)
|
|
}
|
|
|
|
// Restore original context attributes
|
|
ctx.textAlign = originalTextAlign
|
|
ctx.strokeStyle = originalStrokeStyle
|
|
ctx.fillStyle = originalFillStyle
|
|
}
|
|
|
|
onClick(): void {
|
|
this.current_drag_offset = 0
|
|
}
|
|
|
|
current_drag_offset = 0
|
|
override onDrag(options: WidgetEventOptions): void {
|
|
if (this.options.read_only) return
|
|
const { e } = options
|
|
const step = getWidgetStep(this.options)
|
|
// Shift to move by 10% increments
|
|
const range = (this.options.max - this.options.min)
|
|
const range_10_percent = range / 10
|
|
const range_1_percent = range / 100
|
|
const step_for = {
|
|
delta_x: step,
|
|
shift: range_10_percent > step ? range_10_percent - (range_10_percent % step) : step,
|
|
delta_y: range_1_percent > step ? range_1_percent - (range_1_percent % step) : step, // 1% increments
|
|
}
|
|
|
|
const use_y = Math.abs(e.movementY) > Math.abs(e.movementX)
|
|
const delta = use_y ? -e.movementY : e.movementX // Y is inverted so that UP increases the value
|
|
const drag_threshold = 15
|
|
// Calculate new value based on drag movement
|
|
this.current_drag_offset += delta
|
|
let adjustment = 0
|
|
if (this.current_drag_offset > drag_threshold) {
|
|
adjustment += 1
|
|
this.current_drag_offset -= drag_threshold
|
|
} else if (this.current_drag_offset < -drag_threshold) {
|
|
adjustment -= 1
|
|
this.current_drag_offset += drag_threshold
|
|
}
|
|
|
|
const step_with_shift_modifier = e.shiftKey
|
|
? step_for.shift
|
|
: (use_y
|
|
? step_for.delta_y
|
|
: step)
|
|
|
|
const deltaValue = adjustment * step_with_shift_modifier
|
|
const newValue = clamp(
|
|
this.value + deltaValue,
|
|
this.options.min,
|
|
this.options.max,
|
|
)
|
|
if (newValue !== this.value) {
|
|
this.setValue(newValue, options)
|
|
}
|
|
}
|
|
}
|