Compare commits

..
Author SHA1 Message Date
shadowcz007 fee0222910 v0.37.0 移动端适配、修改app模式的Mask编辑器 2024-08-12 10:10:43 +08:00
shadowcz007 1ed7b5511f mixlab app new mask editor 2024-08-12 00:19:26 +08:00
shadowcz007 b8f7c31537 Update index.html 2024-08-11 17:53:18 +08:00
shadowcz007 164791c257 webui 移动端适配 2024-08-11 17:20:36 +08:00
shadowcz007 8f5e599928 fixbug & ui 2024-08-11 16:29:16 +08:00
shadowcz007 7a7aaeb84d Update index.html 2024-08-10 23:00:58 +08:00
shadowcz007 e2136ab2fc fixbug 2024-08-10 17:13:28 +08:00
shadowcz007 c75cb21946 clean 2024-08-10 10:47:54 +08:00
shadowcz007 bf95218c91 p5-video-workflow 2024-08-10 00:59:44 +08:00
shadowcz007 8cb4507a5f v0.36.0 p5.js 2024-08-10 00:39:27 +08:00
shadowcz007 555890d1ba Update pyproject.toml 2024-08-09 19:08:36 +08:00
shadowcz007 e4f54e83b6 Update Text-to-Image-app.json 2024-08-09 16:30:47 +08:00
shadowcz007 692c4a709e fixbug:web app 2024-08-09 16:27:34 +08:00
shadowcz007 cbd1961459 test 2024-08-08 21:58:44 +08:00
shadowcz007 2e31a33ebf fixbug 2024-08-08 11:37:36 +08:00
shadowcz007 d16c6137d2 update 2024-08-06 23:07:20 +08:00
shadowcz007 0416ab79ec Update 3d_mixlab.js 2024-08-06 21:08:52 +08:00
shadow fc9a1c62b9 Merge pull request #295 from shadowcz007/0.36.0-py5-processing
Lama 改成手动安装,新增JsonRepair
2024-08-06 11:09:27 +08:00
shadow ae4a17d271 Merge pull request #293 from shadowcz007/0.36.0-py5-processing
0.36.0 py5 processing
2024-08-06 00:24:13 +08:00
shadowcz007 8e488d4b1d update 2024-08-05 11:55:07 +08:00
41 changed files with 65249 additions and 4163 deletions
+7 -4
View File
@@ -5,6 +5,11 @@
##### `最新`:
- 移动端适配、修改app模式的Mask编辑器
- 增加p5.js作为输入节点
[workflow](./workflow/p5workflow.json)
[workflow2](./workflow/p5-video-workflow.json)
- App模式增加batch prompt,批量提示词,可以把动态提示词批量组成后运行
@@ -14,10 +19,6 @@
- 增加 SiliconflowLLM,可以使用由Siliconflow提供的免费LLM
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
- LaMaInpainting 调整为手动安装
<!-- - ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/` -->
<!-- - 右键菜单支持 text-to-text,方便对 prompt 词补全 -->
@@ -246,6 +247,8 @@ pip install llama-cpp-python \
### Other Nodes
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
+10 -5
View File
@@ -782,7 +782,7 @@ def random_seed(seed, data):
@routes.post("/mixlab/prompt")
async def mixlab_post_prompt(request):
p_intance=PromptServer.instance
logging.info("got prompt")
logging.info("/mixlab/prompt")
resp_code = 200
out_string = ""
json_data = await request.json()
@@ -795,13 +795,17 @@ async def mixlab_post_prompt(request):
# 种子
seed=json_data['seed'] if "seed" in json_data else {}
apps=get_my_workflow_for_app(json_data['filename'],json_data['category'],False)
try:
apps=json_data['apps']
except:
apps=get_my_workflow_for_app(json_data['filename'],json_data['category'],False)
prompt=json_data['prompt'] if 'prompt' in json_data else None
if len(apps)==1:
if len(apps)>0:
# 取到prompt
prompt=apps[0]['data']['output']
# logging.info(prompt)
# 更新input_data到prompt里
'''
{
@@ -1009,7 +1013,7 @@ def mix_status(request):
# 导入节点
from .nodes.PromptNode import GLIGENTextBoxApply_Advanced,EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage,JoinWithDelimiter
from .nodes.ImageNode import ImageBatchToList_,ImageListToBatch_,ComparingTwoFrames,LoadImages_,CompositeImages,GridDisplayAndSave,GridInput,ImagesPrompt,SaveImageAndMetadata,SaveImageToLocal,SplitImage,GridOutput,GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,CenterImage,AreaToMask,SmoothMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import DepthViewer_,ImageBatchToList_,ImageListToBatch_,ComparingTwoFrames,LoadImages_,CompositeImages,GridDisplayAndSave,GridInput,ImagesPrompt,SaveImageAndMetadata,SaveImageToLocal,SplitImage,GridOutput,GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,CenterImage,AreaToMask,SmoothMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
# from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
@@ -1053,6 +1057,7 @@ NODE_CLASS_MAPPINGS = {
"ImageListToBatch_":ImageListToBatch_,
"ImageBatchToList_":ImageBatchToList_,
"CompositeImages_":CompositeImages,
"DepthViewer": DepthViewer_,
"SplitImage":SplitImage,
"CenterImage":CenterImage,
"GridOutput":GridOutput,
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4060 -449
View File
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -694,18 +694,20 @@ class JsonRepair:
return {
"required": {
"json_string":("STRING", {"forceInput": True,}),
"key":("STRING", {"multiline": False,"dynamicPrompts": False,"default": ""}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("STRING",)
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("json_string","value",)
FUNCTION = "run"
# OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,)
OUTPUT_IS_LIST = (False,False,)
CATEGORY = "♾️Mixlab/GPT"
def run(self, json_string):
def run(self, json_string,key=""):
json_string=extract_json_strings(json_string)
# print(json_string)
@@ -714,7 +716,11 @@ class JsonRepair:
# 将 JSON 字符串解析为 Python 对象
data = json.loads(good_json_string)
v=""
if key!="" and (key in data):
v=data[key]
# 将 Python 对象转换回 JSON 字符串,确保中文字符不被转义
json_str_with_chinese = json.dumps(data, ensure_ascii=False)
return (json_str_with_chinese,)
return (json_str_with_chinese,v,)
+179 -16
View File
@@ -8,6 +8,7 @@ from PIL.PngImagePlugin import PngInfo
import base64,os,random
from io import BytesIO
import folder_paths
import node_helpers
import json,io
import comfy.utils
from comfy.cli_args import args
@@ -491,6 +492,53 @@ def load_image(fp,white_bg=False):
return images
# 读取图片数据,转成tensor
def load_image_to_tensor( image):
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
def load_image_and_mask_from_url(url, timeout=10):
# Load the image from the URL
response = requests.get(url, timeout=timeout)
@@ -1687,28 +1735,51 @@ class Image3D:
def run(self,upload,material=None):
# print('material',material)
# print(upload )
image = base64_to_image(upload['image'])
mat=None
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
# 截取的系列角度截图
images=upload['images'] if "images" in upload else []
mask = image.split()[3]
image=image.convert('RGB')
ims=[]
for im in images:
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
mask=mask.convert('L')
mask=None
bg_image=None
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mat=None
# 如果没有系列截图
if len(ims)==0:
# 这个是3d模型当前截图
image = base64_to_image(upload['image'])
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
mask = image.split()[3]
image=image.convert('RGB')
mask=mask.convert('L')
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mask=pil2tensor(mask)
image=pil2tensor(image)
mask=pil2tensor(mask)
image=pil2tensor(image)
else:
image = torch.cat(ims, dim=0)
m=[]
if not material is None:
@@ -3220,3 +3291,95 @@ class ImageListToBatch_:
out = torch.cat(out, dim=0)
return (out,)
# https://github.com/gokayfem/ComfyUI-Depth-Visualization?tab=readme-ov-file
class DepthViewer_:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"depth_map": ("IMAGE",),
},
"optional":{
"frames":("IMAGEBASE64",),
},
}
def __init__(self):
self.saved_reference = []
self.saved_depth = []
self.full_output_folder,self.filename,self.counter, self.subfolder, self.filename_prefix = folder_paths.get_save_image_path(
"imagesave",
folder_paths.get_output_directory())
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("frames",)
OUTPUT_NODE = True
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/3D"
def run(self, image, depth_map,frames=None):
self.saved_reference.clear()
self.saved_depth.clear()
image = image[0].detach().cpu().numpy()
depth = depth_map[0].detach().cpu().numpy()
image = Image.fromarray(np.clip(255. * image, 0, 255).astype(np.uint8)).convert('RGB')
depth = Image.fromarray(np.clip(255. * depth, 0, 255).astype(np.uint8))
return self.display([image], [depth],frames)
def display(self, reference_image, depth_map,frames):
for (batch_number, (single_image, single_depth)) in enumerate(zip(reference_image, depth_map)):
filename_with_batch_num = self.filename.replace("%batch_num%", str(batch_number))
image_file = f"{filename_with_batch_num}_{self.counter:05}_reference.png"
single_image.save(os.path.join(self.full_output_folder, image_file))
depth_file = f"{filename_with_batch_num}_{self.counter:05}_depth.png"
single_depth.save(os.path.join(self.full_output_folder, depth_file))
self.saved_reference.append({
"filename": image_file,
"subfolder": self.subfolder,
"type": "output"
})
self.saved_depth.append({
"filename": depth_file,
"subfolder": self.subfolder,
"type": "output"
})
self.counter += 1
ims=[]
image1 = Image.new('RGB', (512, 512), color='black')
image1=pil2tensor(image1)
if frames!=None:
for im in frames['images']:
# print(im)
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
if len(ims)>0:
image1 = ims[0]
for image2 in ims[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
return {"ui": {"reference_image": self.saved_reference, "depth_map": self.saved_depth}, "result": (image1,)}
+2 -2
View File
@@ -16,7 +16,7 @@ def tensor2pil(image):
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def load_image( image):
def load_image_to_tensor( image):
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
@@ -88,7 +88,7 @@ class P5Input:
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image(im['name'])
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
if len(ims)==0:
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-mixlab-nodes"
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
version = "0.35.2"
version = "0.37.0"
license = "MIT"
dependencies = ["numpy", "pyOpenSSL", "watchdog", "opencv-python-headless", "matplotlib", "openai", "simple-lama-inpainting", "clip-interrogator==0.6.0", "transformers>=4.36.0", "lark-parser", "imageio-ffmpeg", "rembg[gpu]", "omegaconf==2.3.0", "Pillow>=9.5.0", "einops==0.7.0", "trimesh>=4.0.5", "huggingface-hub", "scikit-image"]
+937 -507
View File
File diff suppressed because it is too large Load Diff
+344 -236
View File
@@ -26,7 +26,8 @@ const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
async function uploadImage (blob, fileType = '.svg', filename) {
async function uploadImage_ (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
@@ -41,13 +42,17 @@ async function uploadImage (blob, fileType = '.svg', filename) {
// console.log(resp)
let data = await resp.json()
return data
}
async function uploadImage (blob, fileType = '.svg', filename) {
let data = await uploadImage_(blob, fileType, filename)
let { name, subfolder } = data
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
return src
}
@@ -171,6 +176,42 @@ async function changeMaterial (
targetMaterial.pbrMetallicRoughness.baseColorTexture.setTexture(targetTexture)
}
function inputFileClick (isFileURL = false, isGlb = false) {
return new Promise((res, rej) => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
input.accept = isGlb ? '.glb' : 'image/*'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
if (isFileURL) {
res(URL.createObjectURL(file))
return
}
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
input.remove()
res(base64)
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
}
app.registerExtension({
name: 'Mixlab.3D.3DImage',
async getCustomWidgets (app) {
@@ -189,7 +230,7 @@ app.registerExtension({
let d = getLocalData('_mixlab_3d_image')
// console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let { url, bg, material, images } = d[node.id]
let data = {}
if (url) {
data.image = await parseImage(url)
@@ -205,6 +246,10 @@ app.registerExtension({
data.material = await parseImage(material)
}
if (images) {
data.images = images
}
return JSON.parse(JSON.stringify(data))
} else {
return {}
@@ -243,39 +288,29 @@ app.registerExtension({
const inputDiv = (key, placeholder, preview) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
const ip = document.createElement('button')
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
width: 100px;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
ip.innerText = placeholder
div.appendChild(ip)
let that = this,
filename = new Date().getTime()
let that = this
ip.addEventListener('change', async event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
ip.addEventListener('click', async event => {
let fileURL = await inputFileClick(true, true)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
oncontextmenu="return false;"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
@@ -285,230 +320,303 @@ app.registerExtension({
<div>Variant: <select class="variant"></select></div>
<div>Material: <select class="material"></select></div>
<div>Material: <div class="material_img"> </div></div>
<div><button class="bg">BG</button></div>
<div>
<button class="bg">BG</button>
</div>
<div>
<input class="ddcap_step" type="number" min="1" max="20" step="1" value="1">
<input class="total_images" type="number" min="1" max="180" step="1" value="40">
<input class="ddcap_range" type="range" min="-180" max="180" step="1" value="0">
<input class="ddcap_range_top" type="range" min="-180" max="180" step="1" value="0">
<button class="ddcap">Capture Rotational Screenshots</button></div>
<div><button class="export">Export GLB</button></div>
</div></model-viewer>`
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob, '.png')
// console.log(url)
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute(
'data-index'
)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
// 以文本形式读取文件
reader.readAsDataURL(file)
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
const ddcap_step = preview.querySelector('.ddcap_step')
const total_images = preview.querySelector('.total_images')
const ddcap_range = preview.querySelector('.ddcap_range')
const ddcap_range_top = preview.querySelector('.ddcap_range_top')
const ddCap = preview.querySelector('.ddcap')
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
return setTimeout(() => {
res(t)
}, t)
})
}
async function captureImage (isUrl = true) {
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
if (isUrl) return await uploadImage(blob, '.png')
return await uploadImage_(blob, '.png')
}
async function captureImages (angleIncrement = 1, totalImages = 12) {
// 记录初始旋转角度
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
console.log(
'#captureImages',
initialCameraOrbit,
angleIncrement * totalImages
)
// const totalImages = 12
// const angleIncrement = totalRotation / totalImages // Each increment in degrees
let currentAngle =
Number(initialCameraOrbit[0].replace('deg', '')) -
(angleIncrement * totalImages) / 2 // Start from the leftmost angle
let frames = []
modelViewerVariants.removeAttribute('camera-controls')
for (let i = 0; i < totalImages; i++) {
modelViewerVariants.cameraOrbit = `${currentAngle}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
await sleep(1000)
console.log(`Capturing image at angle: ${currentAngle}deg`)
let file = await captureImage(false)
frames.push(file)
currentAngle += angleIncrement
}
await sleep(1000)
// 恢复到初始旋转角度
modelViewerVariants.cameraOrbit = initialCameraOrbit.join(' ')
modelViewerVariants.setAttribute('camera-controls', '')
return frames
}
ddCap.addEventListener('click', async e => {
const angleIncrement = Number(ddcap_step.value),
totalImages = Number(total_images.value)
let images = await captureImages(angleIncrement, totalImages)
// console.log(images)
let dd = getLocalData(key)
dd[that.id].images = images
setLocalDataOfWin(key, dd)
})
ddcap_range.addEventListener('input', async e => {
// console.log(ddcap_range.value)
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
modelViewerVariants.cameraOrbit = `${ddcap_range.value}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
modelViewerVariants.setAttribute('camera-controls', '')
})
ddcap_range_top.addEventListener('input', async e => {
// console.log(ddcap_range.value)
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
modelViewerVariants.cameraOrbit = `${initialCameraOrbit[0]} ${ddcap_range_top.value}deg ${initialCameraOrbit[2]}`
modelViewerVariants.setAttribute('camera-controls', '')
})
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 48}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(modelViewerVariants, selectMaterial, material_img)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await captureImage()
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute('data-index')
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
//更新bg
const updateBgData = (id, key, url, w, h) => {
let dd = getLocalData(key)
// console.log(dd[that.id],url)
if (!dd[id]) dd[id] = { url: '', bg: url }
dd[id] = {
...dd[id],
bg: url,
bg_w: w,
bg_h: h
}
setLocalDataOfWin(key, dd)
}
bg.addEventListener('click', async () => {
//更新bg
updateBgData(that.id, key, '', 0, 0)
preview.style.backgroundImage = 'none'
let base64 = await inputFileClick(false, false)
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
//更新bg
updateBgData(
that.id,
key,
bg_url,
bg_img.naturalWidth,
bg_img.naturalHeight
)
// 更新尺寸
let w = that.size[0] - 48,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 48,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
})
return div
}
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `margin-top: 12px;display: flex;
preview.style = `margin-top: 12px;
display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
align-items: center;background-repeat: no-repeat;
background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
@@ -527,7 +635,7 @@ app.registerExtension({
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
let w = that.size[0] - 48,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
@@ -561,7 +669,7 @@ app.registerExtension({
const r = onExecuted?.apply?.(this, arguments)
let div = this.widgets.filter(d => d.div)[0]?.div
console.log('Test', this.widgets)
// console.log('Test', this.widgets)
let material = message.material[0]
if (material) {
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.35.1'
const version = 'v0.37.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+10 -2
View File
@@ -142,7 +142,14 @@ async function getPromptResult (category) {
}
// 新的运行工作流的接口
function queuePromptNew (filename, category, seed, input, client_id,apps=null) {
function queuePromptNew (
filename,
category,
seed,
input,
client_id,
apps = null
) {
let url = get_url()
// var filename = "Text-to-Image_1.json", category = "";
@@ -325,8 +332,9 @@ function convertImageToBlackBasedOnAlpha (image) {
const alpha = pixels[i + 3]
if (alpha !== 0) {
// Set non-transparent pixels to black
// 蒙版是黑色?
pixels[i] = 0 // Red
pixels[i + 1] = 0 // Green
pixels[i + 1] = 255 // Green
pixels[i + 2] = 0 // Blue
}
}
+1
View File
@@ -681,6 +681,7 @@ const addBase64ToWidgetForLoadImagesToBatch = (
imagesWidget,
imagesDiv
) => {
if(!imagesWidget.value.base64) imagesWidget.value.base64=[]
imagesWidget.value.base64.push(base64)
let im = createInputImageForBatch(base64, imagesWidget)
imagesDiv.appendChild(im)
+10 -5
View File
@@ -94,7 +94,7 @@ const p5InputNode = {
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'P5Input') {
console.log('P5Input')
// console.log('P5Input')
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
@@ -171,6 +171,7 @@ const p5InputNode = {
let widget = node.widgets?.filter(w => w.name == 'image_base64')[0]
let framesWidget = node.widgets?.filter(w => w.name == 'frames')[0]
if (node.type === 'P5Input' && widget) {
console.log('#nodeCreated P5Input')
if (framesWidget && !framesWidget.value)
framesWidget.value = { images: [] }
@@ -185,15 +186,17 @@ const p5InputNode = {
// 监听来自iframe的消息
const ms = async event => {
const data = event.data
console.log('#P5 Input #', data)
if (
data.from === 'p5.widget' &&
data.status === 'save' &&
data.frames &&
data.frames.length > 0 &&
data.nodeId == nodeId
data.frames.length >= 0 &&
data.nodeId == nodeId &&
data.id != framesWidget.value.id
) {
const frames = data.frames
console.log(frames.length, nodeId)
//workflow会存储到local,会卡死
framesWidget.value.images = []
for (const f of frames) {
@@ -201,10 +204,12 @@ const p5InputNode = {
framesWidget.value.images.push(file)
}
// framesWidget.value.base64 = frames
framesWidget.value._seed = Math.random()
// framesWidget.value._seed = Math.random()
node.title = 'P5 Input #' + frames.length
framesWidget.value.id = data.id
}
}
window.addEventListener('message', ms)
}
}, 1000)
+237
View File
@@ -0,0 +1,237 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
return data
}
// 上传得到url
async function uploadBase64ToFile (base64) {
let bg_blob = await base64ToBlobFromURL(base64)
let url = await uploadImage(bg_blob, '.png')
return url
}
class Visualizer {
constructor (node, container, visualSrc) {
this.node = node
this.iframe = document.createElement('iframe')
Object.assign(this.iframe, {
scrolling: 'no',
overflow: 'hidden'
})
this.iframe.src = '/extensions/comfyui-mixlab-nodes/' + visualSrc + '.html'
console.log('#Visualizer', container, this.iframe)
container.appendChild(this.iframe)
}
updateVisual (params) {
console.log('#updateVisual', params, this.iframe)
// const iframeDocument = this.iframe.contentWindow.document
// const previewScript = iframeDocument.getElementById('visualizer')
// previewScript.setAttribute(
// 'reference_image',
// JSON.stringify(params.reference_image)
// )
// previewScript.setAttribute('depth_map', JSON.stringify(params.depth_map))
// Update the reference image and depth map
this.iframe.contentWindow.postMessage(params, '*')
}
remove () {
this.container.remove()
}
}
function createVisualizer (node, inputName, typeName, inputData, app) {
node.name = inputName
const widget = {
type: typeName,
name: 'preview3d',
callback: () => {},
draw: function (ctx, node, widgetWidth, widgetY, widgetHeight) {
const margin = 10
const top_offset = 5
const visible = app.canvas.ds.scale > 0.5 && this.type === typeName
const w = widgetWidth - margin * 4
const clientRectBound = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
clientRectBound.width / ctx.canvas.width,
clientRectBound.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + widgetY)
Object.assign(this.visualizer.style, {
left: `${transform.a * margin + transform.e}px`,
top: `${transform.d + transform.f + top_offset}px`,
width: `${w * transform.a}px`,
height: `${
w * transform.d - widgetHeight - margin * 15 * transform.d
}px`,
position: 'absolute',
overflow: 'hidden',
zIndex: app.graph._nodes.indexOf(node)
})
Object.assign(this.visualizer.children[0].style, {
transformOrigin: '50% 50%',
width: '100%',
height: '100%',
border: '0 none'
})
this.visualizer.hidden = !visible
}
}
const container = document.createElement('div')
container.id = `Comfy3D_${inputName}`
node.visualizer = new Visualizer(node, container, typeName)
widget.visualizer = container
widget.parent = node
document.body.appendChild(widget.visualizer)
node.addCustomWidget(widget)
node.updateParameters = params => {
// console.log('#updateParameters', params)
params.id = node.id
// node.visualizer = new Visualizer(node, container, typeName)
node.visualizer.updateVisual(params)
}
// Events for drawing backgound
node.onDrawBackground = function (ctx) {
if (!this.flags.collapsed) {
node.visualizer.iframe.hidden = false
} else {
node.visualizer.iframe.hidden = true
}
}
// Make sure visualization iframe is always inside the node when resize the node
node.onResize = function () {
let [w, h] = this.size
if (w <= 600) w = 600
if (h <= 500) h = 500
if (w > 600) {
h = w - 100
}
this.size = [w, h]
}
// Events for remove nodes
node.onRemoved = () => {
for (let w in node.widgets) {
if (node.widgets[w].visualizer) {
node.widgets[w].visualizer.remove()
}
}
}
return {
widget: widget
}
}
function registerVisualizer (nodeType, nodeData, nodeClassName, typeName) {
if (nodeData.name == nodeClassName) {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined
let Preview3DNode = app.graph._nodes.filter(
wi => wi.type == nodeClassName
)
let nodeName = `Preview3DNode_${Preview3DNode.length}`
const result = await createVisualizer.apply(this, [
this,
nodeName,
typeName,
{},
app
])
this.setSize([600, 500])
return r
}
nodeType.prototype.onExecuted = async function (message) {
// Check if reference image and depth map are available
if (message.reference_image && message.depth_map) {
const params = {}
params.reference_image = message.reference_image[0]
params.depth_map = message.depth_map[0]
this.updateParameters(params)
}
}
}
}
app.registerExtension({
name: 'Mixlab.nodes.depthviewer',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
registerVisualizer(nodeType, nodeData, 'DepthViewer', 'threeVisualizer')
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
let widget = node.widgets?.filter(w => w.name == 'preview3d')[0]
let framesWidget = node.widgets?.filter(w => w.name == 'frames')[0]
if (node.type === 'DepthViewer' && widget) {
let nodeId = node.id
//延迟才能获得this.id
widget.visualizer.querySelector('iframe').src += '?id=' + nodeId
// console.log('DepthViewer',widget)
window.addEventListener('message', async event => {
// 检查消息的来源,确保消息来自可信的源
console.log(event)
const { id, imgs } = event.data
if (id == nodeId) {
framesWidget.value = { images: [] }
for (const f of imgs) {
let file = await uploadBase64ToFile(f)
framesWidget.value.images.push(file)
}
// framesWidget.value.base64 = frames
framesWidget.value._seed = Math.random()
node.title = 'Input #' + imgs.length
}
})
}
}, 1000)
}
})
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
/**
* https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
*/
import {
BackSide,
BoxGeometry,
Mesh,
MeshBasicMaterial,
MeshStandardMaterial,
PointLight,
Scene,
} from './three.module.js';
class RoomEnvironment extends Scene {
constructor( renderer = null ) {
super();
const geometry = new BoxGeometry();
geometry.deleteAttribute( 'uv' );
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
const boxMaterial = new MeshStandardMaterial();
const mainLight = new PointLight( 0xffffff, 900, 28, 2 );
mainLight.position.set( 0.418, 16.199, 0.300 );
this.add( mainLight );
const room = new Mesh( geometry, roomMaterial );
room.position.set( - 0.757, 13.219, 0.717 );
room.scale.set( 31.713, 28.305, 28.591 );
this.add( room );
const box1 = new Mesh( geometry, boxMaterial );
box1.position.set( - 10.906, 2.009, 1.846 );
box1.rotation.set( 0, - 0.195, 0 );
box1.scale.set( 2.328, 7.905, 4.651 );
this.add( box1 );
const box2 = new Mesh( geometry, boxMaterial );
box2.position.set( - 5.607, - 0.754, - 0.758 );
box2.rotation.set( 0, 0.994, 0 );
box2.scale.set( 1.970, 1.534, 3.955 );
this.add( box2 );
const box3 = new Mesh( geometry, boxMaterial );
box3.position.set( 6.167, 0.857, 7.803 );
box3.rotation.set( 0, 0.561, 0 );
box3.scale.set( 3.927, 6.285, 3.687 );
this.add( box3 );
const box4 = new Mesh( geometry, boxMaterial );
box4.position.set( - 2.017, 0.018, 6.124 );
box4.rotation.set( 0, 0.333, 0 );
box4.scale.set( 2.002, 4.566, 2.064 );
this.add( box4 );
const box5 = new Mesh( geometry, boxMaterial );
box5.position.set( 2.291, - 0.756, - 2.621 );
box5.rotation.set( 0, - 0.286, 0 );
box5.scale.set( 1.546, 1.552, 1.496 );
this.add( box5 );
const box6 = new Mesh( geometry, boxMaterial );
box6.position.set( - 2.193, - 0.369, - 5.547 );
box6.rotation.set( 0, 0.516, 0 );
box6.scale.set( 3.875, 3.487, 2.986 );
this.add( box6 );
// -x right
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light1.position.set( - 16.116, 14.37, 8.208 );
light1.scale.set( 0.1, 2.428, 2.739 );
this.add( light1 );
// -x left
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light2.position.set( - 16.109, 18.021, - 8.207 );
light2.scale.set( 0.1, 2.425, 2.751 );
this.add( light2 );
// +x
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
light3.position.set( 14.904, 12.198, - 1.832 );
light3.scale.set( 0.15, 4.265, 6.331 );
this.add( light3 );
// +z
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
light4.position.set( - 0.462, 8.89, 14.520 );
light4.scale.set( 4.38, 5.441, 0.088 );
this.add( light4 );
// -z
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
light5.position.set( 3.235, 11.486, - 12.541 );
light5.scale.set( 2.5, 2.0, 0.1 );
this.add( light5 );
// +y
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
light6.position.set( 0.0, 20.0, 0.0 );
light6.scale.set( 1.0, 0.1, 1.0 );
this.add( light6 );
}
dispose() {
const resources = new Set();
this.traverse( ( object ) => {
if ( object.isMesh ) {
resources.add( object.geometry );
resources.add( object.material );
}
} );
for ( const resource of resources ) {
resource.dispose();
}
}
}
function createAreaLightMaterial( intensity ) {
const material = new MeshBasicMaterial();
material.color.setScalar( intensity );
return material;
}
export { RoomEnvironment };
File diff suppressed because one or more lines are too long
+316
View File
@@ -0,0 +1,316 @@
import * as THREE from './three/three.module.js'
import { api } from '../../../scripts/api.js'
import { OrbitControls } from './three/OrbitControls.js'
import { RoomEnvironment } from './three/RoomEnvironment.js'
const visualizer = document.getElementById('visualizer')
const container = document.getElementById('container')
const progressDialog = document.getElementById('progress-dialog')
const progressIndicator = document.getElementById('progress-indicator')
const renderer = new THREE.WebGLRenderer({
antialias: true,
extensions: {
derivatives: true
}
})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
if (container) container.appendChild(renderer.domElement)
const pmremGenerator = new THREE.PMREMGenerator(renderer)
// scene
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
scene.environment = pmremGenerator.fromScene(
new RoomEnvironment(renderer),
0.04
).texture
const ambientLight = new THREE.AmbientLight(0xffffff)
const camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.set(0, 0, 10)
const pointLight = new THREE.PointLight(0xffffff, 15)
camera.add(pointLight)
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()
controls.enablePan = true
controls.enableDamping = true
// Handle window resize event
window.onresize = function () {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
var lastReferenceImage = ''
var lastDepthMap = ''
var needUpdate = false
function frameUpdate () {
var referenceImage = visualizer?.getAttribute('reference_image')
var depthMap = visualizer?.getAttribute('depth_map')
if (referenceImage == lastReferenceImage && depthMap == lastDepthMap) {
if (needUpdate) {
controls.update()
renderer.render(scene, camera)
}
requestAnimationFrame(frameUpdate)
} else {
needUpdate = false
scene.clear()
if (progressDialog) progressDialog.open = true
lastReferenceImage = referenceImage
lastDepthMap = depthMap
if (lastReferenceImage && lastReferenceImage != 'undefined') {
// console.log('lastReferenceImage',typeof(lastReferenceImage),lastDepthMap)
main(JSON.parse(lastReferenceImage), JSON.parse(lastDepthMap))
}
}
}
const onProgress = function (xhr) {
if (xhr.lengthComputable) {
progressIndicator.value = (xhr.loaded / xhr.total) * 100
}
}
const onError = function (e) {
console.error(e)
}
async function main (referenceImageParams, depthMapParams) {
let referenceTexture, depthTexture
let imageWidth = 10 // Default width
let imageHeight = 10 // Default height, will be updated based on the image's aspect ratio
// console.log('#referenceImageParams', referenceImageParams)
if (referenceImageParams?.filename) {
const referenceImageUrl = api
.apiURL('/view?' + new URLSearchParams(referenceImageParams))
.replace(/extensions.*\//, '')
const referenceImageExt = referenceImageParams.filename.slice(
referenceImageParams.filename.lastIndexOf('.') + 1
)
if (
referenceImageExt === 'png' ||
referenceImageExt === 'jpg' ||
referenceImageExt === 'jpeg'
) {
const referenceImageLoader = new THREE.TextureLoader()
referenceTexture = await new Promise((resolve, reject) => {
referenceImageLoader.load(
referenceImageUrl,
texture => {
// Once the image is loaded, update the width and height based on the image's aspect ratio
imageWidth = 10 // Keep the width as 10
imageHeight = texture.image.height / (texture.image.width / 10)
resolve(texture)
},
undefined,
reject
)
})
}
}
if (depthMapParams?.filename) {
const depthMapUrl = api
.apiURL('/view?' + new URLSearchParams(depthMapParams))
.replace(/extensions.*\//, '')
const depthMapExt = depthMapParams.filename.slice(
depthMapParams.filename.lastIndexOf('.') + 1
)
if (
depthMapExt === 'png' ||
depthMapExt === 'jpg' ||
depthMapExt === 'jpeg'
) {
const depthMapLoader = new THREE.TextureLoader()
depthTexture = await depthMapLoader.loadAsync(depthMapUrl)
}
}
if (referenceTexture && depthTexture) {
const depthMaterial = new THREE.ShaderMaterial({
uniforms: {
referenceTexture: { value: referenceTexture },
depthTexture: { value: depthTexture },
depthScale: { value: 5.0 },
ambientLightColor: { value: new THREE.Color(0.2, 0.2, 0.2) },
lightPosition: { value: new THREE.Vector3(2, 2, 2) },
lightColor: { value: new THREE.Color(1, 1, 1) },
lightIntensity: { value: 1.0 },
shininess: { value: 30 }
},
vertexShader: `
uniform sampler2D depthTexture;
uniform float depthScale;
varying vec2 vUv;
varying float vDepth;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vUv = uv;
float depth = texture2D(depthTexture, uv).r;
vec3 displacement = normal * depth * depthScale;
vec3 displacedPosition = position + displacement;
vec4 worldPosition = modelMatrix * vec4(displacedPosition, 1.0);
vNormal = normalize(normalMatrix * normal);
vViewPosition = (viewMatrix * worldPosition).xyz;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
vDepth = depth;
}
`,
fragmentShader: `
uniform sampler2D referenceTexture;
varying vec2 vUv;
varying float vDepth;
void main() {
vec4 referenceColor = texture2D(referenceTexture, vUv);
// Directly use reference color without fog
gl_FragColor = referenceColor;
}
`
})
const planeGeometry = new THREE.PlaneGeometry(
imageWidth,
imageHeight,
200,
200
)
const depthMesh = new THREE.Mesh(planeGeometry, depthMaterial)
scene.add(depthMesh)
}
needUpdate = true
scene.add(ambientLight)
scene.add(camera)
progressDialog?.close()
frameUpdate()
}
document
.getElementById('screenshotButton')
?.addEventListener('click', takeScreenshot)
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
setTimeout(() => res(1), t)
})
}
// 方法:旋转摄像机并拍摄图片 // 每次旋转的角度增量,转换为弧度
async function captureImages (
totalFrames = 40,
angleIncrement = THREE.MathUtils.degToRad(0.5)
) {
// 计算场景中所有物体的中心点
const box = new THREE.Box3().setFromObject(scene)
const center = new THREE.Vector3()
box.getCenter(center)
// 计算当前相机距离中心点的半径
const radius = camera.position.distanceTo(center)
// 存储图片的数组
let images = []
// 记录初始相机位置和朝向
const initialPosition = camera.position.clone()
const initialTarget = center.clone()
// 计算当前相机的初始角度
const initialAngle = Math.atan2(
camera.position.z - center.z,
camera.position.x - center.x
)
// 起始角度为从当前角度往左旋转 20 度的位置
const startAngle = initialAngle - (angleIncrement * totalFrames) / 2
for (let i = 0; i < totalFrames; i++) {
const angle = startAngle + i * angleIncrement
// 计算相机的位置
camera.position.x = center.x + radius * Math.cos(angle)
camera.position.z = center.z + radius * Math.sin(angle)
camera.position.y = initialPosition.y // 保持相机高度不变
camera.lookAt(center) // 相机看向中心点
// 渲染当前帧
renderer.render(scene, camera)
// 将当前帧保存为图片
const imgData = renderer.domElement.toDataURL('image/png')
images.push(imgData)
// 等待一段时间
await new Promise(resolve => setTimeout(resolve, 500))
}
// 恢复相机到初始位置和朝向
camera.position.copy(initialPosition)
camera.lookAt(initialTarget)
return images
}
async function takeScreenshot () {
// 更新相机的矩阵,以确保其世界矩阵是最新的
camera.updateMatrixWorld()
const imgs = await captureImages()
// 获取当前网页的 URL
const currentUrl = window.location.href
// 创建一个 URL 对象
const url = new URL(currentUrl)
// 使用 URLSearchParams 获取参数
const params = new URLSearchParams(url.search)
// 获取参数 'id' 的值
const id = params.get('id')
window.parent.postMessage({ imgs, id }, '*')
}
main()
window.addEventListener('message', event => {
// 这里可以添加对来源的验证,以确保安全
// console.log('Message received from parent page:', event.data)
let { reference_image, depth_map } = event.data
if (reference_image && depth_map) {
visualizer?.setAttribute('reference_image', JSON.stringify(reference_image))
visualizer?.setAttribute('depth_map', JSON.stringify(depth_map))
frameUpdate()
}
})
+55 -35
View File
@@ -73,28 +73,22 @@
}
initialContent = initialContent.replace(/\r\n/g, '\n').trim();
ReactDOM.render(React.createElement(app_1.default, {initialContent: initialContent, autosaver: new autosaver_1.SessionStorageAutosaver(id), baseSketchURL: baseSketchURL, p5version: p5version, previewWidth: previewWidth, maxRunTime: maxRunTime, autoplay: autoplay}), document.getElementById('app-holder'));
// 监听来自iframe的消息
window.addEventListener('message', function (event) {
var data = event.data;
// console.log("#main", data)
if (data.from === 'p5.widget' && data.status === 'save') {
var frames_1 = data.frames;
window.parent.postMessage({
frames: frames_1,
from: 'p5.widget',
status: 'save',
_from: "main"
}, '*');
}
});
}
window.addEventListener('load', start);
// 监听来自iframe的消息
window.addEventListener('message', function (event) {
var data = event.data;
if (data.from === 'p5.widget' && data.status === 'save') {
var frames_1 = data.frames;
window.parent.postMessage({
frames: frames_1,
from: 'p5.widget',
status: 'save'
}, '*');
}
// if (data.from === 'p5.widget' && data.status === 'capture') {
// window.parent.postMessage({
// from: 'p5.widget',
// status: 'capture',
// frameCount: data.frameCount,
// maxCount: data.maxCount,
// }, '*');
// }
});
/***/ }),
@@ -23670,7 +23664,7 @@
/***/ (function(module, exports) {
"use strict";
exports.P5_VERSION = '0.4.23';
exports.P5_VERSION = '1.10.0';
exports.PREVIEW_WIDTH = 150;
exports.HEIGHT = 300;
exports.MAX_RUN_TIME = 1000;
@@ -23766,6 +23760,10 @@
};
this.handleStopClick = function () {
_this.setState({ isPlaying: false });
window.parent.postMessage({
from: 'p5.widget',
status: 'stop'
}, '*');
};
this.handleUndoClick = function () {
_this.refs.editor.undo();
@@ -23956,7 +23954,7 @@
}
Toolbar.prototype.render = function () {
return (React.createElement("div", {className: "toolbar"}, React.createElement("a", {className: "p5-logo", href: "http://p5js.org/", target: "_blank"}, React.createElement("img", {src: "static/img/p5js-beta.svg", alt: "p5js.org"})), React.createElement("button", {onClick: this.props.onPlayClick}, React.createElement(OpenIconicMediaPlay, null), "Play"), this.props.onStopClick
? React.createElement("button", {onClick: this.props.onStopClick}, React.createElement(OpenIconicMediaStop, null), "Stop ", this.props.progress)
? React.createElement("button", {onClick: this.props.onStopClick}, React.createElement(OpenIconicMediaStop, null), "Stop")
: null, this.props.onUndoClick
? React.createElement("button", {onClick: this.props.onUndoClick}, React.createElement(OpenIconicActionUndo, null), "Undo")
: null, this.props.onRedoClick
@@ -34945,8 +34943,7 @@
content = implicit_sketch_1.default(content);
try {
content = falafel_1.default(content, {}, loop_inserter_1.default(function (node) {
return LOOP_CHECK_FUNC_NAME + "(" +
JSON.stringify(node.range) + ");";
return LOOP_CHECK_FUNC_NAME + "(" + JSON.stringify(node.range) + ");";
})).toString();
}
catch (e) {
@@ -34998,6 +34995,7 @@
"use strict";
var esprima = __webpack_require__(204);
var parse = esprima.parse;
var LOOP_CHECK_FUNC_NAME = '__loopCheck';
function default_1(src, opts, fn) {
if (typeof opts === 'function') {
fn = opts;
@@ -35080,7 +35078,6 @@
console.log('#Found draw function:', node);
var hasCapturerStart_1 = false;
var hasCapturerEnd_1 = false;
// Traverse the function body to check for `capturer_start()` and `capturer_end()`
(function checkCapturerCalls(bodyNode) {
if (bodyNode.type === 'CallExpression' &&
bodyNode.callee &&
@@ -40865,25 +40862,48 @@
"use strict";
function defaultInserter(name) {
return function (node) {
return name + "();";
return name + '();';
};
}
var LOOP_CHECK_FUNC_NAME = '__loopCheck';
function LoopInserter(fn) {
if (typeof (fn) == "string")
fn = defaultInserter(fn);
return function (node) {
if (node.type == 'WhileStatement' ||
node.type == 'ForStatement' ||
node.type == 'DoWhileStatement') {
node.body.update('{ ' + fn(node) + node.body.source() + ' }');
return true;
if (node.type === 'ForStatement' ||
node.type === 'WhileStatement' ||
node.type === 'DoWhileStatement') {
var loopCheckCode = {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: LOOP_CHECK_FUNC_NAME
},
arguments: [
{
type: 'Literal',
value: JSON.stringify(node.range)
}
]
}
};
if (node.body.type === 'BlockStatement') {
// Insert the loop check at the beginning of the block
node.body.body.unshift(loopCheckCode);
}
else {
// Wrap the single statement body in a block and insert the loop check
node.body = {
type: 'BlockStatement',
body: [loopCheckCode, node.body]
};
}
}
return false;
return '';
};
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = LoopInserter;
;
/***/ }),
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<!-- <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet"> -->
<title>p5.js-widget</title>
<div id="app-holder"></div>
<script src="./main.bundle.js"></script>
+1 -1
View File
@@ -229,7 +229,7 @@
/***/ (function(module, exports) {
"use strict";
exports.P5_VERSION = '0.4.23';
exports.P5_VERSION = '1.10.0';
exports.PREVIEW_WIDTH = 150;
exports.HEIGHT = 300;
exports.MAX_RUN_TIME = 1000;
File diff suppressed because one or more lines are too long
@@ -54,7 +54,7 @@
cb = cb || (function () { });
script.onload = cb;
script.onerror = function () {
console.log("Failed to load script: " + url);
console.log('Failed to load script: ' + url);
};
script.setAttribute('src', url);
document.body.appendChild(script);
@@ -92,7 +92,7 @@
if (Date.now() - startTime > maxRunTime) {
self.wasTriggered = true;
loopCheckFailureRange = range;
throw new Error("Loop took over " + maxRunTime + " ms to run");
throw new Error('Loop took over ' + maxRunTime + ' ms to run');
}
};
setInterval(function () {
@@ -115,11 +115,12 @@
global.addEventListener('error', function (e) {
var message = e.message;
var line = undefined;
// console.log(message)
if (loopChecker.wasTriggered) {
message = "Your loop is taking too long to run.";
message = 'Your loop is taking too long to run.';
line = loopChecker.getLineNumber();
}
else if (typeof (e.lineno) === 'number' &&
else if (typeof e.lineno === 'number' &&
(e.filename === '' || e.filename === window.location.href)) {
line = e.lineno;
}
@@ -131,12 +132,15 @@
catch (e) { }
errorCb(message, line);
});
loadScripts([
p5url(p5version),
], function () {
loadScripts([p5url(p5version)], function () {
document.body.appendChild(sketchScript);
if (document.readyState === 'complete') {
new global.p5();
try {
new global.p5();
}
catch (e) {
console.error('Failed to initialize p5:', e);
}
}
});
}
File diff suppressed because one or more lines are too long
+25 -13
View File
@@ -3,14 +3,8 @@
<title>Preview</title>
<body>
<script src="./p5.js"></script>
<!-- <script src="./p5.js"></script> -->
<script src="./src/CCapture.js"></script>
<!-- <script src="src/gif.js"></script> -->
<!-- <script src="src/gif.worker.js"></script> -->
<!-- <script src="src/download.js"></script> -->
<!-- <script src="src/tar.js"></script> -->
<script>
let capturer = new CCapture({
format: 'png',
@@ -19,24 +13,42 @@
});
var capturer_start = () => {
if (frameCount === 1) {
var capturer_start = (t = 1) => {
if (frameCount === t && capturer) {
capturer.start();
}
}
var capturer_end = (t = 24) => {
if (frameCount < t) {
// console.log(frameCount < t, frameCount, t)
if (frameCount < t && capturer) {
capturer.capture(canvas, t);
} else if (frameCount === t) {
capturer.save(null);
} else if (capturer) {
capturer.save((frames) => {
window.parent.postMessage(
{
frames,
from: 'p5.widget',
status: 'save',
encoder: true
},
'*'
)
});
capturer.stop();
capturer = null;
}
}
// 监听来自iframe的消息
window.addEventListener('message', (event) => {
const data = event.data;
if (data.from === 'p5.widget' && data.status === 'stop') {
window.location.reload()
}
});
</script>
<script src="./preview-frame.bundle.js"></script>
File diff suppressed because it is too large Load Diff
-585
View File
@@ -1,585 +0,0 @@
/*
var vid = new Whammy.Video();
vid.add(canvas or data url)
vid.compile()
*/
window.Whammy = (function(){
// in this case, frames has a very specific meaning, which will be
// detailed once i finish writing the code
function toWebM(frames, outputAsArray){
var info = checkFrames(frames);
//max duration by cluster in milliseconds
var CLUSTER_MAX_DURATION = 30000;
var EBML = [
{
"id": 0x1a45dfa3, // EBML
"data": [
{
"data": 1,
"id": 0x4286 // EBMLVersion
},
{
"data": 1,
"id": 0x42f7 // EBMLReadVersion
},
{
"data": 4,
"id": 0x42f2 // EBMLMaxIDLength
},
{
"data": 8,
"id": 0x42f3 // EBMLMaxSizeLength
},
{
"data": "webm",
"id": 0x4282 // DocType
},
{
"data": 2,
"id": 0x4287 // DocTypeVersion
},
{
"data": 2,
"id": 0x4285 // DocTypeReadVersion
}
]
},
{
"id": 0x18538067, // Segment
"data": [
{
"id": 0x1549a966, // Info
"data": [
{
"data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
"id": 0x2ad7b1 // TimecodeScale
},
{
"data": "whammy",
"id": 0x4d80 // MuxingApp
},
{
"data": "whammy",
"id": 0x5741 // WritingApp
},
{
"data": doubleToString(info.duration),
"id": 0x4489 // Duration
}
]
},
{
"id": 0x1654ae6b, // Tracks
"data": [
{
"id": 0xae, // TrackEntry
"data": [
{
"data": 1,
"id": 0xd7 // TrackNumber
},
{
"data": 1,
"id": 0x73c5 // TrackUID
},
{
"data": 0,
"id": 0x9c // FlagLacing
},
{
"data": "und",
"id": 0x22b59c // Language
},
{
"data": "V_VP8",
"id": 0x86 // CodecID
},
{
"data": "VP8",
"id": 0x258688 // CodecName
},
{
"data": 1,
"id": 0x83 // TrackType
},
{
"id": 0xe0, // Video
"data": [
{
"data": info.width,
"id": 0xb0 // PixelWidth
},
{
"data": info.height,
"id": 0xba // PixelHeight
}
]
}
]
}
]
},
{
"id": 0x1c53bb6b, // Cues
"data": [
//cue insertion point
]
}
//cluster insertion point
]
}
];
var segment = EBML[1];
var cues = segment.data[2];
//Generate clusters (max duration)
var frameNumber = 0;
var clusterTimecode = 0;
while(frameNumber < frames.length){
var cuePoint = {
"id": 0xbb, // CuePoint
"data": [
{
"data": Math.round(clusterTimecode),
"id": 0xb3 // CueTime
},
{
"id": 0xb7, // CueTrackPositions
"data": [
{
"data": 1,
"id": 0xf7 // CueTrack
},
{
"data": 0, // to be filled in when we know it
"size": 8,
"id": 0xf1 // CueClusterPosition
}
]
}
]
};
cues.data.push(cuePoint);
var clusterFrames = [];
var clusterDuration = 0;
do {
clusterFrames.push(frames[frameNumber]);
clusterDuration += frames[frameNumber].duration;
frameNumber++;
}while(frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION);
var clusterCounter = 0;
var cluster = {
"id": 0x1f43b675, // Cluster
"data": [
{
"data": Math.round(clusterTimecode),
"id": 0xe7 // Timecode
}
].concat(clusterFrames.map(function(webp){
var block = makeSimpleBlock({
discardable: 0,
frame: webp.data.slice(4),
invisible: 0,
keyframe: 1,
lacing: 0,
trackNum: 1,
timecode: Math.round(clusterCounter)
});
clusterCounter += webp.duration;
return {
data: block,
id: 0xa3
};
}))
}
//Add cluster to segment
segment.data.push(cluster);
clusterTimecode += clusterDuration;
}
//First pass to compute cluster positions
var position = 0;
for(var i = 0; i < segment.data.length; i++){
if (i >= 3) {
cues.data[i-3].data[1].data[1].data = position;
}
var data = generateEBML([segment.data[i]], outputAsArray);
position += data.size || data.byteLength || data.length;
if (i != 2) { // not cues
//Save results to avoid having to encode everything twice
segment.data[i] = data;
}
}
return generateEBML(EBML, outputAsArray)
}
// sums the lengths of all the frames and gets the duration, woo
function checkFrames(frames){
var width = frames[0].width,
height = frames[0].height,
duration = frames[0].duration;
for(var i = 1; i < frames.length; i++){
if(frames[i].width != width) throw "Frame " + (i + 1) + " has a different width";
if(frames[i].height != height) throw "Frame " + (i + 1) + " has a different height";
if(frames[i].duration < 0 || frames[i].duration > 0x7fff) throw "Frame " + (i + 1) + " has a weird duration (must be between 0 and 32767)";
duration += frames[i].duration;
}
return {
duration: duration,
width: width,
height: height
};
}
function numToBuffer(num){
var parts = [];
while(num > 0){
parts.push(num & 0xff)
num = num >> 8
}
return new Uint8Array(parts.reverse());
}
function numToFixedBuffer(num, size){
var parts = new Uint8Array(size);
for(var i = size - 1; i >= 0; i--){
parts[i] = num & 0xff;
num = num >> 8;
}
return parts;
}
function strToBuffer(str){
// return new Blob([str]);
var arr = new Uint8Array(str.length);
for(var i = 0; i < str.length; i++){
arr[i] = str.charCodeAt(i)
}
return arr;
// this is slower
// return new Uint8Array(str.split('').map(function(e){
// return e.charCodeAt(0)
// }))
}
//sorry this is ugly, and sort of hard to understand exactly why this was done
// at all really, but the reason is that there's some code below that i dont really
// feel like understanding, and this is easier than using my brain.
function bitsToBuffer(bits){
var data = [];
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
bits = pad + bits;
for(var i = 0; i < bits.length; i+= 8){
data.push(parseInt(bits.substr(i,8),2))
}
return new Uint8Array(data);
}
function generateEBML(json, outputAsArray){
var ebml = [];
for(var i = 0; i < json.length; i++){
if (!('id' in json[i])){
//already encoded blob or byteArray
ebml.push(json[i]);
continue;
}
var data = json[i].data;
if(typeof data == 'object') data = generateEBML(data, outputAsArray);
if(typeof data == 'number') data = ('size' in json[i]) ? numToFixedBuffer(data, json[i].size) : bitsToBuffer(data.toString(2));
if(typeof data == 'string') data = strToBuffer(data);
if(data.length){
var z = z;
}
/*var len = data.size || data.byteLength || data.length;
var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
var size_str = len.toString(2);
var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
var size = (new Array(zeroes)).join('0') + '1' + padded;*/
var len = data.size || data.byteLength || data.length;
var zeroes = 0
for( var j = 56; j > 0; j-= 7 ) {
if( len > Math.pow( 2, j ) - 2 ) {
zeroes = j / 7
break
}
}
var size_str = len.toString(2);
var base = ( new Array( 8 * ( zeroes + 1 ) + 1 ) ).join( '0' )
var pre = ( new Array( zeroes + 1 ) ).join( '0' ) + 1;
var padded = base.substr( 0, base.length - size_str.length - pre.length ) + size_str;
var size = pre + padded;
//i actually dont quite understand what went on up there, so I'm not really
//going to fix this, i'm probably just going to write some hacky thing which
//converts that string into a buffer-esque thing
ebml.push(numToBuffer(json[i].id));
ebml.push(bitsToBuffer(size));
ebml.push(data)
}
//output as blob or byteArray
if(outputAsArray){
//convert ebml to an array
var buffer = toFlatArray(ebml)
return new Uint8Array(buffer);
}else{
return new Blob(ebml, {type: "video/webm"});
}
}
function toFlatArray(arr, outBuffer){
if(outBuffer == null){
outBuffer = [];
}
for(var i = 0; i < arr.length; i++){
if(typeof arr[i] == 'object'){
//an array
toFlatArray(arr[i], outBuffer)
}else{
//a simple element
outBuffer.push(arr[i]);
}
}
return outBuffer;
}
//OKAY, so the following two functions are the string-based old stuff, the reason they're
//still sort of in here, is that they're actually faster than the new blob stuff because
//getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the
// only browser which supports get as webp
//Converting between a string of 0010101001's and binary back and forth is probably inefficient
//TODO: get rid of this function
function toBinStr_old(bits){
var data = '';
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
bits = pad + bits;
for(var i = 0; i < bits.length; i+= 8){
data += String.fromCharCode(parseInt(bits.substr(i,8),2))
}
return data;
}
function generateEBML_old(json){
var ebml = '';
for(var i = 0; i < json.length; i++){
var data = json[i].data;
if(typeof data == 'object') data = generateEBML_old(data);
if(typeof data == 'number') data = toBinStr_old(data.toString(2));
var len = data.length;
var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
var size_str = len.toString(2);
var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
var size = (new Array(zeroes)).join('0') + '1' + padded;
ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data;
}
return ebml;
}
//woot, a function that's actually written for this project!
//this parses some json markup and makes it into that binary magic
//which can then get shoved into the matroska comtainer (peaceably)
function makeSimpleBlock(data){
var flags = 0;
if (data.keyframe) flags |= 128;
if (data.invisible) flags |= 8;
if (data.lacing) flags |= (data.lacing << 1);
if (data.discardable) flags |= 1;
if (data.trackNum > 127) {
throw "TrackNumber > 127 not supported";
}
var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e){
return String.fromCharCode(e)
}).join('') + data.frame;
return out;
}
// here's something else taken verbatim from weppy, awesome rite?
function parseWebP(riff){
var VP8 = riff.RIFF[0].WEBP[0];
var frame_start = VP8.indexOf('\x9d\x01\x2a'); //A VP8 keyframe starts with the 0x9d012a header
for(var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);
var width, horizontal_scale, height, vertical_scale, tmp;
//the code below is literally copied verbatim from the bitstream spec
tmp = (c[1] << 8) | c[0];
width = tmp & 0x3FFF;
horizontal_scale = tmp >> 14;
tmp = (c[3] << 8) | c[2];
height = tmp & 0x3FFF;
vertical_scale = tmp >> 14;
return {
width: width,
height: height,
data: VP8,
riff: riff
}
}
// i think i'm going off on a riff by pretending this is some known
// idiom which i'm making a casual and brilliant pun about, but since
// i can't find anything on google which conforms to this idiomatic
// usage, I'm assuming this is just a consequence of some psychotic
// break which makes me make up puns. well, enough riff-raff (aha a
// rescue of sorts), this function was ripped wholesale from weppy
function parseRIFF(string){
var offset = 0;
var chunks = {};
while (offset < string.length) {
var id = string.substr(offset, 4);
chunks[id] = chunks[id] || [];
if (id == 'RIFF' || id == 'LIST') {
var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i){
var unpadded = i.charCodeAt(0).toString(2);
return (new Array(8 - unpadded.length + 1)).join('0') + unpadded
}).join(''),2);
var data = string.substr(offset + 4 + 4, len);
offset += 4 + 4 + len;
chunks[id].push(parseRIFF(data));
} else if (id == 'WEBP') {
// Use (offset + 8) to skip past "VP8 "/"VP8L"/"VP8X" field after "WEBP"
chunks[id].push(string.substr(offset + 8));
offset = string.length;
} else {
// Unknown chunk type; push entire payload
chunks[id].push(string.substr(offset + 4));
offset = string.length;
}
}
return chunks;
}
// here's a little utility function that acts as a utility for other functions
// basically, the only purpose is for encoding "Duration", which is encoded as
// a double (considerably more difficult to encode than an integer)
function doubleToString(num){
return [].slice.call(
new Uint8Array(
(
new Float64Array([num]) //create a float64 array
).buffer) //extract the array buffer
, 0) // convert the Uint8Array into a regular array
.map(function(e){ //since it's a regular array, we can now use map
return String.fromCharCode(e) // encode all the bytes individually
})
.reverse() //correct the byte endianness (assume it's little endian for now)
.join('') // join the bytes in holy matrimony as a string
}
function WhammyVideo(speed, quality){ // a more abstract-ish API
this.frames = [];
this.duration = 1000 / speed;
this.quality = quality || 0.8;
}
WhammyVideo.prototype.add = function(frame, duration){
if(typeof duration != 'undefined' && this.duration) throw "you can't pass a duration if the fps is set";
if(typeof duration == 'undefined' && !this.duration) throw "if you don't have the fps set, you need to have durations here.";
if(frame.canvas){ //CanvasRenderingContext2D
frame = frame.canvas;
}
if(frame.toDataURL){
// frame = frame.toDataURL('image/webp', this.quality);
// quickly store image data so we don't block cpu. encode in compile method.
frame = frame.getContext('2d').getImageData(0, 0, frame.width, frame.height);
}else if(typeof frame != "string"){
throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"
}
if (typeof frame === "string" && !(/^data:image\/webp;base64,/ig).test(frame)) {
throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
}
this.frames.push({
image: frame,
duration: duration || this.duration
});
};
// deferred webp encoding. Draws image data to canvas, then encodes as dataUrl
WhammyVideo.prototype.encodeFrames = function(callback){
if(this.frames[0].image instanceof ImageData){
var frames = this.frames;
var tmpCanvas = document.createElement('canvas');
var tmpContext = tmpCanvas.getContext('2d');
tmpCanvas.width = this.frames[0].image.width;
tmpCanvas.height = this.frames[0].image.height;
var encodeFrame = function(index){
var frame = frames[index];
tmpContext.putImageData(frame.image, 0, 0);
frame.image = tmpCanvas.toDataURL('image/webp', this.quality);
if(index < frames.length-1){
setTimeout(function(){ encodeFrame(index + 1); }, 1);
}else{
callback();
}
}.bind(this);
encodeFrame(0);
}else{
callback();
}
};
WhammyVideo.prototype.compile = function(outputAsArray, callback){
this.encodeFrames(function(){
var webm = new toWebM(this.frames.map(function(frame){
var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
webp.duration = frame.duration;
return webp;
}), outputAsArray);
callback(webm);
}.bind(this));
};
return {
Video: WhammyVideo,
fromImageArray: function(images, fps, outputAsArray){
return toWebM(images.map(function(image){
var webp = parseWebP(parseRIFF(atob(image.slice(23))))
webp.duration = 1000 / fps;
return webp;
}), outputAsArray)
},
toWebM: toWebM
// expose methods of madness
}
})()
-334
View File
@@ -1,334 +0,0 @@
(function () {
"use strict";
var lookup = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'
];
function clean(length) {
var i, buffer = new Uint8Array(length);
for (i = 0; i < length; i += 1) {
buffer[i] = 0;
}
return buffer;
}
function extend(orig, length, addLength, multipleOf) {
var newSize = length + addLength,
buffer = clean((parseInt(newSize / multipleOf) + 1) * multipleOf);
buffer.set(orig);
return buffer;
}
function pad(num, bytes, base) {
num = num.toString(base || 8);
return "000000000000".substr(num.length + 12 - bytes) + num;
}
function stringToUint8 (input, out, offset) {
var i, length;
out = out || clean(input.length);
offset = offset || 0;
for (i = 0, length = input.length; i < length; i += 1) {
out[offset] = input.charCodeAt(i);
offset += 1;
}
return out;
}
function uint8ToBase64(uint8) {
var i,
extraBytes = uint8.length % 3, // if we have 1 byte left, pad 2 bytes
output = "",
temp, length;
function tripletToBase64 (num) {
return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F];
};
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]);
output += tripletToBase64(temp);
}
// this prevents an ERR_INVALID_URL in Chrome (Firefox okay)
switch (output.length % 4) {
case 1:
output += '=';
break;
case 2:
output += '==';
break;
default:
break;
}
return output;
}
window.utils = {}
window.utils.clean = clean;
window.utils.pad = pad;
window.utils.extend = extend;
window.utils.stringToUint8 = stringToUint8;
window.utils.uint8ToBase64 = uint8ToBase64;
}());
(function () {
"use strict";
/*
struct posix_header { // byte offset
char name[100]; // 0
char mode[8]; // 100
char uid[8]; // 108
char gid[8]; // 116
char size[12]; // 124
char mtime[12]; // 136
char chksum[8]; // 148
char typeflag; // 156
char linkname[100]; // 157
char magic[6]; // 257
char version[2]; // 263
char uname[32]; // 265
char gname[32]; // 297
char devmajor[8]; // 329
char devminor[8]; // 337
char prefix[155]; // 345
// 500
};
*/
var utils = window.utils,
headerFormat;
headerFormat = [
{
'field': 'fileName',
'length': 100
},
{
'field': 'fileMode',
'length': 8
},
{
'field': 'uid',
'length': 8
},
{
'field': 'gid',
'length': 8
},
{
'field': 'fileSize',
'length': 12
},
{
'field': 'mtime',
'length': 12
},
{
'field': 'checksum',
'length': 8
},
{
'field': 'type',
'length': 1
},
{
'field': 'linkName',
'length': 100
},
{
'field': 'ustar',
'length': 8
},
{
'field': 'owner',
'length': 32
},
{
'field': 'group',
'length': 32
},
{
'field': 'majorNumber',
'length': 8
},
{
'field': 'minorNumber',
'length': 8
},
{
'field': 'filenamePrefix',
'length': 155
},
{
'field': 'padding',
'length': 12
}
];
function formatHeader(data, cb) {
var buffer = utils.clean(512),
offset = 0;
headerFormat.forEach(function (value) {
var str = data[value.field] || "",
i, length;
for (i = 0, length = str.length; i < length; i += 1) {
buffer[offset] = str.charCodeAt(i);
offset += 1;
}
offset += value.length - i; // space it out with nulls
});
if (typeof cb === 'function') {
return cb(buffer, offset);
}
return buffer;
}
window.header = {}
window.header.structure = headerFormat;
window.header.format = formatHeader;
}());
(function () {
"use strict";
var header = window.header,
utils = window.utils,
recordSize = 512,
blockSize;
function Tar(recordsPerBlock) {
this.written = 0;
blockSize = (recordsPerBlock || 20) * recordSize;
this.out = utils.clean(blockSize);
this.blocks = [];
this.length = 0;
}
Tar.prototype.append = function (filepath, input, opts, callback) {
var data,
checksum,
mode,
mtime,
uid,
gid,
headerArr;
if (typeof input === 'string') {
input = utils.stringToUint8(input);
} else if (input.constructor !== Uint8Array.prototype.constructor) {
throw 'Invalid input type. You gave me: ' + input.constructor.toString().match(/function\s*([$A-Za-z_][0-9A-Za-z_]*)\s*\(/)[1];
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
opts = opts || {};
mode = opts.mode || parseInt('777', 8) & 0xfff;
mtime = opts.mtime || Math.floor(+new Date() / 1000);
uid = opts.uid || 0;
gid = opts.gid || 0;
data = {
fileName: filepath,
fileMode: utils.pad(mode, 7),
uid: utils.pad(uid, 7),
gid: utils.pad(gid, 7),
fileSize: utils.pad(input.length, 11),
mtime: utils.pad(mtime, 11),
checksum: ' ',
type: '0', // just a file
ustar: 'ustar ',
owner: opts.owner || '',
group: opts.group || ''
};
// calculate the checksum
checksum = 0;
Object.keys(data).forEach(function (key) {
var i, value = data[key], length;
for (i = 0, length = value.length; i < length; i += 1) {
checksum += value.charCodeAt(i);
}
});
data.checksum = utils.pad(checksum, 6) + "\u0000 ";
headerArr = header.format(data);
var headerLength = Math.ceil( headerArr.length / recordSize ) * recordSize;
var inputLength = Math.ceil( input.length / recordSize ) * recordSize;
this.blocks.push( { header: headerArr, input: input, headerLength: headerLength, inputLength: inputLength } );
};
Tar.prototype.save = function() {
var buffers = [];
var chunks = [];
var length = 0;
var max = Math.pow( 2, 20 );
var chunk = [];
this.blocks.forEach( function( b ) {
if( length + b.headerLength + b.inputLength > max ) {
chunks.push( { blocks: chunk, length: length } );
chunk = [];
length = 0;
}
chunk.push( b );
length += b.headerLength + b.inputLength;
} );
chunks.push( { blocks: chunk, length: length } );
chunks.forEach( function( c ) {
var buffer = new Uint8Array( c.length );
var written = 0;
c.blocks.forEach( function( b ) {
buffer.set( b.header, written );
written += b.headerLength;
buffer.set( b.input, written );
written += b.inputLength;
} );
buffers.push( buffer );
} );
buffers.push( new Uint8Array( 2 * recordSize ) );
return new Blob( buffers, { type: 'octet/stream' } );
};
Tar.prototype.clear = function () {
this.written = 0;
this.out = utils.clean(blockSize);
};
window.Tar = Tar;
}());
File diff suppressed because it is too large Load Diff
+34 -11
View File
@@ -65,6 +65,7 @@
"use strict";
var esprima = __webpack_require__(204);
var parse = esprima.parse;
var LOOP_CHECK_FUNC_NAME = '__loopCheck';
function default_1(src, opts, fn) {
if (typeof opts === 'function') {
fn = opts;
@@ -147,7 +148,6 @@
console.log('#Found draw function:', node);
var hasCapturerStart_1 = false;
var hasCapturerEnd_1 = false;
// Traverse the function body to check for `capturer_start()` and `capturer_end()`
(function checkCapturerCalls(bodyNode) {
if (bodyNode.type === 'CallExpression' &&
bodyNode.callee &&
@@ -5934,25 +5934,48 @@
"use strict";
function defaultInserter(name) {
return function (node) {
return name + "();";
return name + '();';
};
}
var LOOP_CHECK_FUNC_NAME = '__loopCheck';
function LoopInserter(fn) {
if (typeof (fn) == "string")
fn = defaultInserter(fn);
return function (node) {
if (node.type == 'WhileStatement' ||
node.type == 'ForStatement' ||
node.type == 'DoWhileStatement') {
node.body.update('{ ' + fn(node) + node.body.source() + ' }');
return true;
if (node.type === 'ForStatement' ||
node.type === 'WhileStatement' ||
node.type === 'DoWhileStatement') {
var loopCheckCode = {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: LOOP_CHECK_FUNC_NAME
},
arguments: [
{
type: 'Literal',
value: JSON.stringify(node.range)
}
]
}
};
if (node.body.type === 'BlockStatement') {
// Insert the loop check at the beginning of the block
node.body.body.unshift(loopCheckCode);
}
else {
// Wrap the single statement body in a block and insert the loop check
node.body = {
type: 'BlockStatement',
body: [loopCheckCode, node.body]
};
}
}
return false;
return '';
};
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = LoopInserter;
;
/***/ }),
File diff suppressed because one or more lines are too long
+65 -32
View File
@@ -5,8 +5,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
height: 600px;
@@ -38,14 +36,6 @@
/* 滚动条滑块悬停颜色 */
}
</style>
<!-- <script src="./p5.js"></script> -->
<!-- <script src="./sketch.js"></script> -->
<!-- <script src="./src/CCapture.js"></script> -->
<!-- <script src="./src/gif.js"></script>
<script src="./src/gif.worker.js"></script> -->
<!-- <script src="./src/download.js"></script> -->
<!-- <script src="./src/webm-writer-0.2.0.js"></script> -->
</head>
<body>
@@ -57,14 +47,12 @@
return urlParams.get('id');
}
// 监听来自iframe的消息
window.addEventListener('message', (event) => {
const data = event.data;
const nodeId = getIdFromUrl(window.location.href);
if (data.from === 'p5.widget' && data.status === 'save') {
// console.log('#p5html', data)
if (data.from === 'p5.widget' && data.status === 'save' && data._from == 'main') {
const frames = data.frames;
// 示例用法
// const url = 'https://example.com/page?id=12345';
@@ -72,32 +60,77 @@
frames,
from: 'p5.widget',
status: 'save',
nodeId,
id:(new Date()).getTime()
}, '*');
// window.location.reload()
}
if (data.from === 'p5.widget' && data.status === 'stop') {
window.parent.postMessage({
from: 'p5.widget',
status: 'stop',
nodeId
}, '*');
window.location.reload()
// window.location.reload()
}
// console.log(data)
// if (data.from === 'p5.widget' && data.status === 'capture') {
// window.parent.postMessage({
// from: 'p5.widget',
// status: 'capture',
// frameCount: data.frameCount,
// maxCount: data.maxCount,
// nodeId
// }, '*');
// }
});
</script>
<script type="text/p5" data-height="500" data-preview-width="300">
function setup() {
createCanvas(100, 100);
}
<script type="text/p5" data-height="500" data-preview-width="300" >
const TO_GOAL = 0.3
const NOISE_AMP = 3.1415 / 100
let g_width
let g_red
let g_green
let g_blue
function draw_triangle (angle) {
triangle(
cos(TWO_PI / 3 + angle) * g_width,
sin(TWO_PI / 3 + angle) * g_width,
cos((TWO_PI / 3) * 2 + angle) * g_width,
sin((TWO_PI / 3) * 2 + angle) * g_width,
cos((TWO_PI / 3) * 3 + angle) * g_width,
sin((TWO_PI / 3) * 3 + angle) * g_width
)
}
function setup () {
createCanvas(300, 300)
mouseX = 300 / 2
mouseY = 300 / 2
g_red = map(mouseX, 0, 300, 0, PI)
g_green = map(mouseX, 0, 300, 0, PI)
g_blue = map(mouseX, 0, 300, 0, PI)
g_width = min(300, 300) / 3
}
function draw () {
background(0)
push()
translate(300 / 2, 300 / 2)
blendMode(ADD)
g_red =
g_red +
(map(0, 0, windowWidth, 0, PI) - g_red) * TO_GOAL +
random(-NOISE_AMP, NOISE_AMP)
g_green =
g_green + (g_red - g_green) * TO_GOAL + random(-NOISE_AMP, NOISE_AMP)
g_blue = g_blue + (g_green - g_blue) * TO_GOAL + random(-NOISE_AMP, NOISE_AMP)
fill(255, 0, 0)
draw_triangle(g_red)
fill(0, 255, 0)
draw_triangle(g_green)
fill(0, 0, 255)
draw_triangle(g_blue)
pop()
}
function draw() {
background(255, 0, 200);
}
</script>
<script src="./p5-widget/p5-widget.js"></script>
</body>
+36
View File
@@ -0,0 +1,36 @@
dialog {
width: 100%;
text-align: center;
max-width: 20em;
color: white;
background-color: #000;
border: none;
position: relative;
transform: translate(-50%, -50%);
}
#progress-container {
position: absolute;
top: 50%;
left: 50%;
}
progress {
width: 100%;
height: 1em;
border: none;
background-color: #fff;
color: #eee;
}
progress::-webkit-progress-bar {
background-color: #333;
}
progress::-webkit-progress-value {
background-color: #eee;
}
progress::-moz-progress-bar {
background-color: #eee;
}
+119
View File
@@ -0,0 +1,119 @@
body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}
a {
color: #ff0;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
cursor: pointer;
text-transform: uppercase;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
z-index: 1; /* TODO Solve this in HTML */
}
a, button, input, select {
pointer-events: auto;
}
.lil-gui {
z-index: 2 !important; /* TODO Solve this in HTML */
}
@media all and ( max-width: 640px ) {
.lil-gui.root {
right: auto;
top: auto;
max-height: 50%;
max-width: 80%;
bottom: 0;
left: 0;
}
}
#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0,0,0,0.7);
}
#overlay button {
background: transparent;
border: 0;
border: 1px solid rgb(255, 255, 255);
border-radius: 4px;
color: #ffffff;
padding: 12px 18px;
text-transform: uppercase;
cursor: pointer;
}
#notSupported {
width: 50%;
margin: auto;
background-color: #f00;
margin-top: 20px;
padding: 10px;
}
#screenshotButton {
position: absolute;
bottom: 10px; /* Adjust as needed */
left: 10px; /* Position to the left */
z-index: 10; /* Ensure this is above the canvas's z-index */
padding: 5px 5px;
border: none;
border-radius: 5px;
background: linear-gradient(145deg, #007bff, #0056b3);
color: #ffffff;
font-size: 10px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background 0.3s ease-in-out, transform 0.2s ease;
}
#screenshotButton:hover {
background: linear-gradient(145deg, #0056b3, #007bff);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}
#screenshotButton:active {
background: #0056b3;
transform: translateY(1px);
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
}
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="/extensions/comfyui-mixlab-nodes/style/threeStyle.css">
<link type="text/css" rel="stylesheet" href="/extensions/comfyui-mixlab-nodes/style/progressStyle.css">
</head>
<body>
<div id="progress-container">
<dialog open id="progress-dialog">
<p>
<label for="progress-indicator">Loading scene...</label>
</p>
<progress max="100" id="progress-indicator"></progress>
</dialog>
</div>
<div id="container"></div>
<script id="visualizer" type="module" filepath="" crossorigin src="/extensions/comfyui-mixlab-nodes/lib/threeVisualizer.js"></script>
<button id="screenshotButton">Take Screenshot</button>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long