Wan_Backup / custom_nodes /ComfyUI-Easy-Use /ComfyUI-Easy-Use-Frontend /src /composable /drawBackground.js
| import { app, api} from './comfyAPI.js' | |
| import {useNodeCanvasImagePreview} from "@/composable/node/canvasImagePreview.js"; | |
| import {useNodeVideoPreview} from "@/composable/node/videoPreview.js"; | |
| const IMAGE_NODE_PROPERTY = 'image_upload' | |
| const VIDEO_NODE_PROPERTY = 'video_upload' | |
| const getNodeData = (node) => node.constructor?.nodeData | |
| const getInputSpecsFromData = (inputData) =>{ | |
| if (!inputData) return [] | |
| const { required, optional } = inputData | |
| const inputSpecs = [] | |
| if (required) { | |
| for (const value of Object.values(required)) { | |
| inputSpecs.push(value) | |
| } | |
| } | |
| if (optional) { | |
| for (const value of Object.values(optional)) { | |
| inputSpecs.push(value) | |
| } | |
| } | |
| return inputSpecs | |
| } | |
| const hasInputProperty = (node, property | |
| ) => { | |
| if (!node) return false | |
| const nodeData = getNodeData(node) | |
| if (!nodeData?.input) return false | |
| const inputs = getInputSpecsFromData(nodeData.input) | |
| return inputs.some((input) => input?.[1]?.[property]) | |
| } | |
| const hasImageElements = imgs => Array.isArray(imgs) && imgs.some(img => img instanceof HTMLImageElement) | |
| export function isImageNode(node) { | |
| if (!node) return false | |
| if (node.imgs?.length && hasImageElements(node.imgs)) return true | |
| if (!node.widgets) return false | |
| return hasInputProperty(node, IMAGE_NODE_PROPERTY) | |
| } | |
| export function isVideoNode(node) { | |
| if (!node) return false | |
| if (node.videoContainer) return true | |
| if (!node.widgets) return false | |
| return hasInputProperty(node, VIDEO_NODE_PROPERTY) | |
| } | |
| const getNodeId = (node) => node?.id.toString() | |
| const getNodeOutputs = (node) => { | |
| return app.nodeOutputs[getNodeId(node)] | |
| } | |
| const getNodePreviews = (node) => { | |
| return app.nodePreviewImages[getNodeId(node)] | |
| } | |
| const getPreviewParam = (node) => { | |
| if (node.animatedImages) return '' | |
| return app.getPreviewFormatParam() | |
| } | |
| function getNodeImageUrls(node) { | |
| const previews = getNodePreviews(node) | |
| const outputs = getNodeOutputs(node) | |
| // If the node is running, return the previews | |
| if (previews?.length && !node.isOutputFinal) return previews | |
| if (!outputs?.images?.length) { | |
| if(previews?.length) return previews | |
| return | |
| } | |
| const rand = app.getRandParam() | |
| const previewParam = getPreviewParam(node) | |
| return outputs.images.map((image) => { | |
| const imgUrlPart = new URLSearchParams(image) | |
| return api.apiURL(`/view?${imgUrlPart}${previewParam}${rand}`) | |
| }) | |
| } | |
| const VIDEO_WIDGET_NAME = 'video-preview' | |
| const VIDEO_DEFAULT_OPTIONS = { | |
| playsInline: true, | |
| controls: true, | |
| loop: true | |
| } | |
| const MEDIA_LOAD_TIMEOUT = 8192 | |
| const MAX_RETRIES = 1 | |
| const VIDEO_PIXEL_OFFSET = 64 | |
| const createContainer = () => { | |
| const container = document.createElement('div') | |
| container.classList.add('comfy-img-preview') | |
| return container | |
| } | |
| const createTimeout = (ms) => new Promise((resolve) => setTimeout(() => resolve(null), ms)) | |
| const getVideoRealURL = obj => { | |
| return api.apiURL(`/view?filename=${encodeURIComponent(obj.filename)}&type=${obj.type}&subfolder=${obj.subfolder}&rand=${Math.random()}`) | |
| } | |
| export const useNodePreview = (node, options) => { | |
| const { loadElement, onLoaded, onFailedLoading } = options | |
| const loadElementWithTimeout = async (url, retryCount = 0) => { | |
| const result = await Promise.race([ | |
| loadElement(url), | |
| createTimeout(MEDIA_LOAD_TIMEOUT) | |
| ]) | |
| if (result === null && retryCount < MAX_RETRIES) { | |
| return loadElementWithTimeout(url, retryCount + 1) | |
| } | |
| return result | |
| } | |
| const loadElements = async (urls) => | |
| Promise.all(urls.map((url) => loadElementWithTimeout(url))) | |
| // const render = () => { | |
| // node.graph?.setDirtyCanvas(true) | |
| // } | |
| /** | |
| * Displays media element(s) on the node. | |
| */ | |
| function showPreview() { | |
| if (node.isLoading) return | |
| const outputUrls = node.videos?.length>0 ? node.videos.map(cate=> getVideoRealURL(cate)) : getNodeImageUrls(node) | |
| if (!outputUrls?.length) return | |
| if (options?.block) node.isLoading = true | |
| loadElements(outputUrls) | |
| .then((elements) => { | |
| const validElements = elements.filter( | |
| (el) => el !== null | |
| ) | |
| if (validElements.length) { | |
| onLoaded?.(validElements) | |
| // render() | |
| } | |
| }) | |
| .catch(() => { | |
| onFailedLoading?.() | |
| }) | |
| .finally(() => { | |
| node.isLoading = false | |
| }) | |
| } | |
| return { | |
| showPreview | |
| } | |
| } | |
| export const useNodeImage = (node) => { | |
| node.previewMediaType = 'image' | |
| const loadElement = (url) => | |
| new Promise((resolve) => { | |
| const img = new Image() | |
| img.onload = () => resolve(img) | |
| img.onerror = () => resolve(null) | |
| img.src = url | |
| }) | |
| const onLoaded = (elements) => { | |
| node.imageIndex = null | |
| node.imgs = elements | |
| } | |
| return useNodePreview(node, { | |
| loadElement, | |
| onLoaded, | |
| onFailedLoading: () => { | |
| node.imgs = undefined | |
| } | |
| }) | |
| } | |
| export const useNodeVideo = (node) => { | |
| node.previewMediaType = 'video' | |
| const loadElement = (url) => | |
| new Promise((resolve) => { | |
| const video = document.createElement('video') | |
| Object.assign(video, VIDEO_DEFAULT_OPTIONS) | |
| video.onloadeddata = () => resolve(video) | |
| video.onerror = () => resolve(null) | |
| video.src = url | |
| }) | |
| const addVideoDomWidget = (container) => { | |
| const hasWidget = node.widgets?.some((w) => w.name === VIDEO_WIDGET_NAME) | |
| if (!hasWidget) { | |
| node.addDOMWidget(VIDEO_WIDGET_NAME, 'video', container, { | |
| hideOnZoom: false, | |
| serialize: false | |
| }) | |
| } | |
| } | |
| const onLoaded = (videoElements) => { | |
| const videoElement = videoElements[0] | |
| if (!videoElement) return | |
| if (!node.videoContainer) { | |
| if(node.imgs) node.imgs = undefined | |
| node.videoContainer = createContainer() | |
| node.videoContainer.style.pointerEvents = 'auto' | |
| addVideoDomWidget(node.videoContainer) | |
| } | |
| node.videoContainer.replaceChildren(videoElement) | |
| node.imageOffset = VIDEO_PIXEL_OFFSET | |
| } | |
| return useNodePreview(node, { | |
| loadElement, | |
| onLoaded, | |
| onFailedLoading: () => { | |
| node.videoContainer = undefined | |
| } | |
| }) | |
| } | |
| export function unsafeDrawBackground(_this, ctx) { | |
| if (_this.flags.collapsed) return | |
| const { showCanvasImagePreview, removeCanvasImagePreview } = useNodeCanvasImagePreview() | |
| const { showVideoPreview, removeVideoPreview} = useNodeVideoPreview() | |
| const output = getNodeOutputs(_this) | |
| const preview = getNodePreviews(_this) | |
| const isNewOutput = output && (_this.images !== output.images || _this.videos !== output.videos) | |
| const isNewPreview = preview && _this.preview !== preview | |
| if (isNewPreview) { | |
| _this.isOutputFinal = false | |
| if(_this.isTwiceRendered) _this.isTwiceRendered = false | |
| _this.preview = preview | |
| } | |
| if (isNewOutput) { | |
| _this.isOutputFinal = true | |
| if(output.images) _this.images = output.images | |
| if(output.videos) _this.videos = output.videos | |
| } | |
| if (isNewOutput || isNewPreview) { | |
| _this.animatedImages = output?.animated | |
| const isAnimatedWebp = _this.animatedImages && output.images.some((img) => img.filename?.includes('webp')) | |
| const isVideo = (_this.animatedImages && !isAnimatedWebp) || isVideoNode(this) | |
| if (isNewOutput && output?.videos){ | |
| useNodeVideo(_this).showPreview() | |
| } | |
| else if (isVideo) { | |
| useNodeVideo(_this).showPreview() | |
| } else { | |
| useNodeImage(_this).showPreview() | |
| } | |
| } | |
| // 二次渲染 | |
| if(_this.id != app.runningNodeId && !_this.isTwiceRendered && _this.isOutputFinal){ | |
| if(_this.videos) useNodeVideo(_this.videos) | |
| else useNodeImage(_this).showPreview() | |
| _this.isTwiceRendered = true | |
| } | |
| // Nothing to do | |
| if (!_this.imgs?.length) return | |
| if (_this.animatedImages || (isNewOutput && output?.videos)) { | |
| removeCanvasImagePreview(_this) | |
| showVideoPreview(_this) | |
| } else { | |
| removeVideoPreview(_this) | |
| showCanvasImagePreview(_this) | |
| } | |
| } | |