| import { app } from '../../../scripts/app.js' |
| |
| import { ComfyWidgets } from '../../../scripts/widgets.js' |
| import { $el } from '../../../scripts/ui.js' |
|
|
| let api_host = `${window.location.hostname}:${window.location.port}` |
| let api_base = '' |
| let url = `${window.location.protocol}//${api_host}${api_base}` |
|
|
| async function getQueue () { |
| try { |
| const res = await fetch(`${url}/queue`) |
| const data = await res.json() |
| |
| return { |
| |
| Running: data.queue_running.length, |
| Pending: data.queue_pending.length |
| } |
| } catch (error) { |
| console.error(error) |
| return { Running: 0, Pending: 0 } |
| } |
| } |
|
|
| async function interrupt () { |
| const resp = await fetch(`${url}/interrupt`, { |
| method: 'POST' |
| }) |
| } |
|
|
| async function clipboardWriteImage (win, url) { |
| const canvas = document.createElement('canvas') |
| const ctx = canvas.getContext('2d') |
| |
| const img = await createImage(url) |
| |
| canvas.width = img.naturalWidth |
| canvas.height = img.naturalHeight |
|
|
| ctx.clearRect(0, 0, canvas.width, canvas.height) |
| ctx.drawImage(img, 0, 0) |
| |
| canvas.toBlob(async blob => { |
| const data = [ |
| new ClipboardItem({ |
| [blob.type]: blob |
| }) |
| ] |
|
|
| win.navigator.clipboard |
| .write(data) |
| .then(() => { |
| console.log('Image copied to clipboard') |
| }) |
| .catch(error => { |
| console.error('Failed to copy image to clipboard:', error) |
| }) |
| }) |
| } |
|
|
| async function uploadFile (file) { |
| try { |
| const body = new FormData() |
| body.append('image', file) |
| body.append('overwrite', 'true') |
| body.append('type', 'temp') |
|
|
| const resp = await fetch(`${url}/upload/image`, { |
| method: 'POST', |
| body |
| }) |
|
|
| if (resp.status === 200) { |
| const data = await resp.json() |
| let path = data.name |
| if (data.subfolder) path = data.subfolder + '/' + path |
| return path |
| } else { |
| alert(resp.status + ' - ' + resp.statusText) |
| } |
| } catch (error) { |
| alert(error) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| async function shareScreen ( |
| isCamera = false, |
| webcamVideo, |
| shareBtn, |
| liveBtn, |
| previewArea |
| ) { |
| try { |
| let mediaStream |
|
|
| if (!isCamera) { |
| mediaStream = await navigator.mediaDevices.getDisplayMedia({ |
| video: true |
| }) |
| } else { |
| if (!localStorage.getItem('_mixlab_webcamera_select')) return |
| let constraints = |
| JSON.parse(localStorage.getItem('_mixlab_webcamera_select')) || {} |
| mediaStream = await navigator.mediaDevices.getUserMedia(constraints) |
| } |
|
|
| webcamVideo.removeEventListener('timeupdate', videoTimeUpdateHandler) |
| webcamVideo.srcObject = mediaStream |
| webcamVideo.onloadedmetadata = () => { |
| let x = 0, |
| y = 0, |
| width = webcamVideo.videoWidth, |
| height = webcamVideo.videoHeight, |
| imgWidth = webcamVideo.videoWidth, |
| imgHeight = webcamVideo.videoHeight |
|
|
| let d = getSetAreaData() |
| if ( |
| d && |
| d.x >= 0 && |
| d.imgWidth === imgWidth && |
| d.imgHeight === imgHeight |
| ) { |
| x = d.x |
| y = d.y |
| width = d.width |
| height = d.height |
| imgWidth = d.imgWidth |
| imgHeight = d.imgHeight |
| console.log('#screen_share::使用上一次选区') |
| } |
| updateSetAreaData(x, y, width, height, imgWidth, imgHeight) |
|
|
| webcamVideo.play() |
|
|
| createBlobFromVideo(webcamVideo, true) |
|
|
| webcamVideo.addEventListener('timeupdate', videoTimeUpdateHandler) |
|
|
| |
| |
| |
| |
| |
| } |
|
|
| mediaStream.addEventListener('inactive', handleStopSharing) |
|
|
| |
| function handleStopSharing () { |
| |
| |
| if (window._mixlab_stopVideo) { |
| window._mixlab_stopVideo() |
| window._mixlab_stopVideo = null |
| shareBtn.innerText = 'Share Screen' |
| } |
| if (window._mixlab_stopLive) { |
| window._mixlab_stopLive() |
| window._mixlab_stopLive = null |
| liveBtn.innerText = 'Live Run' |
| } |
| return |
| } |
|
|
| window._mixlab_screen_webcamVideo = webcamVideo |
|
|
| async function videoTimeUpdateHandler () { |
| if (!window._mixlab_screen_live) return |
| createBlobFromVideo(webcamVideo) |
| } |
| } catch (error) { |
| alert('Error accessing screen stream: ' + error) |
| } |
| return () => { |
| webcamVideo.pause() |
| webcamVideo.srcObject.getTracks().forEach(track => { |
| track.stop() |
| }) |
| webcamVideo.srcObject = null |
| window._mixlab_screen_live = false |
| window._mixlab_screen_blob = null |
| previewArea.innerHTML = '' |
| interrupt() |
| } |
| } |
|
|
| async function sleep (t = 200) { |
| return new Promise((res, rej) => { |
| setTimeout(() => { |
| res(true) |
| }, t) |
| }) |
| } |
|
|
| function createImage (url) { |
| let im = new Image() |
| return new Promise((res, rej) => { |
| im.onload = () => res(im) |
| im.src = url |
| }) |
| } |
|
|
| async function compareImages (threshold, previousImage, currentImage) { |
| |
| var previousImg = await createImage(previousImage) |
| var currentImg = await createImage(currentImage) |
|
|
| if ( |
| previousImg.naturalWidth != currentImg.naturalWidth || |
| previousImg.naturalHeight != currentImg.naturalHeight |
| ) { |
| return true |
| } |
|
|
| |
| var canvas1 = document.createElement('canvas') |
| canvas1.width = previousImg.naturalWidth |
| canvas1.height = previousImg.naturalHeight |
| var context1 = canvas1.getContext('2d') |
|
|
| |
| context1.drawImage(previousImg, 0, 0) |
|
|
| |
| var previousData = context1.getImageData( |
| 0, |
| 0, |
| previousImg.naturalWidth, |
| previousImg.naturalHeight |
| ).data |
|
|
| var canvas2 = document.createElement('canvas') |
| canvas2.width = currentImg.naturalWidth |
| canvas2.height = currentImg.naturalHeight |
| var context2 = canvas2.getContext('2d') |
| context2.drawImage(currentImg, 0, 0) |
| var currentData = context2.getImageData( |
| 0, |
| 0, |
| currentImg.naturalWidth, |
| currentImg.naturalHeight |
| ).data |
|
|
| |
| var pixelDiff = 0 |
| for (var i = 0; i < previousData.length; i += 4) { |
| var diffR = Math.abs(previousData[i] - currentData[i]) |
| var diffG = Math.abs(previousData[i + 1] - currentData[i + 1]) |
| var diffB = Math.abs(previousData[i + 2] - currentData[i + 2]) |
|
|
| |
| pixelDiff += diffR + diffG + diffB |
| } |
|
|
| |
| var averageDiff = pixelDiff / (previousData.length / 4) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| if (averageDiff > threshold) { |
| return true |
| } else { |
| return false |
| } |
| } |
|
|
| async function startLive (btn) { |
| if (btn) window._mixlab_screen_live = !window._mixlab_screen_live |
|
|
| if (btn) btn.innerText = `Stop Live` |
| |
| |
| |
| |
| |
| const { Pending, Running } = await getQueue() |
| |
| if (Pending <= 1 && window._mixlab_screen_blob && Running === 0) { |
| |
|
|
| const threshold = 1 |
| const previousImage = window._mixlab_screen_imagePath |
| let currentImage = await blobToBase64(window._mixlab_screen_blob) |
|
|
| if (previousImage) { |
| |
| const imageChanged = await compareImages( |
| threshold, |
| previousImage, |
| currentImage |
| ) |
| console.log('#图片是否有变化:', imageChanged) |
|
|
| if (imageChanged) { |
| window._mixlab_screen_imagePath = currentImage |
| document.querySelector('#queue-button').click() |
| } |
| } else { |
| window._mixlab_screen_imagePath = currentImage |
| |
| document.querySelector('#queue-button').click() |
| } |
|
|
| |
| |
|
|
| await sleep(window._mixlab_screen_refresh_rate || 200) |
| |
| } |
|
|
| if (btn) { |
| startLive() |
| return () => { |
| |
| window._mixlab_screen_live = false |
| window._mixlab_screen_blob = null |
| interrupt() |
| } |
| } else if (window._mixlab_screen_live) { |
| startLive() |
| } |
| } |
|
|
| async function createBlobFromVideoForArea (webcamVideo) { |
| const videoW = webcamVideo.videoWidth |
| const videoH = webcamVideo.videoHeight |
| const aspectRatio = videoW / videoH |
| const WIDTH = videoW, |
| HEIGHT = videoH |
| const canvas = new OffscreenCanvas(WIDTH, HEIGHT) |
| const ctx = canvas.getContext('2d') |
| ctx.drawImage(webcamVideo, 0, 0, videoW, videoH, 0, 0, WIDTH, HEIGHT) |
|
|
| const blob = await canvas.convertToBlob({ |
| type: 'image/jpeg', |
| quality: 1 |
| }) |
|
|
| return blob |
| } |
|
|
| async function createBlobFromVideo (webcamVideo, updateImageBase64 = false) { |
| const videoW = webcamVideo.videoWidth |
| const videoH = webcamVideo.videoHeight |
| const aspectRatio = videoW / videoH |
|
|
| const { x, y, width, height } = window._mixlab_share_screen |
|
|
| const canvas = new OffscreenCanvas(width, height) |
| const ctx = canvas.getContext('2d') |
|
|
| ctx.drawImage(webcamVideo, x, y, width, height, 0, 0, width, height) |
|
|
| const blob = await canvas.convertToBlob({ |
| type: 'image/jpeg', |
| quality: 1 |
| }) |
| |
| window._mixlab_screen_blob = blob |
|
|
| console.log( |
| '########updateImageBase64 ', |
| updateImageBase64, |
| x, |
| y, |
| width, |
| height |
| ) |
| if (updateImageBase64) { |
| window._mixlab_screen_imagePath = await blobToBase64(blob) |
| } |
| } |
|
|
| async function blobToBase64 (blob) { |
| const reader = new FileReader() |
| return new Promise((res, rej) => { |
| reader.onload = function (event) { |
| res(event.target.result) |
| } |
| reader.readAsDataURL(blob) |
| }) |
| } |
| function base64ToBlob (base64) { |
| |
| const parts = base64.split(';base64,') |
| const type = parts[0].split(':')[1] |
| const data = window.atob(parts[1]) |
| const arrayBuffer = new ArrayBuffer(data.length) |
| const uint8Array = new Uint8Array(arrayBuffer) |
|
|
| |
| for (let i = 0; i < data.length; i++) { |
| uint8Array[i] = data.charCodeAt(i) |
| } |
|
|
| |
| const blob = new Blob([arrayBuffer], { type }) |
|
|
| return blob |
| } |
|
|
| async function requestCamera () { |
| |
| try { |
| let stream = await navigator.mediaDevices.getUserMedia({ video: true }) |
| console.log('摄像头授权成功') |
| |
| var videoTrack = stream.getVideoTracks()[0] |
|
|
| |
| videoTrack.stop() |
|
|
| return true |
| } catch (error) { |
| |
| console.error('摄像头授权失败:', error) |
|
|
| |
| if (error.name === 'NotAllowedError') { |
| alert('请授权摄像头访问权限 chrome://settings/content/camera') |
| } else { |
| alert('摄像头访问权限请求失败,请重试 chrome://settings/content/camera') |
| } |
|
|
| |
| |
| } |
| return false |
| } |
|
|
| |
| |
| |
| function get_position_style (ctx, widget_width, y, node_height, top) { |
| const MARGIN = 4 |
|
|
| |
| const elRect = ctx.canvas.getBoundingClientRect() |
| const transform = new DOMMatrix() |
| .scaleSelf( |
| elRect.width / ctx.canvas.width, |
| elRect.height / ctx.canvas.height |
| ) |
| .multiplySelf(ctx.getTransform()) |
| .translateSelf(MARGIN, MARGIN + y) |
|
|
| return { |
| transformOrigin: '0 0', |
| transform: transform, |
| left: `0`, |
| top: `${top}px`, |
| cursor: 'pointer', |
| position: 'absolute', |
| maxWidth: `${widget_width - MARGIN * 2}px`, |
| |
| width: `${widget_width - MARGIN * 2}px`, |
| |
| |
| display: 'flex', |
| flexDirection: 'column', |
| |
| justifyContent: 'space-around' |
| } |
| } |
|
|
| const base64Df = |
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
|
|
| app.registerExtension({ |
| name: 'Mixlab.image.ScreenShareNode', |
| async getCustomWidgets (app) { |
| |
| return { |
| CHEESE (node, inputName, inputData, app) { |
| |
| const widget = { |
| type: inputData[0], |
| name: inputName, |
| size: [128, 12], |
| draw (ctx, node, width, y) { |
| |
| }, |
| computeSize (...args) { |
| return [128, 12] |
| }, |
| async serializeValue (nodeId, widgetIndex) { |
| return window._mixlab_screen_imagePath || base64Df |
| } |
| } |
| |
| node.addCustomWidget(widget) |
| return widget |
| }, |
| PROMPT (node, inputName, inputData, app) { |
| |
| const widget = { |
| type: inputData[0], |
| name: inputName, |
| size: [128, 12], |
| draw (ctx, node, width, y) { |
| |
| }, |
| computeSize (...args) { |
| return [128, 12] |
| }, |
| async serializeValue (nodeId, widgetIndex) { |
| return window._mixlab_screen_prompt || '' |
| } |
| } |
| |
| node.addCustomWidget(widget) |
| return widget |
| }, |
| SLIDE (node, inputName, inputData, app) { |
| |
| const widget = { |
| type: inputData[0], |
| name: inputName, |
| size: [128, 12], |
| draw (ctx, node, width, y) { |
| |
| }, |
| computeSize (...args) { |
| return [128, 12] |
| }, |
| async serializeValue (nodeId, widgetIndex) { |
| return window._mixlab_screen_slide_input || 0.5 |
| } |
| } |
| |
| node.addCustomWidget(widget) |
| return widget |
| }, |
| SEED (node, inputName, inputData, app) { |
| |
| const widget = { |
| type: inputData[0], |
| name: inputName, |
| size: [128, 12], |
| draw (ctx, node, width, y) { |
| |
| }, |
| computeSize (...args) { |
| return [128, 12] |
| }, |
| async serializeValue (nodeId, widgetIndex) { |
| return window._mixlab_screen_seed_input || 0 |
| } |
| } |
| |
| node.addCustomWidget(widget) |
| return widget |
| } |
| } |
| }, |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeType.comfyClass == 'ScreenShare') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
| nodeType.prototype.onNodeCreated = function () { |
| orig_nodeCreated?.apply(this, arguments) |
|
|
| const widget = { |
| type: 'HTML', |
| name: 'sreen_share', |
| draw (ctx, node, widget_width, y, widget_height) { |
| |
| Object.assign( |
| this.card.style, |
| get_position_style( |
| ctx, |
| widget_width, |
| widget_height * 5, |
| node.size[1], |
| 40 |
| ) |
| ) |
| } |
| } |
|
|
| widget.card = $el('div', { |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)' |
| }) |
|
|
| widget.previewCard = $el('div', { |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)' |
| }) |
|
|
| widget.preview = $el('video', { |
| style: { |
| width: '100%' |
| }, |
| controls: true, |
| poster: |
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
| }) |
|
|
| widget.previewArea = $el('div', { |
| style: { |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)' |
| } |
| }) |
|
|
| widget.shareDiv = $el('div', { |
| |
| style: { |
| cursor: 'pointer', |
| fontWeight: '300', |
| display: 'flex', |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)' |
| } |
| }) |
|
|
| widget.shareBtn = $el('button', { |
| innerText: 'Share Screen', |
| style: { |
| cursor: 'pointer', |
| padding: '8px 0', |
| fontWeight: '300', |
| margin: '2px', |
| width: '100%', |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)', |
| borderRadius: '8px', |
| borderColor: 'var(--border-color)', |
| borderStyle: 'solid' |
| } |
| }) |
|
|
| widget.shareOfWebCamBtn = $el('button', { |
| innerText: 'Camera', |
| style: { |
| cursor: 'pointer', |
| padding: '8px 0', |
| fontWeight: '300', |
| margin: '2px', |
| width: '100%', |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)', |
| borderRadius: '8px', |
| borderColor: 'var(--border-color)', |
| borderStyle: 'solid' |
| } |
| }) |
|
|
| widget.openFloatingWinBtn = $el('button', { |
| innerText: 'Set Area', |
| style: { |
| cursor: 'pointer', |
| padding: '8px 0', |
| fontWeight: '300', |
| margin: '2px', |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)', |
| borderRadius: '8px', |
| borderColor: 'var(--border-color)', |
| borderStyle: 'solid' |
| } |
| }) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| widget.liveBtn = $el('button', { |
| innerText: 'Live Run', |
| style: { |
| cursor: 'pointer', |
| padding: '8px 0', |
| fontWeight: '300', |
| margin: '2px', |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)', |
| borderRadius: '8px', |
| borderColor: 'var(--border-color)', |
| borderStyle: 'solid' |
| } |
| }) |
|
|
| document.body.appendChild(widget.card) |
|
|
| widget.card.appendChild(widget.previewCard) |
| widget.previewCard.appendChild(widget.preview) |
| widget.previewCard.appendChild(widget.previewArea) |
|
|
| widget.card.appendChild(widget.shareDiv) |
| widget.shareDiv.appendChild(widget.shareBtn) |
| widget.shareDiv.appendChild(widget.shareOfWebCamBtn) |
| widget.card.appendChild(widget.openFloatingWinBtn) |
| |
| widget.card.appendChild(widget.liveBtn) |
|
|
| const toggleShare = async (isCamera = false) => { |
| if (widget.preview.paused) { |
| window._mixlab_stopVideo = await shareScreen( |
| isCamera, |
| widget.preview, |
| widget.shareBtn, |
| widget.liveBtn, |
| widget.previewArea |
| ) |
|
|
| if (isCamera) { |
| widget.shareOfWebCamBtn.innerText = 'Stop Share' |
| widget.shareBtn.innerText = 'Stop' |
| } else { |
| widget.shareOfWebCamBtn.innerText = 'Stop' |
| widget.shareBtn.innerText = 'Stop Share' |
| } |
|
|
| console.log('视频已暂停') |
| if (window._mixlab_stopLive) { |
| window._mixlab_stopLive() |
| window._mixlab_stopLive = null |
| widget.liveBtn.innerText = 'Live Run' |
| } |
|
|
| setTimeout(() => updateSetAreaDisplay(), 2000) |
| } else { |
| console.log('视频正在播放') |
| if (window._mixlab_stopVideo) { |
| window._mixlab_stopVideo() |
| window._mixlab_stopVideo = null |
| widget.shareBtn.innerText = 'Share Screen' |
| widget.shareOfWebCamBtn.innerText = 'Camera' |
| } |
| if (window._mixlab_stopLive) { |
| window._mixlab_stopLive() |
| window._mixlab_stopLive = null |
| widget.liveBtn.innerText = 'Live Run' |
| } |
| } |
| } |
|
|
| |
| widget.shareOfWebCamBtn.addEventListener('click', async () => { |
| if (!widget.preview.paused) { |
| if (window._mixlab_stopVideo) { |
| window._mixlab_stopVideo() |
| window._mixlab_stopVideo = null |
| widget.shareBtn.innerText = 'Share Screen' |
| widget.shareOfWebCamBtn.innerText = 'Camera' |
| } |
| if (window._mixlab_stopLive) { |
| window._mixlab_stopLive() |
| window._mixlab_stopLive = null |
| widget.liveBtn.innerText = 'Live Run' |
| } |
| return |
| } |
|
|
| let r = await requestCamera() |
| if (r === false) return |
| const devices = await navigator.mediaDevices.enumerateDevices() |
|
|
| |
| var cameras = devices.filter(function (device) { |
| |
| return device.kind === 'videoinput' |
| }) |
|
|
| |
| var select = document.createElement('select') |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| Array.from(cameras, (camera, i) => { |
| var option = document.createElement('option') |
| option.value = camera.deviceId |
| option.text = camera.label || 'Camera ' + (select.length - 1) |
| if (i === 0) option.selected = true |
| select.appendChild(option) |
| }) |
|
|
| let modal = document.createElement('div') |
| modal.className = 'comfy-modal' |
| modal.style.display = 'flex' |
|
|
| let modalContent = document.createElement('div') |
| modalContent.className = 'comfy-modal-content' |
|
|
| let title = document.createElement('p') |
| title.innerText = 'Please select a camera' |
|
|
| modalContent.appendChild(title) |
| modalContent.appendChild(select) |
|
|
| let btns = document.createElement('div') |
| btns.style = `display: flex; |
| justify-content: space-between; |
| margin: 24px 0;` |
|
|
| let btn = document.createElement('button') |
| btn.innerText = 'OK' |
| btn.style = `width: 112px;` |
|
|
| let closeBtn = document.createElement('button') |
| closeBtn.innerText = 'Cancel' |
| closeBtn.style = `width: 112px;` |
|
|
| modalContent.appendChild(btns) |
| btns.appendChild(btn) |
| btns.appendChild(closeBtn) |
|
|
| modal.appendChild(modalContent) |
| document.body.appendChild(modal) |
|
|
| btn.addEventListener('click', () => { |
| |
| var selectedIndex = select.selectedIndex |
|
|
| |
| var selectedValue = select.options[selectedIndex].value |
| if (selectedValue) { |
| const constraints = { |
| audio: false, |
| video: { |
| width: { ideal: 1920, max: 1920 }, |
| height: { ideal: 1080, max: 1080 }, |
| deviceId: selectedValue |
| } |
| } |
|
|
| localStorage.setItem( |
| '_mixlab_webcamera_select', |
| JSON.stringify(constraints) |
| ) |
|
|
| toggleShare(true) |
| } |
|
|
| modal.remove() |
| }) |
|
|
| closeBtn.addEventListener('click', () => { |
| modal.remove() |
| }) |
| }) |
|
|
| widget.shareBtn.addEventListener('click', async () => { |
| toggleShare() |
| }) |
|
|
| |
| |
| |
| |
| |
|
|
| widget.liveBtn.addEventListener('click', async () => { |
| if (window._mixlab_stopLive) { |
| window._mixlab_stopLive() |
| window._mixlab_stopLive = null |
| widget.liveBtn.innerText = 'Live Run' |
| } else { |
| window._mixlab_stopLive = await startLive(widget.liveBtn) |
| console.log('window._mixlab_stopLive', window._mixlab_stopLive) |
| } |
| }) |
|
|
| widget.openFloatingWinBtn.addEventListener('click', async () => { |
| |
| |
| |
| |
| |
| let blob = await createBlobFromVideoForArea( |
| window._mixlab_screen_webcamVideo |
| ) |
|
|
| setArea(await blobToBase64(blob)) |
| }) |
| |
|
|
| this.setSize([this.size[0], this.size[1] + 450]) |
| app.canvas.draw(true, true) |
|
|
| |
| |
| |
| this.addCustomWidget(widget) |
| this.onRemoved = function () { |
| widget.preview.remove() |
| widget.shareDiv.remove() |
| widget.shareOfWebCamBtn.remove() |
| widget.shareBtn.remove() |
| widget.liveBtn.remove() |
| widget.card.remove() |
| |
| widget.previewArea.remove() |
| widget.previewCard.remove() |
| } |
| this.serialize_widgets = true |
| } |
|
|
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| onExecuted?.apply(this, arguments) |
| |
| window._mixlab_screen_refresh_rate = Math.round( |
| message.refresh_rate[0] || 500 |
| ) |
| } |
| } |
| } |
| }) |
|
|
| function updateSetAreaDisplay () { |
| try { |
| let canvas = document.createElement('canvas') |
| canvas.width = window._mixlab_screen_webcamVideo.videoWidth |
| canvas.height = window._mixlab_screen_webcamVideo.videoHeight |
| let ctx = canvas.getContext('2d') |
| const lineWidth = 2 |
| const strokeColor = 'red' |
|
|
| |
| ctx.strokeStyle = strokeColor |
| ctx.lineWidth = lineWidth |
|
|
| ctx.fillStyle = 'rgba(255,0,0,0.35)' |
|
|
| let x = 0, |
| y = 0, |
| width = canvas.width, |
| height = canvas.height |
|
|
| if (!window._mixlab_share_screen) { |
| let d = getSetAreaData() |
| if (d) { |
| window._mixlab_share_screen = d |
| } |
| } |
|
|
| if (window._mixlab_share_screen) { |
| x = window._mixlab_share_screen.x |
| y = window._mixlab_share_screen.y |
| width = window._mixlab_share_screen.width |
| height = window._mixlab_share_screen.height |
| } |
|
|
| ctx.strokeRect(x, y, width, height) |
| ctx.fillRect(x, y, width, height) |
|
|
| canvas.style.width = '100%' |
|
|
| let area = graph._nodes |
| .filter(n => n.type === 'ScreenShare')[0] |
| .widgets.filter(w => w.name == 'sreen_share')[0].previewArea |
|
|
| area.innerHTML = '' |
| area.appendChild(canvas) |
| area.style = ` |
| position: absolute; |
| width:100%%; |
| left:0; |
| top:0; |
| ` |
| } catch (error) { |
| console.log(error) |
| } |
| } |
|
|
| function updateSetAreaData (left, top, width, height, imgWidth, imgHeight) { |
| window._mixlab_share_screen = { |
| x: left, |
| y: top, |
| width, |
| height, |
| imgWidth, |
| imgHeight |
| } |
| localStorage.setItem( |
| '_mixlab_share_screen', |
| JSON.stringify(window._mixlab_share_screen) |
| ) |
| } |
|
|
| function getSetAreaData () { |
| try { |
| let data = JSON.parse(localStorage.getItem('_mixlab_share_screen')) || {} |
| if (data.width === 0 || data.height === 0 || data.width === undefined) |
| return |
| return data |
| } catch (error) {} |
| return |
| } |
|
|
| async function setArea (src) { |
| let displayHeight = Math.round(window.screen.availHeight * 0.6) |
| let div = document.createElement('div') |
| div.innerHTML = ` |
| <div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4; |
| height: 100vh; |
| z-index:999999; |
| width: 100%;'> |
| <img id='ml_video' style='position: absolute; |
| height: ${displayHeight}px;user-select: none; |
| -webkit-user-drag: none; |
| outline: 2px solid #eaeaea; |
| box-shadow: 8px 9px 17px #575757;' /> |
| <div id='ml_selection' style='position: absolute; |
| border: 2px dashed red; |
| pointer-events: none;'></div> |
| </div>` |
| |
| document.body.appendChild(div) |
|
|
| let im = await createImage(src) |
|
|
| let img = div.querySelector('#ml_video') |
| let overlay = div.querySelector('#ml_overlay') |
| let selection = div.querySelector('#ml_selection') |
| let startX, startY, endX, endY |
| let start = false |
| |
| img.src = src |
|
|
| |
| const data = getSetAreaData() |
| let x = 0, |
| y = 0, |
| width = (im.naturalWidth * displayHeight) / im.naturalHeight, |
| height = displayHeight |
| let imgWidth = im.naturalWidth |
| let imgHeight = im.naturalHeight |
|
|
| if ( |
| data && |
| data.width > 0 && |
| data.height > 0 && |
| data.imgWidth === imgWidth && |
| data.imgHeight === imgHeight && |
| data.imgHeight > 0 |
| ) { |
| |
| x = (img.width * data.x) / data.imgWidth |
| y = (img.height * data.y) / data.imgHeight |
| width = (img.width * data.width) / data.imgWidth |
| height = (img.height * data.height) / data.imgHeight |
| } |
|
|
| selection.style.left = x + 'px' |
| selection.style.top = y + 'px' |
| selection.style.width = width + 'px' |
| selection.style.height = height + 'px' |
|
|
| |
| img.addEventListener('mousedown', startSelection) |
| img.addEventListener('mousemove', updateSelection) |
| img.addEventListener('mouseup', endSelection) |
| overlay.addEventListener('click', remove) |
|
|
| function remove () { |
| overlay.removeEventListener('click', remove) |
| img.removeEventListener('mousedown', startSelection) |
| img.removeEventListener('mousemove', updateSelection) |
| img.removeEventListener('mouseup', endSelection) |
| div.remove() |
| } |
|
|
| function startSelection (event) { |
| if (start == false) { |
| startX = event.clientX |
| startY = event.clientY |
| updateSelection(event) |
| start = true |
| } else { |
| } |
| } |
|
|
| function updateSelection (event) { |
| endX = event.clientX |
| endY = event.clientY |
|
|
| |
| let width = Math.abs(endX - startX) |
| let height = Math.abs(endY - startY) |
| let left = Math.min(startX, endX) |
| let top = Math.min(startY, endY) |
|
|
| |
| selection.style.left = left + 'px' |
| selection.style.top = top + 'px' |
| selection.style.width = width + 'px' |
| selection.style.height = height + 'px' |
| } |
|
|
| function endSelection (event) { |
| endX = event.clientX |
| endY = event.clientY |
|
|
| |
| let imgWidth = img.naturalWidth |
| let imgHeight = img.naturalHeight |
|
|
| |
| let realStartX = (startX / img.offsetWidth) * imgWidth |
| let realStartY = (startY / img.offsetHeight) * imgHeight |
|
|
| |
| let realEndX = (endX / img.offsetWidth) * imgWidth |
| let realEndY = (endY / img.offsetHeight) * imgHeight |
|
|
| startX = realStartX |
| startY = realStartY |
| endX = realEndX |
| endY = realEndY |
| |
| let width = Math.abs(endX - startX) |
| let height = Math.abs(endY - startY) |
| let left = Math.min(startX, endX) |
| let top = Math.min(startY, endY) |
|
|
| if (width <= 0 && height <= 0) return remove() |
|
|
| updateSetAreaData(left, top, width, height, imgWidth, imgHeight) |
|
|
| updateSetAreaDisplay() |
|
|
| createBlobFromVideo( |
| window._mixlab_screen_webcamVideo, |
| !window._mixlab_screen_live |
| ) |
| remove() |
| } |
| } |
|
|
| async function save_workflow (json) { |
| let api_host = `${window.location.hostname}:${window.location.port}` |
| let api_base = '' |
| let url = `${window.location.protocol}//${api_host}${api_base}` |
|
|
| const res = await fetch(`${url}/mixlab/workflow`, { |
| method: 'POST', |
| body: JSON.stringify({ |
| data: json, |
| task: 'save' |
| }) |
| }) |
| return await res.json() |
| } |
|
|
| async function get_my_workflow () { |
| let api_host = `${window.location.hostname}:${window.location.port}` |
| let api_base = '' |
| let url = `${window.location.protocol}//${api_host}${api_base}` |
|
|
| const res = await fetch(`${url}/mixlab/workflow`, { |
| method: 'POST', |
| body: JSON.stringify({ |
| task: 'list' |
| }) |
| }) |
| let result = await res.json() |
| return result.data |
| } |
|
|
| app.registerExtension({ |
| name: 'Mixlab.image.FloatingVideo', |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeType.comfyClass == 'FloatingVideo') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
| nodeType.prototype.onNodeCreated = function () { |
| orig_nodeCreated?.apply(this, arguments) |
|
|
| const widget = { |
| type: 'video', |
| name: 'FloatingVideo', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign( |
| this.card.style, |
| get_position_style(ctx, widget_width, y, node.size[1], 0) |
| ) |
| } |
| } |
|
|
| widget.card = $el('div', {}) |
|
|
| widget.preview = $el('video', { |
| controls: true, |
| draggable: true, |
| style: { |
| width: '100%' |
| }, |
| poster: |
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
| }) |
|
|
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| widget.canvas = $el('canvas', { |
| style: { |
| display: 'none' |
| } |
| }) |
|
|
| widget.PictureInPicture = $el('button', { |
| innerText: 'Picture In Picture', |
| style: { |
| display: 'pictureInPictureEnabled' in document ? 'block' : 'none', |
| cursor: 'pointer', |
| padding: '8px 0', |
| fontWeight: '300', |
| margin: '2px', |
| color: 'var(--descrip-text)', |
| backgroundColor: 'var(--comfy-input-bg)', |
| borderRadius: '8px', |
| borderColor: 'var(--border-color)', |
| borderStyle: 'solid' |
| } |
| }) |
|
|
| document.body.appendChild(widget.card) |
| widget.card.appendChild(widget.PictureInPicture) |
| widget.card.appendChild(widget.preview) |
| widget.card.appendChild(widget.canvas) |
|
|
| widget.preview.addEventListener('click', event => { |
| const imageUrl = window._mixlab_screen_result || '' |
| |
| try { |
| if (imageUrl) clipboardWriteImage(pipWindow, imageUrl) |
| } catch (error) { |
| console.log(error) |
| if (imageUrl) clipboardWriteImage(window, imageUrl) |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| }) |
|
|
| |
|
|
| widget.PictureInPicture.addEventListener('click', async () => { |
| if (window.location.protocol != 'https:') { |
| let http_workflow = app.graph.serialize() |
| await save_workflow(http_workflow) |
|
|
| window.alert( |
| `Redirecting to HTTPS access due to the requirement of the floating window. https://${ |
| window.location.hostname |
| }:${~~window.location.port + 1}` |
| ) |
| window.open( |
| `https://${window.location.hostname}:${ |
| ~~window.location.port + 1 |
| }?workflow=1` |
| ) |
| } |
| |
| let w = 360, |
| s = widget.preview.videoWidth / widget.preview.videoHeight, |
| h = w / s || w |
| |
|
|
| if (!window.documentPictureInPicture) { |
| window.alert( |
| 'This feature is available only in secure contexts (HTTPS), in some or all supporting browsers. https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API' |
| ) |
| } |
|
|
| const pipWindow = await documentPictureInPicture.requestWindow({ |
| width: w, |
| height: Math.round(h) + 120 |
| }) |
|
|
| pipWindow.document.body.style = `margin: 0px; |
| overflow: hidden; |
| background: #2a2c34; |
| border: 4px solid #878787; |
| outline: none;background: black;` |
|
|
| let div = document.createElement('div') |
| div.style = `display:flex;position: fixed;flex-direction: column; |
| bottom: 0px; |
| z-index: 9999; |
| left: 0px; |
| width: calc(100% - 24px); |
| margin: 12px;` |
|
|
| let inputDiv = document.createElement('div') |
| inputDiv.style = `width: 100%;` |
|
|
| |
| let infoDiv = document.createElement('div') |
| infoDiv.style = `width: 100%; |
| display: flex; |
| justify-content: space-between; |
| height: 16px; |
| color: white; |
| margin-bottom: 4px; |
| font-size: 12px; |
| text-shadow: gray 1px 1px; |
| align-items: center;` |
|
|
| let infoText = document.createElement('div') |
| infoText.id = 'info' |
|
|
| let hideBtn = document.createElement('button') |
| hideBtn.innerText = '🤖' |
| hideBtn.style = ` |
| border: none; |
| background: none; |
| cursor: pointer; height: 24px; margin: 4px; color: red;` |
|
|
| hideBtn.addEventListener('click', () => { |
| if (fnDiv.style.display == 'none') { |
| fnDiv.style.display = 'flex' |
| } else { |
| fnDiv.style.display = 'none' |
| } |
| try { |
| pipWindow.document.querySelector('#info').innerText = '' |
| } catch (error) { |
| console.log(error) |
| } |
| }) |
|
|
| |
| let input = document.createElement('textarea') |
| input.style = ` |
| min-width:90%; |
| max-width:100%; |
| background: #24283db3; |
| color: white; |
| font-size: 14px; |
| padding: 8px; |
| font-weight: 300; |
| letter-spacing: 1px; |
| outline: none; |
| min-height: 98px; |
| border-radius: 8px; |
| border: 1px solid rgb(91, 91, 91); |
| font-family: sans-serif; |
| ` |
| |
| const style = document.createElement('style') |
| |
| const cssRule = `::-webkit-scrollbar { width: 2px;}` |
| |
| style.appendChild(document.createTextNode(cssRule)) |
|
|
| |
| pipWindow.document.head.appendChild(style) |
|
|
| window._mixlab_screen_prompt = |
| window._mixlab_screen_prompt || |
| 'beautiful scenery nature glass bottle landscape,under water' |
| input.value = window._mixlab_screen_prompt |
|
|
| let btnDiv = document.createElement('div') |
|
|
| btnDiv.style = `cursor: pointer; |
| display: flex; |
| flex-direction: column; |
| justify-content: start; |
| align-items: center; |
| width: 24px; |
| font-size: 16px; |
| margin-right: 6px;user-select: none;` |
|
|
| let seedBtn = document.createElement('butotn') |
| seedBtn.innerText = '🎲' |
| seedBtn.style = `cursor: pointer;height: 24px;margin:4px; |
| color: red;` |
|
|
| seedBtn.addEventListener('click', () => { |
| window._mixlab_screen_seed_input = Math.round( |
| Math.floor(Math.random() * 0xffffffffffffffff) |
| ) |
|
|
| try { |
| pipWindow.document.querySelector('#info').innerText = |
| window._mixlab_screen_seed_input |
| } catch (error) { |
| console.log(error) |
| } |
|
|
| |
| if (window._mixlab_screen_imagePath) |
| document.querySelector('#queue-button').click() |
| }) |
|
|
| |
| let pauseBtn = document.createElement('butotn') |
| pauseBtn.innerText = '⏸' |
| pauseBtn.style = `cursor: pointer;height: 24px;margin:4px; |
| color: #03A9F4;` |
|
|
| pauseBtn.addEventListener('click', async () => { |
| if (window._mixlab_stopLive) { |
| pauseBtn.innerText = '▶' |
|
|
| window._mixlab_stopLive() |
| window._mixlab_stopLive = null |
|
|
| let node = this.graph._nodes.filter( |
| n => n.type === 'ScreenShare' |
| )[0] |
|
|
| var w = node.widgets?.filter(w => w.name === 'sreen_share')[0] |
| if (w) { |
| w.liveBtn.innerText = 'Live Run' |
| } |
|
|
| try { |
| pipWindow.document.querySelector('#info').innerText = |
| 'Stop Live' |
| } catch (error) { |
| console.log(error) |
| } |
| } else { |
| pauseBtn.innerText = '⏸' |
| let node = this.graph._nodes.filter( |
| n => n.type === 'ScreenShare' |
| )[0] |
| var w = node.widgets?.filter(w => w.name === 'sreen_share')[0] |
| if (w) { |
| w.liveBtn.innerText = 'Stop Live' |
| window._mixlab_stopLive = await startLive(w.liveBtn) |
| console.log('window._mixlab_stopLive', window._mixlab_stopLive) |
| } |
|
|
| try { |
| pipWindow.document.querySelector('#info').innerText = 'Live' |
| } catch (error) { |
| console.log(error) |
| } |
| } |
| }) |
|
|
| let promptFinishBtn = document.createElement('butotn') |
| promptFinishBtn.innerText = '🚀' |
| promptFinishBtn.style = `cursor: pointer;height: 24px;margin:4px;` |
| promptFinishBtn.addEventListener('click', () => { |
| console.log('##更新Prompt') |
| window._mixlab_screen_prompt = |
| window._mixlab_screen_prompt_input || window._mixlab_screen_prompt |
|
|
| if (window._mixlab_screen_imagePath) |
| document.querySelector('#queue-button').click() |
|
|
| try { |
| pipWindow.document.querySelector('#info').innerText = |
| 'Update Prompt' |
| } catch (error) { |
| console.log(error) |
| } |
| }) |
|
|
| widget.preview.addEventListener('click', event => { |
| const imageUrl = window._mixlab_screen_result || '' |
| |
| try { |
| if (imageUrl) clipboardWriteImage(pipWindow, imageUrl) |
| } catch (error) { |
| console.log(error) |
| if (imageUrl) clipboardWriteImage(window, imageUrl) |
| } |
|
|
| try { |
| pipWindow.document.querySelector('#info').innerText = |
| 'Image copied to clipboard' |
| setTimeout( |
| () => |
| (pipWindow.document.querySelector('#info').innerText = ''), |
| 8000 |
| ) |
| } catch (error) { |
| console.log(error) |
| } |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| }) |
|
|
| pipWindow.document.body.append(widget.preview) |
| pipWindow.document.body.append(div) |
|
|
| |
| const createSlide = () => { |
| let d = document.createElement('div') |
| d.style = `width: 100%;margin-bottom: 12px;` |
| let range = document.createElement('input') |
| range.type = 'range' |
| d.appendChild(range) |
| return range |
| } |
|
|
| let slideInp = createSlide() |
| slideInp.addEventListener('change', () => { |
| console.log(~~slideInp.value / 100) |
| window._mixlab_screen_slide_input = ~~slideInp.value / 100 |
| try { |
| pipWindow.document.querySelector('#info').innerText = |
| window._mixlab_screen_slide_input |
| if (window._mixlab_screen_imagePath) |
| document.querySelector('#queue-button').click() |
| } catch (error) { |
| console.log(error) |
| } |
| }) |
|
|
| |
|
|
| let fnDiv = document.createElement('div') |
| fnDiv.style = `display: flex;` |
|
|
| div.appendChild(infoDiv) |
| div.appendChild(fnDiv) |
|
|
| infoDiv.appendChild(infoText) |
| infoDiv.appendChild(hideBtn) |
|
|
| fnDiv.appendChild(btnDiv) |
| |
| btnDiv.appendChild(seedBtn) |
| btnDiv.appendChild(pauseBtn) |
| btnDiv.appendChild(promptFinishBtn) |
|
|
| |
| fnDiv.appendChild(inputDiv) |
| inputDiv.appendChild(slideInp) |
|
|
| inputDiv.appendChild(input) |
|
|
| input.addEventListener('input', () => { |
| window._mixlab_screen_prompt_input = input.value |
| try { |
| pipWindow.document.querySelector('#info').innerText = '' |
| } catch (error) { |
| console.log(error) |
| } |
| }) |
|
|
| input.addEventListener('keydown', handleKeyDown) |
|
|
| function handleKeyDown (event) { |
| if (event.key === 'Enter') { |
| if (!event.shiftKey) { |
| |
| event.preventDefault() |
| |
| console.log('##更新Prompt') |
| window._mixlab_screen_prompt = |
| window._mixlab_screen_prompt_input || |
| window._mixlab_screen_prompt |
|
|
| if (window._mixlab_screen_imagePath) |
| document.querySelector('#queue-button').click() |
|
|
| try { |
| pipWindow.document.querySelector('#info').innerText = |
| 'Update Prompt' |
| } catch (error) { |
| console.log(error) |
| } |
| } |
| } |
| } |
|
|
| |
| pipWindow.addEventListener('pagehide', event => { |
| widget.card.appendChild(widget.preview) |
| |
| pipWindow.close() |
| }) |
| }) |
|
|
| this.addCustomWidget(widget) |
| this.onRemoved = function () { |
| widget.preview.remove() |
| widget.canvas.remove() |
| widget.card.remove() |
| widget.PictureInPicture.remove() |
| } |
| this.serialize_widgets = true |
| } |
|
|
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| const r = onExecuted ? onExecuted.apply(this, message) : undefined |
|
|
| if (this.widgets) { |
| const video = this.widgets.filter(w => w.type === `video`)[0] |
| const canvas = video.canvas |
|
|
| if (video.preview.paused) { |
| const stream = canvas.captureStream() |
| const videoTrack = stream.getVideoTracks()[0] |
|
|
| video.preview.srcObject = new MediaStream([videoTrack]) |
| try { |
| video.preview.play() |
| } catch (error) { |
| console.log(error) |
| } |
|
|
| |
| if ('pictureInPictureEnabled' in document) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| } else { |
| |
| console.error('浏览器不支持画中画模式') |
| } |
| } |
|
|
| const context = canvas.getContext('2d') |
|
|
| if (message?.images_) { |
| window._mixlab_screen_result = `data:image/png;base64,${message.images_[0]}` |
| const image = new Image() |
| image.onload = function () { |
| canvas.width = image.width |
| canvas.height = image.height |
| context.drawImage(image, 0, 0) |
| } |
| |
| image.src = window._mixlab_screen_result |
| } |
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| |
| return onRemoved?.() |
| } |
| } |
| this.setSize([ |
| this.size[0], |
| this.computeSize([this.size[0], this.size[1]])[1] |
| ]) |
| return r |
| } |
| } |
| } |
| }) |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| function run (mutable_prompt, immutable_prompt) { |
| |
| const words1 = mutable_prompt.split('\n') |
|
|
| |
| const words2 = immutable_prompt.split('\n') |
|
|
| const prompts = [] |
| for (let i = 0; i < words1.length; i++) { |
| words1[i] = words1[i].trim() |
| for (let j = 0; j < words2.length; j++) { |
| words2[j] = words2[j].trim() |
| if (words2[j] && words1[i]) { |
| prompts.push(words2[j].replaceAll('``', words1[i])) |
| } |
| } |
| } |
|
|
| return prompts |
| } |
|
|
| |
| const updateUI = node => { |
| const mutable_prompt_w = node.widgets.filter( |
| w => w.name === 'mutable_prompt' |
| )[0] |
| mutable_prompt_w.inputEl.title = 'Enter keywords, one per line' |
| const immutable_prompt_w = node.widgets.filter( |
| w => w.name === 'immutable_prompt' |
| )[0] |
| immutable_prompt_w.inputEl.title = |
| 'Enter prompts, one per line, variables represented by ``' |
|
|
| const max_count = node.widgets.filter(w => w.name === 'max_count')[0] |
| let prompts = run(mutable_prompt_w.value, immutable_prompt_w.value) |
|
|
| prompts = prompts.slice(0, max_count.value) |
|
|
| max_count.value = prompts.length |
|
|
| |
| const pw = node.widgets.filter(w => w.name === 'prompts')[0] |
| if (pw) { |
| |
| pw.value = prompts.join('\n\n') |
| pw.inputEl.title = `Total of ${prompts.length} prompts` |
| } else { |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| const w = ComfyWidgets.STRING( |
| node, |
| 'prompts', |
| ['STRING', { multiline: true }], |
| app |
| ).widget |
| w.inputEl.readOnly = true |
| w.inputEl.style.opacity = 0.6 |
| w.value = prompts.join('\n\n') |
| w.inputEl.title = `Total of ${prompts.length} prompts` |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| |
|
|
| node.widgets.length = 5 |
| node.onResize?.(node.size) |
| } |
|
|
| const exportGraph = () => { |
| const graph = app.graph |
|
|
| var clipboard_info = { |
| nodes: [], |
| links: [] |
| } |
| var index = 0 |
| var selected_nodes_array = [] |
| for (var i in graph._nodes_in_order) { |
| var node = graph._nodes_in_order[i] |
| if (node.clonable === false) continue |
| node._relative_id = index |
| selected_nodes_array.push(node) |
| index += 1 |
| } |
|
|
| for (var i = 0; i < selected_nodes_array.length; ++i) { |
| var node = selected_nodes_array[i] |
| var cloned = node.clone() |
| if (!cloned) { |
| console.warn('node type not found: ' + node.type) |
| continue |
| } |
|
|
| let nc = {} |
| let n = cloned.serialize() |
| for (const key in n) { |
| if ( |
| [ |
| 'type', |
| 'pos', |
| 'size', |
| 'flags', |
| 'order', |
| 'mode', |
| 'inputs', |
| 'outputs', |
| 'properties', |
| 'widgets_values' |
| ].includes(key) |
| ) { |
| nc[key] = n[key] |
| } |
| } |
|
|
| clipboard_info.nodes.push(nc) |
|
|
| if (node.inputs && node.inputs.length) { |
| for (var j = 0; j < node.inputs.length; ++j) { |
| var input = node.inputs[j] |
| if (!input || input.link == null) { |
| continue |
| } |
| var link_info = graph.links[input.link] |
| if (!link_info) { |
| continue |
| } |
| var target_node = graph.getNodeById(link_info.origin_id) |
| if (!target_node) { |
| continue |
| } |
| clipboard_info.links.push([ |
| target_node._relative_id, |
| link_info.origin_slot, |
| node._relative_id, |
| link_info.target_slot, |
| target_node.id |
| ]) |
| } |
| } |
| } |
| localStorage.setItem('_Mixlab_clipboard', JSON.stringify(clipboard_info)) |
|
|
| return clipboard_info |
| } |
|
|
| const my = { |
| nodes: [ |
| { |
| type: 'LoadImage', |
| pos: [719.5130480797907, 172.9437092123179], |
| size: { 0: 315, 1: 314 }, |
| flags: {}, |
| order: 0, |
| mode: 0, |
| outputs: [ |
| { name: 'IMAGE', type: 'IMAGE', links: [], shape: 3 }, |
| { name: 'MASK', type: 'MASK', links: null, shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'LoadImage' }, |
| widgets_values: ['00204211b3c71288c12ed66516a1a20a.jpg', 'image'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1199.5130480797907, -331.0562907876821], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 1, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11p_sd15_canny.pth'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1204.5130480797907, -169.0562907876821], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 2, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11f1p_sd15_depth.pth'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1206.5130480797907, -20.056290787682087], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 3, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['t2iadapter_seg-fp16.safetensors'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1209.5130480797907, 125.94370921231791], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 4, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11p_sd15_openpose.pth'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1214.5130480797907, 293.9437092123179], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 5, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11e_sd15_ip2p.safetensors'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1212.5130480797907, 461.9437092123179], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 6, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11p_sd15_inpaint.pth'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1216.5130480797907, 636.9437092123179], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 7, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11f1e_sd15_tile.bin'] |
| }, |
| { |
| type: 'ControlNetLoader', |
| pos: [1227.5130480797907, 804.9437092123179], |
| size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
| flags: {}, |
| order: 8, |
| mode: 0, |
| outputs: [ |
| { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetLoader' }, |
| widgets_values: ['control_v11f1e_sd15_tile.bin'] |
| }, |
| { |
| type: 'ControlNetApplyAdvanced', |
| pos: [1816, 94], |
| size: { 0: 315, 1: 166 }, |
| flags: {}, |
| order: 9, |
| mode: 0, |
| inputs: [ |
| { name: 'positive', type: 'CONDITIONING', link: null }, |
| { name: 'negative', type: 'CONDITIONING', link: null }, |
| { name: 'control_net', type: 'CONTROL_NET', link: null, slot_index: 2 }, |
| { name: 'image', type: 'IMAGE', link: null, slot_index: 3 } |
| ], |
| outputs: [ |
| { name: 'positive', type: 'CONDITIONING', links: null, shape: 3 }, |
| { name: 'negative', type: 'CONDITIONING', links: null, shape: 3 } |
| ], |
| properties: { 'Node name for S&R': 'ControlNetApplyAdvanced' }, |
| widgets_values: [1, 0, 1] |
| } |
| ], |
| links: [ |
| [1, 0, 9, 2, 2], |
| [0, 0, 9, 3, 1] |
| ] |
| } |
|
|
| |
| const importWorkflow = my => { |
| localStorage.setItem('litegrapheditor_clipboard', JSON.stringify(my)) |
| app.canvas.pasteFromClipboard() |
| } |
|
|
| function getURLParameters (url) { |
| var params = {} |
| var paramStr = url.split('?')[1] |
| if (paramStr) { |
| var paramArr = paramStr.split('&') |
| for (var i = 0; i < paramArr.length; i++) { |
| var param = paramArr[i].split('=') |
| var paramName = decodeURIComponent(param[0]) |
| var paramValue = decodeURIComponent(param[1] || '') |
| if (paramName) { |
| if (params[paramName]) { |
| params[paramName] = Array.isArray(params[paramName]) |
| ? [...params[paramName], paramValue] |
| : [params[paramName], paramValue] |
| } else { |
| params[paramName] = paramValue |
| } |
| } |
| } |
| } |
| return params |
| } |
|
|
| |
| |
| |
|
|
| const node = { |
| name: 'RandomPrompt', |
| async init (app) { |
| |
| |
|
|
| if (window.location.href.match('/?')) { |
| const { workflow } = getURLParameters(window.location.href) |
| if (workflow) |
| get_my_workflow().then(data => { |
| |
| let my_workflow = data.filter( |
| d => d.filename == 'my_workflow.json' |
| )[0] |
| if (my_workflow?.data) { |
| |
| localStorage.setItem('workflow', JSON.stringify(my_workflow.data)) |
| } |
| }) |
| } |
| }, |
| async setup (a) { |
| for (const node of app.graph._nodes) { |
| |
| if (node.type === 'RandomPrompt') { |
| updateUI(node) |
| } |
| } |
| |
| }, |
| addCustomNodeDefs (defs, app) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| }, |
| loadedGraphNode (node, app) { |
| if (node.type === 'RandomPrompt') { |
| try { |
| let max_count = node.widgets.filter(w => w.name === 'max_count')[0] |
| max_count.value = node.widgets_values[0] |
| |
| } catch (error) { |
| console.log(error) |
| } |
| } |
| }, |
| async nodeCreated (node) { |
| if (node.type === 'RandomPrompt') { |
| updateUI(node) |
| } |
|
|
| if (node.type === 'RunWorkflow') { |
| const pw = node.widgets.filter(w => w.name === 'workflow')[0] |
| console.log('nodeCreated', pw) |
| |
| |
| |
| |
| |
|
|
| |
| node.onResize?.(node.size) |
| } |
| }, |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| if (nodeData.name === 'WSServer') { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| } |
|
|
| if (nodeData.name === 'RandomPrompt') { |
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| const r = onExecuted?.apply?.(this, arguments) |
|
|
| let prompts = message.prompts |
| |
| |
| const pw = this.widgets.filter(w => w.name === 'prompts')[0] |
|
|
| if (pw) { |
| |
| pw.value = prompts.join('\n\n') |
| pw.inputEl.title = `Total of ${prompts.length} prompts` |
| } else { |
| |
| const w = ComfyWidgets.STRING( |
| this, |
| 'prompts', |
| ['STRING', { multiline: true }], |
| app |
| ).widget |
| w.inputEl.readOnly = true |
| w.inputEl.style.opacity = 0.6 |
| w.value = prompts.join('\n\n') |
| w.inputEl.title = `Total of ${prompts.length} prompts` |
| } |
|
|
| this.widgets.length = 5 |
|
|
| this.onResize?.(this.size) |
|
|
| return r |
| } |
| } |
| } |
| } |
|
|
| app.registerExtension(node) |
|
|