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(); }