Spaces:
Runtime error
Runtime error
| import { app } from "scripts/app.js"; | |
| import type { LGraphCanvas as TLGraphCanvas, Vector2 } from "../typings/litegraph.js"; | |
| function binarySearch(max: number, getValue: (n: number) => number, match: number) { | |
| let min = 0; | |
| while (min <= max) { | |
| let guess = Math.floor((min + max) / 2); | |
| const compareVal = getValue(guess); | |
| if (compareVal === match) return guess; | |
| if (compareVal < match) min = guess + 1; | |
| else max = guess - 1; | |
| } | |
| return max; | |
| } | |
| /** | |
| * Fits a string against a max width for a ctx. Font should be defined on ctx beforehand. | |
| */ | |
| export function fitString(ctx: CanvasRenderingContext2D, str: string, maxWidth: number) { | |
| let width = ctx.measureText(str).width; | |
| const ellipsis = "…"; | |
| const ellipsisWidth = measureText(ctx, ellipsis); | |
| if (width <= maxWidth || width <= ellipsisWidth) { | |
| return str; | |
| } | |
| const index = binarySearch( | |
| str.length, | |
| (guess) => measureText(ctx, str.substring(0, guess)), | |
| maxWidth - ellipsisWidth, | |
| ); | |
| return str.substring(0, index) + ellipsis; | |
| } | |
| /** Measures the width of text for a canvas context. */ | |
| export function measureText(ctx: CanvasRenderingContext2D, str: string) { | |
| return ctx.measureText(str).width; | |
| } | |
| export type WidgetRenderingOptionsPart = { | |
| type?: "toggle" | "custom"; | |
| margin?: number; | |
| fillStyle?: string; | |
| strokeStyle?: string; | |
| lowQuality?: boolean; | |
| draw?(ctx: CanvasRenderingContext2D, x: number, lowQuality: boolean): number; | |
| }; | |
| type WidgetRenderingOptions = { | |
| width: number; | |
| height: number; | |
| posX?: number; | |
| posY: number; | |
| borderRadius?: number; | |
| colorStroke?: string; | |
| colorBackground?: string; | |
| // node: LGraphNode; | |
| // value?: any; | |
| // margin?: number; | |
| // direction?: "right" | "left"; | |
| // fillStyle?: string; | |
| // strokeStyle?: string; | |
| // parts: WidgetRenderingOptionsPart[]; | |
| }; | |
| export function isLowQuality() { | |
| const canvas = app.canvas as TLGraphCanvas; | |
| return (canvas.ds?.scale || 1) <= 0.5; | |
| } | |
| export function drawNodeWidget(ctx: CanvasRenderingContext2D, options: WidgetRenderingOptions) { | |
| const lowQuality = isLowQuality(); | |
| const data = { | |
| width: options.width, | |
| height: options.height, | |
| posY: options.posY, | |
| lowQuality, | |
| margin: 15, | |
| colorOutline: LiteGraph.WIDGET_OUTLINE_COLOR, | |
| colorBackground: LiteGraph.WIDGET_BGCOLOR, | |
| colorText: LiteGraph.WIDGET_TEXT_COLOR, | |
| colorTextSecondary: LiteGraph.WIDGET_SECONDARY_TEXT_COLOR, | |
| }; | |
| // Draw background. | |
| ctx.strokeStyle = options.colorStroke || data.colorOutline; | |
| ctx.fillStyle = options.colorBackground || data.colorBackground; | |
| ctx.beginPath(); | |
| ctx.roundRect( | |
| data.margin, | |
| data.posY, | |
| data.width - data.margin * 2, | |
| data.height, | |
| lowQuality ? [0] : options.borderRadius ? [options.borderRadius] : [options.height * 0.5], | |
| ); | |
| ctx.fill(); | |
| if (!lowQuality) { | |
| ctx.stroke(); | |
| } | |
| return data; | |
| } | |
| /** Draws a rounded rectangle. */ | |
| export function drawRoundedRectangle( | |
| ctx: CanvasRenderingContext2D, | |
| options: WidgetRenderingOptions, | |
| ) { | |
| const lowQuality = isLowQuality(); | |
| options = { ...options }; | |
| ctx.strokeStyle = options.colorStroke || LiteGraph.WIDGET_OUTLINE_COLOR; | |
| ctx.fillStyle = options.colorBackground || LiteGraph.WIDGET_BGCOLOR; | |
| ctx.beginPath(); | |
| ctx.roundRect( | |
| options.posX!, | |
| options.posY, | |
| options.width, | |
| options.height, | |
| lowQuality ? [0] : options.borderRadius ? [options.borderRadius] : [options.height * 0.5], | |
| ); | |
| ctx.fill(); | |
| !lowQuality && ctx.stroke(); | |
| } | |
| type DrawNumberWidgetPartOptions = { | |
| posX: number; | |
| posY: number; | |
| height: number; | |
| value: number; | |
| direction?: 1 | -1; | |
| textColor?: string; | |
| }; | |
| /** | |
| * Draws a number picker with arrows off to each side. | |
| * | |
| * This is for internal widgets that may have many hit areas (full-width, default number widgets put | |
| * the arrows on either side of the full-width row). | |
| */ | |
| export function drawNumberWidgetPart( | |
| ctx: CanvasRenderingContext2D, | |
| options: DrawNumberWidgetPartOptions, | |
| ): [Vector2, Vector2, Vector2] { | |
| const arrowWidth = 9; | |
| const arrowHeight = 10; | |
| const innerMargin = 3; | |
| const numberWidth = 32; | |
| const xBoundsArrowLess: Vector2 = [0, 0]; | |
| const xBoundsNumber: Vector2 = [0, 0]; | |
| const xBoundsArrowMore: Vector2 = [0, 0]; | |
| ctx.save(); | |
| let posX = options.posX; | |
| const { posY, height, value, textColor } = options; | |
| const midY = posY + height / 2; | |
| // If we're drawing parts from right to left (usually when something in the middle will be | |
| // flexible), then we can simply move left the expected width of our widget and draw forwards. | |
| if (options.direction === -1) { | |
| posX = posX - arrowWidth - innerMargin - numberWidth - innerMargin - arrowWidth; | |
| } | |
| // Draw the strength left arrow. | |
| ctx.fill( | |
| new Path2D( | |
| `M ${posX} ${midY} l ${arrowWidth} ${ | |
| arrowHeight / 2 | |
| } l 0 -${arrowHeight} L ${posX} ${midY} z`, | |
| ), | |
| ); | |
| xBoundsArrowLess[0] = posX; | |
| xBoundsArrowLess[1] = arrowWidth; | |
| posX += arrowWidth + innerMargin; | |
| // Draw the strength text. | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| const oldTextcolor = ctx.fillStyle; | |
| if (textColor) { | |
| ctx.fillStyle = textColor; | |
| } | |
| ctx.fillText(fitString(ctx, value.toFixed(2), numberWidth), posX + numberWidth / 2, midY); | |
| ctx.fillStyle = oldTextcolor; | |
| xBoundsNumber[0] = posX; | |
| xBoundsNumber[1] = numberWidth; | |
| posX += numberWidth + innerMargin; | |
| // Draw the strength right arrow. | |
| ctx.fill( | |
| new Path2D( | |
| `M ${posX} ${midY - arrowHeight / 2} l ${arrowWidth} ${arrowHeight / 2} l -${arrowWidth} ${ | |
| arrowHeight / 2 | |
| } v -${arrowHeight} z`, | |
| ), | |
| ); | |
| xBoundsArrowMore[0] = posX; | |
| xBoundsArrowMore[1] = arrowWidth; | |
| ctx.restore(); | |
| return [xBoundsArrowLess, xBoundsNumber, xBoundsArrowMore]; | |
| } | |
| drawNumberWidgetPart.WIDTH_TOTAL = 9 + 3 + 32 + 3 + 9; | |
| type DrawTogglePartOptions = { | |
| posX: number; | |
| posY: number; | |
| height: number; | |
| value: boolean | null; | |
| }; | |
| /** | |
| * Draws a toggle for a widget. The toggle is a three-way switch with left being false, right being | |
| * true, and a middle state being null. | |
| */ | |
| export function drawTogglePart( | |
| ctx: CanvasRenderingContext2D, | |
| options: DrawTogglePartOptions, | |
| ): Vector2 { | |
| const lowQuality = isLowQuality(); | |
| ctx.save(); | |
| const { posX, posY, height, value } = options; | |
| const toggleRadius = height * 0.36; // This is the standard toggle height calc. | |
| const toggleBgWidth = height * 1.5; // We don't draw a separate bg, but this would be it. | |
| // Toggle Track | |
| if (!lowQuality) { | |
| ctx.beginPath(); | |
| ctx.roundRect(posX + 4, posY + 4, toggleBgWidth - 8, height - 8, [height * 0.5]); | |
| ctx.globalAlpha = app.canvas.editor_alpha * 0.25; | |
| ctx.fillStyle = "rgba(255,255,255,0.45)"; | |
| ctx.fill(); | |
| ctx.globalAlpha = app.canvas.editor_alpha; | |
| } | |
| // Toggle itself | |
| ctx.fillStyle = value === true ? "#89B" : "#888"; | |
| const toggleX = | |
| lowQuality || value === false | |
| ? posX + height * 0.5 | |
| : value === true | |
| ? posX + height | |
| : posX + height * 0.75; | |
| ctx.beginPath(); | |
| ctx.arc(toggleX, posY + height * 0.5, toggleRadius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| return [posX, toggleBgWidth]; | |
| } | |
| export function drawInfoIcon( | |
| ctx: CanvasRenderingContext2D, | |
| x: number, | |
| y: number, | |
| size: number = 12, | |
| ) { | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.roundRect(x, y, size, size, [size * 0.1]); | |
| ctx.fillStyle = "#2f82ec"; | |
| ctx.strokeStyle = "#0f2a5e"; | |
| ctx.fill(); | |
| // ctx.stroke(); | |
| ctx.strokeStyle = "#FFF"; | |
| ctx.lineWidth = 2; | |
| // ctx.lineCap = 'round'; | |
| const midX = x + size / 2; | |
| const serifSize = size * 0.175; | |
| ctx.stroke( | |
| new Path2D(` | |
| M ${midX} ${y + size * 0.15} | |
| v 2 | |
| M ${midX - serifSize} ${y + size * 0.45} | |
| h ${serifSize} | |
| v ${size * 0.325} | |
| h ${serifSize} | |
| h -${serifSize * 2} | |
| `), | |
| ); | |
| ctx.restore(); | |
| } | |