feat: Enhance live preview rendering and state management
- Introduced new state properties in ensureState to manage rendering state, including lastRenderTick, renderInFlight, queuedRenderTick, and renderNonce. - Added hashText and serializePreviewValue functions for efficient preview value serialization. - Implemented buildPreviewRenderKey to create a unique key for rendering based on node state and inputs. - Improved rendering logic in registerImageOpsLivePreview to handle queued renders and prevent unnecessary re-renders. - Refactored blit function to accept various source types and improved canvas handling. - Added ensureCanvasSize and renderImageSourceToCanvas functions to streamline canvas creation and image rendering. - Updated ops and renderer modules to utilize new rendering functions and improve performance. - Enhanced error handling during rendering processes to provide clearer feedback.
This commit is contained in:
+136
-29
@@ -50,6 +50,10 @@ function ensureState(node) {
|
||||
rafId: null,
|
||||
debounceTimer: null,
|
||||
lastKey: null,
|
||||
lastRenderTick: null,
|
||||
renderInFlight: false,
|
||||
queuedRenderTick: null,
|
||||
renderNonce: 0,
|
||||
isPreview: isPreviewNode(node),
|
||||
nativeAnimated: false,
|
||||
nativeDirty: false,
|
||||
@@ -92,6 +96,50 @@ function ensureState(node) {
|
||||
});
|
||||
return node.__imageops_state;
|
||||
}
|
||||
function hashText(value) {
|
||||
let hash = 2166136261;
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
hash ^= value.charCodeAt(index);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
function serializePreviewValue(value) {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "string") {
|
||||
return value.length > 120 ? `str:${value.length}:${hashText(value)}` : `str:${value}`;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (Array.isArray(value)) {
|
||||
return `arr:${value.length}:${value.map((entry) => serializePreviewValue(entry)).join(",")}`;
|
||||
}
|
||||
try {
|
||||
const json = JSON.stringify(value);
|
||||
if (!json) return String(value);
|
||||
return json.length > 120 ? `json:${json.length}:${hashText(json)}` : `json:${json}`;
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
function buildPreviewRenderKey(node, tick, st) {
|
||||
const parts = [
|
||||
String(node.id),
|
||||
String(node.comfyClass ?? ""),
|
||||
`tick:${tick}`,
|
||||
`dirty:${st.nativeDirty ? 1 : 0}`,
|
||||
`anim:${st.nativeAnimated ? 1 : 0}`,
|
||||
`imageIndex:${typeof node.imageIndex === "number" ? node.imageIndex : -1}`,
|
||||
`imgs:${Array.isArray(node.imgs) ? node.imgs.length : 0}`
|
||||
];
|
||||
for (const widget of node.widgets ?? []) {
|
||||
parts.push(`${widget?.name ?? "widget"}=${serializePreviewValue(widget?.value)}`);
|
||||
}
|
||||
for (let index = 0; index < (node.inputs?.length ?? 0); index++) {
|
||||
const input = node.inputs?.[index];
|
||||
parts.push(`in${index}:${input?.name ?? ""}:${input?.link ?? "null"}`);
|
||||
}
|
||||
return parts.join("|");
|
||||
}
|
||||
function stopRAF(st) {
|
||||
if (st?.rafId) {
|
||||
cancelAnimationFrame(st.rafId);
|
||||
@@ -952,29 +1000,34 @@ function tryRenderNativePreview(node, st, canvasSize) {
|
||||
}
|
||||
const sourceWidth = img.naturalWidth || img.width || 1;
|
||||
const sourceHeight = img.naturalHeight || img.height || 1;
|
||||
const frame = document.createElement("canvas");
|
||||
frame.width = sourceWidth;
|
||||
frame.height = sourceHeight;
|
||||
const frameCtx = frame.getContext("2d");
|
||||
if (!frameCtx) return false;
|
||||
frameCtx.clearRect(0, 0, sourceWidth, sourceHeight);
|
||||
frameCtx.drawImage(img, 0, 0, sourceWidth, sourceHeight);
|
||||
blit(node, st, frame, canvasSize);
|
||||
blit(node, st, img, canvasSize, sourceWidth, sourceHeight);
|
||||
st.info.textContent = st.nativeAnimated ? "Node preview (animated)" : "Node preview";
|
||||
return true;
|
||||
}
|
||||
function blit(node, st, imgCanvas, canvasSize) {
|
||||
function blit(node, st, source, canvasSize, sourceWidth, sourceHeight) {
|
||||
if (!st.canvas) return;
|
||||
const ctx = st.canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
st.canvas.width = canvasSize;
|
||||
st.canvas.height = canvasSize;
|
||||
drawFitSource(ctx, canvasSize, canvasSize, imgCanvas, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
if (st.canvas.width !== canvasSize) st.canvas.width = canvasSize;
|
||||
if (st.canvas.height !== canvasSize) st.canvas.height = canvasSize;
|
||||
const resolvedWidth = Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
sourceWidth ?? source.naturalWidth ?? source.videoWidth ?? source.width ?? 1
|
||||
)
|
||||
);
|
||||
const resolvedHeight = Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
sourceHeight ?? source.naturalHeight ?? source.videoHeight ?? source.height ?? 1
|
||||
)
|
||||
);
|
||||
drawFitSource(ctx, canvasSize, canvasSize, source, resolvedWidth, resolvedHeight);
|
||||
if (isDrawNode(node)) {
|
||||
const fit = getFitPlacement(canvasSize, canvasSize, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
const fit = getFitPlacement(canvasSize, canvasSize, resolvedWidth, resolvedHeight);
|
||||
st.drawGeometry = {
|
||||
sourceWidth: imgCanvas.width || 1,
|
||||
sourceHeight: imgCanvas.height || 1,
|
||||
sourceWidth: resolvedWidth,
|
||||
sourceHeight: resolvedHeight,
|
||||
fitDx: fit.dx,
|
||||
fitDy: fit.dy,
|
||||
fitDrawWidth: fit.drawWidth,
|
||||
@@ -983,9 +1036,9 @@ function blit(node, st, imgCanvas, canvasSize) {
|
||||
} else {
|
||||
st.drawGeometry = null;
|
||||
}
|
||||
st.cropGeometry = drawCropBounds(node, ctx, canvasSize, canvasSize, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
drawTransformBounds(node, ctx, canvasSize, canvasSize, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
drawCompBounds(node, ctx, canvasSize, canvasSize, st.compOutputWidth || imgCanvas.width || 1, st.compOutputHeight || imgCanvas.height || 1, st.compLayers);
|
||||
st.cropGeometry = drawCropBounds(node, ctx, canvasSize, canvasSize, resolvedWidth, resolvedHeight);
|
||||
drawTransformBounds(node, ctx, canvasSize, canvasSize, resolvedWidth, resolvedHeight);
|
||||
drawCompBounds(node, ctx, canvasSize, canvasSize, st.compOutputWidth || resolvedWidth, st.compOutputHeight || resolvedHeight, st.compLayers);
|
||||
}
|
||||
function getCropInfoText(node) {
|
||||
const preset = widgetString(node, "aspect_ratio", "custom");
|
||||
@@ -1717,15 +1770,46 @@ function registerImageOpsLivePreview() {
|
||||
function renderNode(node, tick = 0) {
|
||||
const st = ensurePreviewWidget(node, progress, canvasSize);
|
||||
if (!st) return;
|
||||
const renderKey = buildPreviewRenderKey(node, tick, st);
|
||||
if (!st.nativeDirty && st.lastKey === renderKey && st.lastRenderTick === tick) {
|
||||
return;
|
||||
}
|
||||
if (st.renderInFlight) {
|
||||
st.queuedRenderTick = tick;
|
||||
return;
|
||||
}
|
||||
const finishRender = () => {
|
||||
st.renderInFlight = false;
|
||||
const queuedTick = st.queuedRenderTick;
|
||||
st.queuedRenderTick = null;
|
||||
if (queuedTick != null) {
|
||||
renderNode(node, queuedTick);
|
||||
}
|
||||
};
|
||||
const commitRender = () => {
|
||||
st.lastKey = renderKey;
|
||||
st.lastRenderTick = tick;
|
||||
st.nativeDirty = false;
|
||||
};
|
||||
const failRender = (message, error) => {
|
||||
st.info.textContent = message;
|
||||
console.warn("[ImageOps] render error", error);
|
||||
finishRender();
|
||||
};
|
||||
st.renderInFlight = true;
|
||||
st.renderNonce += 1;
|
||||
if (isPreviewNode(node)) {
|
||||
if (isGraphTooLarge(node?.graph, cfg.maxGraphNodes)) {
|
||||
st.info.textContent = "Live preview disabled: graph too large";
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
renderPreviewBridgeNode(node, tick).catch((err) => {
|
||||
st.info.textContent = "Preview bridge error (check console)";
|
||||
console.warn("[ImageOps] preview bridge render error", err);
|
||||
renderPreviewBridgeNode(node, tick).then(() => {
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
failRender("Preview bridge error (check console)", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1733,11 +1817,14 @@ function registerImageOpsLivePreview() {
|
||||
if (isGraphTooLarge(node?.graph, cfg.maxGraphNodes)) {
|
||||
st.info.textContent = "Live preview disabled: graph too large";
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
renderDrawNode(node, tick).catch((err) => {
|
||||
st.info.textContent = "Draw preview error (check console)";
|
||||
console.warn("[ImageOps] draw render error", err);
|
||||
renderDrawNode(node, tick).then(() => {
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
failRender("Draw preview error (check console)", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1746,26 +1833,30 @@ function registerImageOpsLivePreview() {
|
||||
st.info.textContent = "Live preview disabled: graph too large";
|
||||
st.cropGeometry = null;
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
const upstream = getUpstreamNode(node, 0);
|
||||
if (!upstream) {
|
||||
st.info.textContent = "Live preview: connect a supported loader/chain";
|
||||
st.cropGeometry = null;
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
renderer.render(upstream, tick).then((result) => {
|
||||
if (!result?.canvas) {
|
||||
st.info.textContent = "Live preview: connect a supported loader/chain";
|
||||
st.cropGeometry = null;
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
blit(node, st, result.canvas, canvasSize);
|
||||
st.info.textContent = getCropInfoText(node);
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
st.info.textContent = "Live preview error (check console)";
|
||||
st.cropGeometry = null;
|
||||
console.warn("[ImageOps] render error", err);
|
||||
failRender("Live preview error (check console)", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1774,6 +1865,7 @@ function registerImageOpsLivePreview() {
|
||||
st.info.textContent = "Live preview disabled: graph too large";
|
||||
st.compLayers = [];
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
const slots = getCompSlots(node);
|
||||
@@ -1796,6 +1888,7 @@ function registerImageOpsLivePreview() {
|
||||
st.compOutputWidth = Math.max(1, Math.round(widgetNumber(node, "width", 1024)));
|
||||
st.compOutputHeight = Math.max(1, Math.round(widgetNumber(node, "height", 1024)));
|
||||
updateCompControls(node);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
const rendered = renderCompPreview(node, ordered);
|
||||
@@ -1805,24 +1898,31 @@ function registerImageOpsLivePreview() {
|
||||
blit(node, st, rendered.canvas, canvasSize);
|
||||
st.info.textContent = getCompInfoText(node, ordered.length, slots.length, st.compOutputWidth, st.compOutputHeight);
|
||||
updateCompControls(node);
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
st.info.textContent = "Comp preview error (check console)";
|
||||
st.compLayers = [];
|
||||
console.warn("[ImageOps] comp render error", err);
|
||||
finishRender();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (tryRenderNativePreview(node, st, canvasSize)) {
|
||||
commitRender();
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
if (isGraphTooLarge(node?.graph, cfg.maxGraphNodes)) {
|
||||
st.info.textContent = "Live preview disabled: graph too large";
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
renderer.render(node, tick).then((result) => {
|
||||
if (!result?.canvas) {
|
||||
st.info.textContent = "Live preview: connect a supported loader/chain";
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
blit(node, st, result.canvas, canvasSize);
|
||||
@@ -1836,9 +1936,10 @@ function registerImageOpsLivePreview() {
|
||||
} else {
|
||||
st.info.textContent = "Live preview (no queue)";
|
||||
}
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
st.info.textContent = "Live preview error (check console)";
|
||||
console.warn("[ImageOps] render error", err);
|
||||
failRender("Live preview error (check console)", err);
|
||||
});
|
||||
}
|
||||
function startLoopIfVideo(node) {
|
||||
@@ -1857,6 +1958,7 @@ function registerImageOpsLivePreview() {
|
||||
return;
|
||||
}
|
||||
let tick = 0;
|
||||
let lastLoopTick = null;
|
||||
const proceduralFrameCount = getProceduralFrameCount(node);
|
||||
const proceduralFps = proceduralFrameCount != null ? getProceduralPlaybackFps(node) ?? 12 : null;
|
||||
const startedAt = performance.now();
|
||||
@@ -1867,7 +1969,10 @@ function registerImageOpsLivePreview() {
|
||||
} else {
|
||||
tick++;
|
||||
}
|
||||
renderNode(node, tick);
|
||||
if (tick !== lastLoopTick) {
|
||||
lastLoopTick = tick;
|
||||
renderNode(node, tick);
|
||||
}
|
||||
st.rafId = requestAnimationFrame(loop);
|
||||
};
|
||||
stopRAF(st);
|
||||
@@ -1968,6 +2073,8 @@ function registerImageOpsLivePreview() {
|
||||
const r = origExecuted?.apply(this, arguments);
|
||||
st.nativeAnimated = !!message?.animated?.[0];
|
||||
st.nativeDirty = false;
|
||||
st.lastKey = null;
|
||||
st.lastRenderTick = null;
|
||||
schedule(node, () => {
|
||||
if (IMAGEOPS_CLASSES.has(node.comfyClass)) startLoopIfVideo(node);
|
||||
refreshDependents(node);
|
||||
|
||||
+3
-3
@@ -421,7 +421,7 @@ function distortConnectedInputs(node, inputs) {
|
||||
return { source, displacement, mask };
|
||||
}
|
||||
function extractCanvasField(canvas, width, height, channel) {
|
||||
const fitted = fitCanvas(canvas, width, height);
|
||||
const fitted = (canvas.width || 1) === width && (canvas.height || 1) === height ? canvas : fitCanvas(canvas, width, height);
|
||||
const data = fitted.getContext("2d").getImageData(0, 0, width, height).data;
|
||||
const field = new Float32Array(width * height);
|
||||
const normalized = String(channel || "red").toLowerCase();
|
||||
@@ -515,7 +515,7 @@ function renderDistortCanvas(node, inputs, frameIndex = 0) {
|
||||
xField = extractCanvasField(driver, width, height, strAny(node, ["x_channel"], "Red", frameIndex));
|
||||
yField = extractCanvasField(driver, width, height, strAny(node, ["y_channel"], "Green", frameIndex));
|
||||
}
|
||||
const sourceCanvas = fitCanvas(source, width, height);
|
||||
const sourceCanvas = source;
|
||||
const sourceCtx = sourceCanvas.getContext("2d");
|
||||
const sourceData = sourceCtx.getImageData(0, 0, width, height);
|
||||
const output = makeCanvas(width, height);
|
||||
@@ -1011,7 +1011,7 @@ function resolvePreviewMaskCanvas(node, source, rawMask, frameIndex = 0) {
|
||||
return boolAny(node, ["invert_mask"], false, frameIndex) ? invertMaskCanvas(matte) : matte;
|
||||
}
|
||||
function compositeProcessedWithMask(baseCanvas, processedCanvas, maskCanvas) {
|
||||
if (!maskCanvas) return fitCanvas(processedCanvas, processedCanvas.width || 1, processedCanvas.height || 1);
|
||||
if (!maskCanvas) return processedCanvas;
|
||||
const output = fitCanvas(baseCanvas, processedCanvas.width || 1, processedCanvas.height || 1);
|
||||
const octx = output.getContext("2d");
|
||||
octx.imageSmoothingEnabled = true;
|
||||
|
||||
+6
-32
@@ -1,5 +1,5 @@
|
||||
import { getInputLink, getUpstreamNode, detectSource } from "./graph.js";
|
||||
import { makeViewUrl, ensureBitmap, ensureImageElement, ensureVideoFrameCanvas, fitWithinMaxSize } from "./source.js";
|
||||
import { makeViewUrl, ensureBitmap, ensureImageElement, ensureVideoFrameCanvas, fitWithinMaxSize, renderImageSourceToCanvas } from "./source.js";
|
||||
function buildRenderer({ api, registry, canvasSize }) {
|
||||
const MAX_RECURSION = 64;
|
||||
function hashString(value) {
|
||||
@@ -29,13 +29,7 @@ function buildRenderer({ api, registry, canvasSize }) {
|
||||
if (media.readyState < 2) return null;
|
||||
}
|
||||
const dims = fitWithinMaxSize(media.videoWidth || 1, media.videoHeight || 1, canvasSize);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = dims.width;
|
||||
canvas.height = dims.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(media, 0, 0, dims.width, dims.height);
|
||||
return canvas;
|
||||
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
|
||||
}
|
||||
if (media instanceof HTMLImageElement) {
|
||||
if (!media.complete) {
|
||||
@@ -46,23 +40,11 @@ function buildRenderer({ api, registry, canvasSize }) {
|
||||
if (!media.complete) return null;
|
||||
}
|
||||
const dims = fitWithinMaxSize(media.naturalWidth || media.width || 1, media.naturalHeight || media.height || 1, canvasSize);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = dims.width;
|
||||
canvas.height = dims.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(media, 0, 0, dims.width, dims.height);
|
||||
return canvas;
|
||||
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
|
||||
}
|
||||
if (media instanceof HTMLCanvasElement) {
|
||||
const dims = fitWithinMaxSize(media.width || 1, media.height || 1, canvasSize);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = dims.width;
|
||||
canvas.height = dims.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(media, 0, 0, dims.width, dims.height);
|
||||
return canvas;
|
||||
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -102,11 +84,7 @@ function buildRenderer({ api, registry, canvasSize }) {
|
||||
return null;
|
||||
}
|
||||
const dims = fitWithinMaxSize(img.naturalWidth || img.width || 1, img.naturalHeight || img.height || 1, ctx.canvasSize);
|
||||
c = document.createElement("canvas");
|
||||
c.width = dims.width;
|
||||
c.height = dims.height;
|
||||
const cctx = c.getContext("2d");
|
||||
if (cctx) cctx.drawImage(img, 0, 0, dims.width, dims.height);
|
||||
c = renderImageSourceToCanvas(node, img, dims.width, dims.height, "animatedImageCanvas");
|
||||
} else if (src.kind === "image") {
|
||||
const bmp = await ensureBitmap(node, url);
|
||||
if (!bmp) {
|
||||
@@ -114,11 +92,7 @@ function buildRenderer({ api, registry, canvasSize }) {
|
||||
return null;
|
||||
}
|
||||
const dims = fitWithinMaxSize(bmp.width || 1, bmp.height || 1, ctx.canvasSize);
|
||||
c = document.createElement("canvas");
|
||||
c.width = dims.width;
|
||||
c.height = dims.height;
|
||||
const cctx = c.getContext("2d");
|
||||
if (cctx) cctx.drawImage(bmp, 0, 0, dims.width, dims.height);
|
||||
c = renderImageSourceToCanvas(node, bmp, dims.width, dims.height, "imageCanvas");
|
||||
} else {
|
||||
c = await ensureVideoFrameCanvas(node, url, ctx.canvasSize);
|
||||
}
|
||||
|
||||
+23
-4
@@ -31,6 +31,25 @@ function fitWithinMaxSize(width, height, maxSize) {
|
||||
height: Math.max(1, Math.round(safeHeight * scale))
|
||||
};
|
||||
}
|
||||
function ensureCanvasSize(canvas, width, height) {
|
||||
const targetWidth = Math.max(1, Math.round(width || 1));
|
||||
const targetHeight = Math.max(1, Math.round(height || 1));
|
||||
const next = canvas ?? document.createElement("canvas");
|
||||
if (next.width !== targetWidth) next.width = targetWidth;
|
||||
if (next.height !== targetHeight) next.height = targetHeight;
|
||||
return next;
|
||||
}
|
||||
function renderImageSourceToCanvas(node, source, width, height, slot) {
|
||||
node.__imageops_media ?? (node.__imageops_media = {});
|
||||
const st = node.__imageops_media;
|
||||
const canvas = ensureCanvasSize(st[slot], width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return canvas;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
||||
st[slot] = canvas;
|
||||
return canvas;
|
||||
}
|
||||
async function ensureImageElement(node, url) {
|
||||
node.__imageops_media ?? (node.__imageops_media = {});
|
||||
const st = node.__imageops_media;
|
||||
@@ -83,14 +102,13 @@ async function ensureVideoFrameCanvas(node, url, size) {
|
||||
}
|
||||
const v = st.videoEl;
|
||||
const { width, height } = fitWithinMaxSize(v.videoWidth || size, v.videoHeight || size, size);
|
||||
const c = document.createElement("canvas");
|
||||
c.width = width;
|
||||
c.height = height;
|
||||
const c = ensureCanvasSize(st.videoCanvas, width, height);
|
||||
const ctx = c.getContext("2d");
|
||||
if (!ctx) return c;
|
||||
if (v.readyState < 2) return c;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.drawImage(v, 0, 0, width, height);
|
||||
st.videoCanvas = c;
|
||||
return c;
|
||||
}
|
||||
export {
|
||||
@@ -99,5 +117,6 @@ export {
|
||||
ensureVideoFrameCanvas,
|
||||
fitWithinMaxSize,
|
||||
makeViewUrl,
|
||||
parseAnnotated
|
||||
parseAnnotated,
|
||||
renderImageSourceToCanvas
|
||||
};
|
||||
|
||||
+160
-30
@@ -71,6 +71,10 @@ function ensureState(node: ComfyNode): NodeState {
|
||||
rafId: null,
|
||||
debounceTimer: null,
|
||||
lastKey: null,
|
||||
lastRenderTick: null,
|
||||
renderInFlight: false,
|
||||
queuedRenderTick: null,
|
||||
renderNonce: 0,
|
||||
isPreview: isPreviewNode(node),
|
||||
nativeAnimated: false,
|
||||
nativeDirty: false,
|
||||
@@ -114,6 +118,53 @@ function ensureState(node: ComfyNode): NodeState {
|
||||
return node.__imageops_state!;
|
||||
}
|
||||
|
||||
function hashText(value: string): string {
|
||||
let hash = 2166136261;
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
hash ^= value.charCodeAt(index);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
|
||||
function serializePreviewValue(value: unknown): string {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "string") {
|
||||
return value.length > 120 ? `str:${value.length}:${hashText(value)}` : `str:${value}`;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (Array.isArray(value)) {
|
||||
return `arr:${value.length}:${value.map((entry) => serializePreviewValue(entry)).join(",")}`;
|
||||
}
|
||||
try {
|
||||
const json = JSON.stringify(value);
|
||||
if (!json) return String(value);
|
||||
return json.length > 120 ? `json:${json.length}:${hashText(json)}` : `json:${json}`;
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function buildPreviewRenderKey(node: ComfyNode, tick: number, st: NodeState): string {
|
||||
const parts: string[] = [
|
||||
String(node.id),
|
||||
String(node.comfyClass ?? ""),
|
||||
`tick:${tick}`,
|
||||
`dirty:${st.nativeDirty ? 1 : 0}`,
|
||||
`anim:${st.nativeAnimated ? 1 : 0}`,
|
||||
`imageIndex:${typeof node.imageIndex === "number" ? node.imageIndex : -1}`,
|
||||
`imgs:${Array.isArray(node.imgs) ? node.imgs.length : 0}`,
|
||||
];
|
||||
for (const widget of (node.widgets ?? [])) {
|
||||
parts.push(`${widget?.name ?? "widget"}=${serializePreviewValue(widget?.value)}`);
|
||||
}
|
||||
for (let index = 0; index < (node.inputs?.length ?? 0); index++) {
|
||||
const input = node.inputs?.[index];
|
||||
parts.push(`in${index}:${input?.name ?? ""}:${input?.link ?? "null"}`);
|
||||
}
|
||||
return parts.join("|");
|
||||
}
|
||||
|
||||
function stopRAF(st: NodeState): void {
|
||||
if (st?.rafId) {
|
||||
cancelAnimationFrame(st.rafId);
|
||||
@@ -1138,31 +1189,50 @@ function tryRenderNativePreview(node: ComfyNode, st: NodeState, canvasSize: numb
|
||||
|
||||
const sourceWidth = img.naturalWidth || img.width || 1;
|
||||
const sourceHeight = img.naturalHeight || img.height || 1;
|
||||
const frame = document.createElement("canvas");
|
||||
frame.width = sourceWidth;
|
||||
frame.height = sourceHeight;
|
||||
const frameCtx = frame.getContext("2d");
|
||||
if (!frameCtx) return false;
|
||||
|
||||
frameCtx.clearRect(0, 0, sourceWidth, sourceHeight);
|
||||
frameCtx.drawImage(img, 0, 0, sourceWidth, sourceHeight);
|
||||
blit(node, st, frame, canvasSize);
|
||||
blit(node, st, img, canvasSize, sourceWidth, sourceHeight);
|
||||
st.info!.textContent = st.nativeAnimated ? "Node preview (animated)" : "Node preview";
|
||||
return true;
|
||||
}
|
||||
|
||||
function blit(node: ComfyNode, st: NodeState, imgCanvas: HTMLCanvasElement, canvasSize: number): void {
|
||||
function blit(
|
||||
node: ComfyNode,
|
||||
st: NodeState,
|
||||
source: CanvasImageSource,
|
||||
canvasSize: number,
|
||||
sourceWidth?: number,
|
||||
sourceHeight?: number,
|
||||
): void {
|
||||
if (!st.canvas) return;
|
||||
const ctx = st.canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
st.canvas.width = canvasSize;
|
||||
st.canvas.height = canvasSize;
|
||||
drawFitSource(ctx, canvasSize, canvasSize, imgCanvas, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
if (st.canvas.width !== canvasSize) st.canvas.width = canvasSize;
|
||||
if (st.canvas.height !== canvasSize) st.canvas.height = canvasSize;
|
||||
const resolvedWidth = Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
sourceWidth
|
||||
?? (source as HTMLImageElement).naturalWidth
|
||||
?? (source as HTMLVideoElement).videoWidth
|
||||
?? (source as HTMLCanvasElement).width
|
||||
?? 1,
|
||||
),
|
||||
);
|
||||
const resolvedHeight = Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
sourceHeight
|
||||
?? (source as HTMLImageElement).naturalHeight
|
||||
?? (source as HTMLVideoElement).videoHeight
|
||||
?? (source as HTMLCanvasElement).height
|
||||
?? 1,
|
||||
),
|
||||
);
|
||||
drawFitSource(ctx, canvasSize, canvasSize, source, resolvedWidth, resolvedHeight);
|
||||
if (isDrawNode(node)) {
|
||||
const fit = getFitPlacement(canvasSize, canvasSize, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
const fit = getFitPlacement(canvasSize, canvasSize, resolvedWidth, resolvedHeight);
|
||||
st.drawGeometry = {
|
||||
sourceWidth: imgCanvas.width || 1,
|
||||
sourceHeight: imgCanvas.height || 1,
|
||||
sourceWidth: resolvedWidth,
|
||||
sourceHeight: resolvedHeight,
|
||||
fitDx: fit.dx,
|
||||
fitDy: fit.dy,
|
||||
fitDrawWidth: fit.drawWidth,
|
||||
@@ -1171,9 +1241,9 @@ function blit(node: ComfyNode, st: NodeState, imgCanvas: HTMLCanvasElement, canv
|
||||
} else {
|
||||
st.drawGeometry = null;
|
||||
}
|
||||
st.cropGeometry = drawCropBounds(node, ctx, canvasSize, canvasSize, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
drawTransformBounds(node, ctx, canvasSize, canvasSize, imgCanvas.width || 1, imgCanvas.height || 1);
|
||||
drawCompBounds(node, ctx, canvasSize, canvasSize, st.compOutputWidth || imgCanvas.width || 1, st.compOutputHeight || imgCanvas.height || 1, st.compLayers);
|
||||
st.cropGeometry = drawCropBounds(node, ctx, canvasSize, canvasSize, resolvedWidth, resolvedHeight);
|
||||
drawTransformBounds(node, ctx, canvasSize, canvasSize, resolvedWidth, resolvedHeight);
|
||||
drawCompBounds(node, ctx, canvasSize, canvasSize, st.compOutputWidth || resolvedWidth, st.compOutputHeight || resolvedHeight, st.compLayers);
|
||||
}
|
||||
|
||||
function getCropInfoText(node: ComfyNode): string {
|
||||
@@ -1996,16 +2066,53 @@ export function registerImageOpsLivePreview(): void {
|
||||
function renderNode(node: ComfyNode, tick: number = 0): void {
|
||||
const st = ensurePreviewWidget(node, progress, canvasSize);
|
||||
if (!st) return;
|
||||
const renderKey = buildPreviewRenderKey(node, tick, st);
|
||||
|
||||
if (!st.nativeDirty && st.lastKey === renderKey && st.lastRenderTick === tick) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (st.renderInFlight) {
|
||||
st.queuedRenderTick = tick;
|
||||
return;
|
||||
}
|
||||
|
||||
const finishRender = (): void => {
|
||||
st.renderInFlight = false;
|
||||
const queuedTick = st.queuedRenderTick;
|
||||
st.queuedRenderTick = null;
|
||||
if (queuedTick != null) {
|
||||
renderNode(node, queuedTick);
|
||||
}
|
||||
};
|
||||
|
||||
const commitRender = (): void => {
|
||||
st.lastKey = renderKey;
|
||||
st.lastRenderTick = tick;
|
||||
st.nativeDirty = false;
|
||||
};
|
||||
|
||||
const failRender = (message: string, error: unknown): void => {
|
||||
st.info!.textContent = message;
|
||||
console.warn("[ImageOps] render error", error);
|
||||
finishRender();
|
||||
};
|
||||
|
||||
st.renderInFlight = true;
|
||||
st.renderNonce += 1;
|
||||
|
||||
if (isPreviewNode(node)) {
|
||||
if (isGraphTooLarge(node?.graph, cfg.maxGraphNodes)) {
|
||||
st.info!.textContent = "Live preview disabled: graph too large";
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
renderPreviewBridgeNode(node, tick).catch((err) => {
|
||||
st.info!.textContent = "Preview bridge error (check console)";
|
||||
console.warn("[ImageOps] preview bridge render error", err);
|
||||
renderPreviewBridgeNode(node, tick).then(() => {
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
failRender("Preview bridge error (check console)", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2014,12 +2121,15 @@ export function registerImageOpsLivePreview(): void {
|
||||
if (isGraphTooLarge(node?.graph, cfg.maxGraphNodes)) {
|
||||
st.info!.textContent = "Live preview disabled: graph too large";
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
renderDrawNode(node, tick).catch((err) => {
|
||||
st.info!.textContent = "Draw preview error (check console)";
|
||||
console.warn("[ImageOps] draw render error", err);
|
||||
renderDrawNode(node, tick).then(() => {
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
failRender("Draw preview error (check console)", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2029,6 +2139,7 @@ export function registerImageOpsLivePreview(): void {
|
||||
st.info!.textContent = "Live preview disabled: graph too large";
|
||||
st.cropGeometry = null;
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2036,6 +2147,7 @@ export function registerImageOpsLivePreview(): void {
|
||||
if (!upstream) {
|
||||
st.info!.textContent = "Live preview: connect a supported loader/chain";
|
||||
st.cropGeometry = null;
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2043,14 +2155,16 @@ export function registerImageOpsLivePreview(): void {
|
||||
if (!result?.canvas) {
|
||||
st.info!.textContent = "Live preview: connect a supported loader/chain";
|
||||
st.cropGeometry = null;
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
blit(node, st, result.canvas, canvasSize);
|
||||
st.info!.textContent = getCropInfoText(node);
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch(err => {
|
||||
st.info!.textContent = "Live preview error (check console)";
|
||||
st.cropGeometry = null;
|
||||
console.warn("[ImageOps] render error", err);
|
||||
failRender("Live preview error (check console)", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2060,6 +2174,7 @@ export function registerImageOpsLivePreview(): void {
|
||||
st.info!.textContent = "Live preview disabled: graph too large";
|
||||
st.compLayers = [];
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2083,6 +2198,7 @@ export function registerImageOpsLivePreview(): void {
|
||||
st.compOutputWidth = Math.max(1, Math.round(widgetNumber(node, "width", 1024)));
|
||||
st.compOutputHeight = Math.max(1, Math.round(widgetNumber(node, "height", 1024)));
|
||||
updateCompControls(node);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
const rendered = renderCompPreview(node, ordered);
|
||||
@@ -2092,27 +2208,34 @@ export function registerImageOpsLivePreview(): void {
|
||||
blit(node, st, rendered.canvas, canvasSize);
|
||||
st.info!.textContent = getCompInfoText(node, ordered.length, slots.length, st.compOutputWidth, st.compOutputHeight);
|
||||
updateCompControls(node);
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch((err) => {
|
||||
st.info!.textContent = "Comp preview error (check console)";
|
||||
st.compLayers = [];
|
||||
console.warn("[ImageOps] comp render error", err);
|
||||
finishRender();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryRenderNativePreview(node, st, canvasSize)) {
|
||||
commitRender();
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGraphTooLarge(node?.graph, cfg.maxGraphNodes)) {
|
||||
st.info!.textContent = "Live preview disabled: graph too large";
|
||||
stopRAF(st);
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.render(node, tick).then(result => {
|
||||
if (!result?.canvas) {
|
||||
st.info!.textContent = "Live preview: connect a supported loader/chain";
|
||||
finishRender();
|
||||
return;
|
||||
}
|
||||
blit(node, st, result.canvas, canvasSize);
|
||||
@@ -2126,9 +2249,10 @@ export function registerImageOpsLivePreview(): void {
|
||||
} else {
|
||||
st.info!.textContent = "Live preview (no queue)";
|
||||
}
|
||||
commitRender();
|
||||
finishRender();
|
||||
}).catch(err => {
|
||||
st.info!.textContent = "Live preview error (check console)";
|
||||
console.warn("[ImageOps] render error", err);
|
||||
failRender("Live preview error (check console)", err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2151,6 +2275,7 @@ export function registerImageOpsLivePreview(): void {
|
||||
}
|
||||
|
||||
let tick = 0;
|
||||
let lastLoopTick: number | null = null;
|
||||
const proceduralFrameCount = getProceduralFrameCount(node);
|
||||
const proceduralFps = proceduralFrameCount != null ? (getProceduralPlaybackFps(node) ?? 12) : null;
|
||||
const startedAt = performance.now();
|
||||
@@ -2161,7 +2286,10 @@ export function registerImageOpsLivePreview(): void {
|
||||
} else {
|
||||
tick++;
|
||||
}
|
||||
renderNode(node, tick);
|
||||
if (tick !== lastLoopTick) {
|
||||
lastLoopTick = tick;
|
||||
renderNode(node, tick);
|
||||
}
|
||||
st.rafId = requestAnimationFrame(loop);
|
||||
};
|
||||
stopRAF(st);
|
||||
@@ -2271,6 +2399,8 @@ export function registerImageOpsLivePreview(): void {
|
||||
const r = origExecuted?.apply(this, arguments as any);
|
||||
st.nativeAnimated = !!message?.animated?.[0];
|
||||
st.nativeDirty = false;
|
||||
st.lastKey = null;
|
||||
st.lastRenderTick = null;
|
||||
schedule(node, () => {
|
||||
if (IMAGEOPS_CLASSES.has(node.comfyClass)) startLoopIfVideo(node);
|
||||
refreshDependents(node);
|
||||
|
||||
+5
-3
@@ -514,7 +514,9 @@ function distortConnectedInputs(
|
||||
}
|
||||
|
||||
function extractCanvasField(canvas: HTMLCanvasElement, width: number, height: number, channel: string): Float32Array {
|
||||
const fitted = fitCanvas(canvas, width, height);
|
||||
const fitted = (canvas.width || 1) === width && (canvas.height || 1) === height
|
||||
? canvas
|
||||
: fitCanvas(canvas, width, height);
|
||||
const data = fitted.getContext("2d")!.getImageData(0, 0, width, height).data;
|
||||
const field = new Float32Array(width * height);
|
||||
const normalized = String(channel || "red").toLowerCase();
|
||||
@@ -619,7 +621,7 @@ function renderDistortCanvas(
|
||||
yField = extractCanvasField(driver, width, height, strAny(node, ["y_channel"], "Green", frameIndex));
|
||||
}
|
||||
|
||||
const sourceCanvas = fitCanvas(source, width, height);
|
||||
const sourceCanvas = source;
|
||||
const sourceCtx = sourceCanvas.getContext("2d")!;
|
||||
const sourceData = sourceCtx.getImageData(0, 0, width, height);
|
||||
const output = makeCanvas(width, height);
|
||||
@@ -1167,7 +1169,7 @@ function compositeProcessedWithMask(
|
||||
processedCanvas: HTMLCanvasElement,
|
||||
maskCanvas: HTMLCanvasElement | null,
|
||||
): HTMLCanvasElement {
|
||||
if (!maskCanvas) return fitCanvas(processedCanvas, processedCanvas.width || 1, processedCanvas.height || 1);
|
||||
if (!maskCanvas) return processedCanvas;
|
||||
const output = fitCanvas(baseCanvas, processedCanvas.width || 1, processedCanvas.height || 1);
|
||||
const octx = output.getContext("2d")!;
|
||||
octx.imageSmoothingEnabled = true;
|
||||
|
||||
+12
-38
@@ -1,7 +1,7 @@
|
||||
// Renderer (recursive, supports interop) (v6)
|
||||
import type { ComfyNode, ComfyAPI, AdapterRegistry, RenderContext, RenderInputInfo, RenderResult } from "../types.js";
|
||||
import { getInputLink, getUpstreamNode, detectSource } from "./graph.js";
|
||||
import { makeViewUrl, ensureBitmap, ensureImageElement, ensureVideoFrameCanvas, fitWithinMaxSize } from "./source.js";
|
||||
import { makeViewUrl, ensureBitmap, ensureImageElement, ensureVideoFrameCanvas, fitWithinMaxSize, renderImageSourceToCanvas } from "./source.js";
|
||||
|
||||
interface RendererConfig {
|
||||
api: ComfyAPI;
|
||||
@@ -38,20 +38,14 @@ export function buildRenderer({ api, registry, canvasSize }: RendererConfig): Re
|
||||
const media = getNativePreviewElement(node);
|
||||
if (!media) return null;
|
||||
|
||||
if (media instanceof HTMLVideoElement) {
|
||||
if (media.readyState < 2) {
|
||||
try { await media.play(); } catch {}
|
||||
if (media.readyState < 2) return null;
|
||||
if (media instanceof HTMLVideoElement) {
|
||||
if (media.readyState < 2) {
|
||||
try { await media.play(); } catch {}
|
||||
if (media.readyState < 2) return null;
|
||||
}
|
||||
const dims = fitWithinMaxSize(media.videoWidth || 1, media.videoHeight || 1, canvasSize);
|
||||
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
|
||||
}
|
||||
const dims = fitWithinMaxSize(media.videoWidth || 1, media.videoHeight || 1, canvasSize);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = dims.width;
|
||||
canvas.height = dims.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(media, 0, 0, dims.width, dims.height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
if (media instanceof HTMLImageElement) {
|
||||
if (!media.complete) {
|
||||
@@ -59,24 +53,12 @@ export function buildRenderer({ api, registry, canvasSize }: RendererConfig): Re
|
||||
if (!media.complete) return null;
|
||||
}
|
||||
const dims = fitWithinMaxSize(media.naturalWidth || media.width || 1, media.naturalHeight || media.height || 1, canvasSize);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = dims.width;
|
||||
canvas.height = dims.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(media, 0, 0, dims.width, dims.height);
|
||||
return canvas;
|
||||
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
|
||||
}
|
||||
|
||||
if (media instanceof HTMLCanvasElement) {
|
||||
const dims = fitWithinMaxSize(media.width || 1, media.height || 1, canvasSize);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = dims.width;
|
||||
canvas.height = dims.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(media, 0, 0, dims.width, dims.height);
|
||||
return canvas;
|
||||
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -118,20 +100,12 @@ export function buildRenderer({ api, registry, canvasSize }: RendererConfig): Re
|
||||
const img = await ensureImageElement(node, url);
|
||||
if (!img) { ctx.visited.delete(node.id); return null; }
|
||||
const dims = fitWithinMaxSize(img.naturalWidth || img.width || 1, img.naturalHeight || img.height || 1, ctx.canvasSize);
|
||||
c = document.createElement("canvas");
|
||||
c.width = dims.width;
|
||||
c.height = dims.height;
|
||||
const cctx = c.getContext("2d");
|
||||
if (cctx) cctx.drawImage(img, 0, 0, dims.width, dims.height);
|
||||
c = renderImageSourceToCanvas(node, img, dims.width, dims.height, "animatedImageCanvas");
|
||||
} else if (src.kind === "image") {
|
||||
const bmp = await ensureBitmap(node, url);
|
||||
if (!bmp) { ctx.visited.delete(node.id); return null; }
|
||||
const dims = fitWithinMaxSize(bmp.width || 1, bmp.height || 1, ctx.canvasSize);
|
||||
c = document.createElement("canvas");
|
||||
c.width = dims.width;
|
||||
c.height = dims.height;
|
||||
const cctx = c.getContext("2d");
|
||||
if (cctx) cctx.drawImage(bmp, 0, 0, dims.width, dims.height);
|
||||
c = renderImageSourceToCanvas(node, bmp, dims.width, dims.height, "imageCanvas");
|
||||
} else {
|
||||
c = await ensureVideoFrameCanvas(node, url, ctx.canvasSize);
|
||||
}
|
||||
|
||||
+29
-3
@@ -39,6 +39,33 @@ export function fitWithinMaxSize(width: number, height: number, maxSize: number)
|
||||
};
|
||||
}
|
||||
|
||||
function ensureCanvasSize(canvas: HTMLCanvasElement | undefined, width: number, height: number): HTMLCanvasElement {
|
||||
const targetWidth = Math.max(1, Math.round(width || 1));
|
||||
const targetHeight = Math.max(1, Math.round(height || 1));
|
||||
const next = canvas ?? document.createElement("canvas");
|
||||
if (next.width !== targetWidth) next.width = targetWidth;
|
||||
if (next.height !== targetHeight) next.height = targetHeight;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function renderImageSourceToCanvas(
|
||||
node: ComfyNode,
|
||||
source: CanvasImageSource,
|
||||
width: number,
|
||||
height: number,
|
||||
slot: "imageCanvas" | "animatedImageCanvas" | "videoCanvas" | "nativeCanvas",
|
||||
): HTMLCanvasElement {
|
||||
node.__imageops_media ??= {} as MediaState;
|
||||
const st = node.__imageops_media!;
|
||||
const canvas = ensureCanvasSize(st[slot], width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return canvas;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
||||
st[slot] = canvas;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export async function ensureImageElement(node: ComfyNode, url: string): Promise<HTMLImageElement | null> {
|
||||
node.__imageops_media ??= {} as MediaState;
|
||||
const st = node.__imageops_media!;
|
||||
@@ -90,9 +117,7 @@ export async function ensureVideoFrameCanvas(node: ComfyNode, url: string, size:
|
||||
|
||||
const v = st.videoEl!;
|
||||
const { width, height } = fitWithinMaxSize(v.videoWidth || size, v.videoHeight || size, size);
|
||||
const c = document.createElement("canvas");
|
||||
c.width = width;
|
||||
c.height = height;
|
||||
const c = ensureCanvasSize(st.videoCanvas, width, height);
|
||||
const ctx = c.getContext("2d");
|
||||
if (!ctx) return c;
|
||||
|
||||
@@ -100,5 +125,6 @@ export async function ensureVideoFrameCanvas(node: ComfyNode, url: string, size:
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.drawImage(v, 0, 0, width, height);
|
||||
st.videoCanvas = c;
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,10 @@ export interface NodeState {
|
||||
rafId: number | null;
|
||||
debounceTimer: ReturnType<typeof setTimeout> | null;
|
||||
lastKey: string | null;
|
||||
lastRenderTick: number | null;
|
||||
renderInFlight: boolean;
|
||||
queuedRenderTick: number | null;
|
||||
renderNonce: number;
|
||||
isPreview: boolean;
|
||||
nativeAnimated: boolean;
|
||||
nativeDirty: boolean;
|
||||
@@ -281,6 +285,10 @@ export interface MediaState {
|
||||
lastBitmap?: ImageBitmap;
|
||||
videoEl?: HTMLVideoElement;
|
||||
lastVideoURL?: string;
|
||||
imageCanvas?: HTMLCanvasElement;
|
||||
animatedImageCanvas?: HTMLCanvasElement;
|
||||
videoCanvas?: HTMLCanvasElement;
|
||||
nativeCanvas?: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
// ── Media / source ──
|
||||
|
||||
Reference in New Issue
Block a user