first commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# ComfyUI Layer Style
|
||||
A set of nodes for ComfyUI it generate image like Adobe Photoshop's Layer Style.
|
||||
the Drop Shadow is first completed node, and follow-up work is in progress.
|
||||
|
||||
[中文说明点这里](./README_CN.MD)
|
||||
|
||||
## Node Description
|
||||
|
||||
### Drop Shadow:
|
||||
Generate shadow
|
||||

|
||||
|
||||
|
||||
Node options:
|
||||
* background_image: The background image.
|
||||
* layer_image: Layer images for composite.
|
||||
* layer_mask: Mask for layer_image, shadows are generated according to their shape.
|
||||
* invert_mask: Whether to reverse the mask.
|
||||
* blend_mode: Blending method of shadows, there are **_normal, multply, screen, add, subtract, difference, darker_** and **_lighter_**.
|
||||
* opacity: Opacity of shadow.
|
||||
* distance_x: Horizontal offset of shadow.
|
||||
* distance_y: Vertical offset of shadow.
|
||||
* grow: Shadow expansion amplitude.
|
||||
* blur:Shadow blur level.
|
||||
* shadow_color: Shadow color, described in hexadecimal RGB format.
|
||||
|
||||
Output type:
|
||||
* image:Completed image.
|
||||
* shadow_mask:Shadow's mask.
|
||||
|
||||
## Example workflow
|
||||

|
||||
image Some JSON workflow files in the workflow directory, that is example for ComfyUI.
|
||||
|
||||
## How to install
|
||||
|
||||
* Open the cmd window in the plugin directory of ComfyUI, like "ComfyUI\custom_nodes\",type```git clone https://github.com/chflame163/ComfyUI_WordCloud.git```
|
||||
or download the zip file and extracted, copy the resulting folder to ComfyUI\custom_ Nodes\
|
||||
|
||||
* Install dependency packages, open the cmd window in the WordCloud plugin directory like "ComfyUI\custom_ Nodes\ComfyUI_WordCloud" and enter the following command:
|
||||
```..\..\..\python_embeded\python.exe -m pip install -r requirements.txt```
|
||||
|
||||
* Restart ComfyUI
|
||||
@@ -0,0 +1,38 @@
|
||||
# ComfyUI Layer Style:
|
||||
ComfyUI插件,通过一组节点实现仿照Adobe Photoshop的图层样式。
|
||||
Drop Shadow是首个完成的节点,后续工作进行中。
|
||||
|
||||
## 节点说明:
|
||||
|
||||
### Drop Shadow:
|
||||
生成阴影。
|
||||

|
||||
|
||||
|
||||
选项说明:
|
||||
* background_image: 背景图像。
|
||||
* layer_image: 用于合成的层图像。
|
||||
* layer_mask: 层图像的遮罩,阴影按此生成。
|
||||
* invert_mask: 是否反转遮罩。
|
||||
* blend_mode: 阴影的混合方式,包括normal、multply、screen、add、subtract、difference、darker和lighter。
|
||||
* opacity: 阴影的不透明度。
|
||||
* distance_x: 阴影的水平方向偏移量。
|
||||
* distance_y: 阴影的垂直方向偏移量。
|
||||
* grow: 阴影扩张幅度。
|
||||
* blur:阴影模糊程度。
|
||||
* shadow_color: 阴影颜色,使用16进制RGB格式描述。
|
||||
|
||||
输出:
|
||||
* image:完成的图像。
|
||||
* shadow_mask:阴影的通道。
|
||||
|
||||
## 使用示例:
|
||||

|
||||
在workflow目录下有json格式的工作流示例文件。
|
||||
|
||||
## 安装方法:
|
||||
|
||||
* 在CompyUI插件目录(例如“CompyUI\custom_nodes\”)中打开cmd窗口,键入```git clone https://github.com/chflame163/ComfyUI_WordCloud.git```安装。或者下载解压zip文件,将得到的文件夹复制到 ComfyUI\custom_nodes\
|
||||
* 安装依赖包,在资源管理器ComfyUI\custom_nodes\ComfyUI_WordCloud 插件目录位置打开cmd窗口,输入以下命令:
|
||||
```..\..\..\python_embeded\python.exe -m pip install -r requirements.txt```
|
||||
* 重新打开ComfyUI。
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import importlib.util
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import filecmp
|
||||
import shutil
|
||||
import __main__
|
||||
from .dzNodes import init, get_ext_dir, log
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
|
||||
if init():
|
||||
py = get_ext_dir("py")
|
||||
files = glob.glob("*.py", root_dir=py, recursive=False)
|
||||
for file in files:
|
||||
name = os.path.splitext(file)[0]
|
||||
spec = importlib.util.spec_from_file_location(name, os.path.join(py, file))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
|
||||
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
|
||||
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "dzNodes",
|
||||
"logging": false
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import inspect
|
||||
import aiohttp
|
||||
from server import PromptServer
|
||||
from tqdm import tqdm
|
||||
|
||||
config = None
|
||||
|
||||
|
||||
def is_logging_enabled():
|
||||
config = get_extension_config()
|
||||
if "logging" not in config:
|
||||
return False
|
||||
return config["logging"]
|
||||
|
||||
|
||||
def log(message, type=None, always=False, name=None):
|
||||
if not always and not is_logging_enabled():
|
||||
return
|
||||
|
||||
if type is not None:
|
||||
message = f"[{type}] {message}"
|
||||
|
||||
if name is None:
|
||||
name = get_extension_config()["name"]
|
||||
|
||||
print(f"# 😺dzNodes: {name} -> {message}")
|
||||
|
||||
|
||||
def get_ext_dir(subpath=None, mkdir=False):
|
||||
dir = os.path.dirname(__file__)
|
||||
if subpath is not None:
|
||||
dir = os.path.join(dir, subpath)
|
||||
|
||||
dir = os.path.abspath(dir)
|
||||
|
||||
if mkdir and not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
return dir
|
||||
|
||||
|
||||
def get_comfy_dir(subpath=None, mkdir=False):
|
||||
dir = os.path.dirname(inspect.getfile(PromptServer))
|
||||
if subpath is not None:
|
||||
dir = os.path.join(dir, subpath)
|
||||
|
||||
dir = os.path.abspath(dir)
|
||||
|
||||
if mkdir and not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
return dir
|
||||
|
||||
|
||||
def get_web_ext_dir():
|
||||
config = get_extension_config()
|
||||
name = config["name"]
|
||||
dir = get_comfy_dir("web/extensions/dzNodes")
|
||||
if not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
dir = os.path.join(dir, name)
|
||||
return dir
|
||||
|
||||
|
||||
def get_extension_config(reload=False):
|
||||
global config
|
||||
if reload == False and config is not None:
|
||||
return config
|
||||
|
||||
config_path = get_ext_dir("dzNodes.json")
|
||||
if not os.path.exists(config_path):
|
||||
log("Missing json, this extension may not work correctly. Please reinstall the extension.",
|
||||
type="ERROR", always=True, name="???")
|
||||
print(f"Extension path: {get_ext_dir()}")
|
||||
return {"name": "Unknown", "version": -1}
|
||||
with open(config_path, "r") as f:
|
||||
config = json.loads(f.read())
|
||||
return config
|
||||
|
||||
|
||||
def link_js(src, dst):
|
||||
src = os.path.abspath(src)
|
||||
dst = os.path.abspath(dst)
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import _winapi
|
||||
_winapi.CreateJunction(src, dst)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.symlink(src, dst)
|
||||
return True
|
||||
except:
|
||||
import logging
|
||||
logging.exception('')
|
||||
return False
|
||||
|
||||
def is_junction(path):
|
||||
if os.name != "nt":
|
||||
return False
|
||||
try:
|
||||
return bool(os.readlink(path))
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def install_js():
|
||||
src_dir = get_ext_dir("js")
|
||||
if not os.path.exists(src_dir):
|
||||
log("No JS")
|
||||
return
|
||||
|
||||
dst_dir = get_web_ext_dir()
|
||||
|
||||
if os.path.exists(dst_dir):
|
||||
if os.path.islink(dst_dir) or is_junction(dst_dir):
|
||||
log("JS already linked")
|
||||
return
|
||||
elif link_js(src_dir, dst_dir):
|
||||
log("JS linked")
|
||||
return
|
||||
|
||||
log("Copying JS files")
|
||||
shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True)
|
||||
|
||||
|
||||
def init(check_imports=None):
|
||||
log("Init")
|
||||
|
||||
if check_imports is not None:
|
||||
import importlib.util
|
||||
for imp in check_imports:
|
||||
spec = importlib.util.find_spec(imp)
|
||||
if spec is None:
|
||||
log(f"{imp} is required, please check requirements are installed.",
|
||||
type="ERROR", always=True)
|
||||
return False
|
||||
|
||||
install_js()
|
||||
return True
|
||||
|
||||
|
||||
def get_async_loop():
|
||||
loop = None
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop
|
||||
|
||||
|
||||
def get_http_session():
|
||||
loop = get_async_loop()
|
||||
return aiohttp.ClientSession(loop=loop)
|
||||
|
||||
|
||||
async def download(url, stream, update_callback=None, session=None):
|
||||
close_session = False
|
||||
if session is None:
|
||||
close_session = True
|
||||
session = get_http_session()
|
||||
try:
|
||||
async with session.get(url) as response:
|
||||
size = int(response.headers.get('content-length', 0)) or None
|
||||
|
||||
with tqdm(
|
||||
unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1], total=size,
|
||||
) as progressbar:
|
||||
perc = 0
|
||||
async for chunk in response.content.iter_chunked(2048):
|
||||
stream.write(chunk)
|
||||
progressbar.update(len(chunk))
|
||||
if update_callback is not None and progressbar.total is not None and progressbar.total != 0:
|
||||
last = perc
|
||||
perc = round(progressbar.n / progressbar.total, 2)
|
||||
if perc != last:
|
||||
last = perc
|
||||
await update_callback(perc)
|
||||
finally:
|
||||
if close_session and session is not None:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def download_to_file(url, destination, update_callback=None, is_ext_subpath=True, session=None):
|
||||
if is_ext_subpath:
|
||||
destination = get_ext_dir(destination)
|
||||
with open(destination, mode='wb') as f:
|
||||
download(url, f, update_callback, session)
|
||||
|
||||
|
||||
def wait_for_async(async_fn, loop=None):
|
||||
res = []
|
||||
|
||||
async def run_async():
|
||||
r = await async_fn()
|
||||
res.append(r)
|
||||
|
||||
if loop is None:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
loop.run_until_complete(run_async())
|
||||
|
||||
return res[0]
|
||||
|
||||
|
||||
def update_node_status(client_id, node, text, progress=None):
|
||||
if client_id is None:
|
||||
client_id = PromptServer.instance.client_id
|
||||
|
||||
if client_id is None:
|
||||
return
|
||||
|
||||
PromptServer.instance.send_sync("dzNodes/update_status", {
|
||||
"node": node,
|
||||
"progress": progress,
|
||||
"text": text
|
||||
}, client_id)
|
||||
|
||||
|
||||
async def update_node_status_async(client_id, node, text, progress=None):
|
||||
if client_id is None:
|
||||
client_id = PromptServer.instance.client_id
|
||||
|
||||
if client_id is None:
|
||||
return
|
||||
|
||||
await PromptServer.instance.send("dzNodes/update_status", {
|
||||
"node": node,
|
||||
"progress": progress,
|
||||
"text": text
|
||||
}, client_id)
|
||||
|
||||
|
||||
def get_config_value(key, default=None, throw=False):
|
||||
split = key.split(".")
|
||||
obj = get_extension_config()
|
||||
for s in split:
|
||||
if s in split:
|
||||
obj = obj[s]
|
||||
else:
|
||||
if throw:
|
||||
raise KeyError("Configuration key missing: " + key)
|
||||
else:
|
||||
return default
|
||||
return obj
|
||||
|
||||
|
||||
def is_inside_dir(root_dir, check_path):
|
||||
root_dir = os.path.abspath(root_dir)
|
||||
if not os.path.isabs(check_path):
|
||||
check_path = os.path.abspath(os.path.join(root_dir, check_path))
|
||||
return os.path.commonpath([check_path, root_dir]) == root_dir
|
||||
|
||||
|
||||
def get_child_dir(root_dir, child_path, throw_if_outside=True):
|
||||
child_path = os.path.abspath(os.path.join(root_dir, child_path))
|
||||
if is_inside_dir(root_dir, child_path):
|
||||
return child_path
|
||||
if throw_if_outside:
|
||||
raise NotADirectoryError(
|
||||
"Saving outside the target folder is not allowed.")
|
||||
return None
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 559 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,189 @@
|
||||
import math
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
import matplotlib.pyplot as plt
|
||||
import scipy.ndimage
|
||||
from typing import Union, List
|
||||
from PIL import Image, ImageFilter, ImageChops
|
||||
|
||||
def log(message):
|
||||
name = 'Layer Style'
|
||||
print(f"# 😺dzNodes: {name} -> {message}")
|
||||
|
||||
def pil2tensor(image:Image) -> torch.Tensor:
|
||||
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
|
||||
if isinstance(img_np, list):
|
||||
return torch.cat([np2tensor(img) for img in img_np], dim=0)
|
||||
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
|
||||
if len(tensor.shape) == 3: # Single image
|
||||
return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
else: # Batch of images
|
||||
return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor]
|
||||
|
||||
def tensor2pil(t_image: torch.Tensor) -> Image:
|
||||
return Image.fromarray(np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
def image2mask(image:Image) -> torch.Tensor:
|
||||
_image = image.convert('RGBA')
|
||||
alpha = _image.split() [0]
|
||||
bg = Image.new("L", _image.size)
|
||||
_image = Image.merge('RGBA', (bg, bg, bg, alpha))
|
||||
return pil2tensor(_image)[0, :, :, 3]
|
||||
|
||||
def mask2image(mask:torch.Tensor) -> Image:
|
||||
masks = tensor2np(mask)
|
||||
# images = []
|
||||
for m in masks:
|
||||
_mask = Image.fromarray(m).convert("L")
|
||||
_image = Image.new("RGBA", _mask.size, color='white')
|
||||
_image = Image.composite(
|
||||
_image, Image.new("RGBA", _mask.size, color='black'), _mask)
|
||||
return _image
|
||||
|
||||
def shift_image(image:Image, distance_x:int, distance_y:int) -> Image:
|
||||
bkcolor = (0, 0, 0)
|
||||
width = image.width
|
||||
height = image.height
|
||||
ret_image = Image.new('RGB', size=(width, height), color=bkcolor)
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
if x + distance_x < width and y + distance_y < height:
|
||||
pixel = image.getpixel((x + distance_x, y + distance_y))
|
||||
ret_image.putpixel((x, y), pixel)
|
||||
return ret_image
|
||||
|
||||
def chop_image(background_image:Image, layer_image:Image, blend_mode:str, opacity:int) -> Image:
|
||||
|
||||
ret_image = background_image
|
||||
if blend_mode == 'normal':
|
||||
ret_image = layer_image
|
||||
if blend_mode == 'multply':
|
||||
ret_image = ImageChops.multiply(background_image,layer_image)
|
||||
if blend_mode == 'screen':
|
||||
ret_image = ImageChops.screen(background_image, layer_image)
|
||||
if blend_mode == 'add':
|
||||
ret_image = ImageChops.add(background_image, layer_image, 1, 0)
|
||||
if blend_mode == 'subtract':
|
||||
ret_image = ImageChops.subtract(background_image, layer_image, 1, 0)
|
||||
if blend_mode == 'difference':
|
||||
ret_image = ImageChops.difference(background_image, layer_image)
|
||||
if blend_mode == 'darker':
|
||||
ret_image = ImageChops.darker(background_image, layer_image)
|
||||
if blend_mode == 'lighter':
|
||||
ret_image = ImageChops.lighter(background_image, layer_image)
|
||||
|
||||
# opacity
|
||||
if opacity == 0:
|
||||
ret_image = background_image
|
||||
elif opacity < 100:
|
||||
alpha = 1.0 - float(opacity) / 100
|
||||
ret_image = Image.blend(ret_image, background_image, alpha)
|
||||
|
||||
return ret_image
|
||||
|
||||
def expand_mask(mask:torch.Tensor, grow:int, blur:int, expandrate:int) -> torch.Tensor:
|
||||
# grow
|
||||
c = 0
|
||||
kernel = np.array([[c, 1, c],
|
||||
[1, 1, 1],
|
||||
[c, 1, c]])
|
||||
growmask = mask.reshape((-1, mask.shape[-2], mask.shape[-1]))
|
||||
out = []
|
||||
for m in growmask:
|
||||
output = m.numpy()
|
||||
for _ in range(abs(grow)):
|
||||
if grow < 0:
|
||||
output = scipy.ndimage.grey_erosion(output, footprint=kernel)
|
||||
else:
|
||||
output = scipy.ndimage.grey_dilation(output, footprint=kernel)
|
||||
if grow < 0:
|
||||
grow -= abs(expandrate)
|
||||
else:
|
||||
grow += abs(expandrate)
|
||||
output = torch.from_numpy(output)
|
||||
out.append(output)
|
||||
# blur
|
||||
if blur != 0:
|
||||
for idx, tensor in enumerate(out):
|
||||
pil_image = tensor2pil(tensor.cpu().detach())
|
||||
pil_image = pil_image.filter(ImageFilter.GaussianBlur(blur))
|
||||
out[idx] = pil2tensor(pil_image)
|
||||
|
||||
blurred = torch.cat(out, dim=0)
|
||||
return blurred
|
||||
|
||||
class DropShadow:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
chop_mode = ['normal','multply','screen','add','subtract','difference','darker','lighter']
|
||||
return {
|
||||
"required": {
|
||||
"background_image": ("IMAGE", ), #
|
||||
"layer_image": ("IMAGE",), #
|
||||
"layer_mask": ("MASK",), #
|
||||
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
|
||||
"blend_mode": (chop_mode,), # 混合模式
|
||||
"opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 透明度
|
||||
"distance_x": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # x_偏移
|
||||
"distance_y": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # y_偏移
|
||||
"grow": ("INT", {"default": 2, "min": -9999, "max": 9999, "step": 1}), # 扩散
|
||||
"blur": ("INT", {"default": 15, "min": 0, "max": 100, "step": 1}), # 模糊
|
||||
"shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色
|
||||
},
|
||||
"optional": {
|
||||
# "test_mask": ("MASK",), #
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK",)
|
||||
RETURN_NAMES = ("image", "shadow_mask",)
|
||||
FUNCTION = 'drop_shadow'
|
||||
CATEGORY = '😺dzNodes'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def drop_shadow(self, background_image, layer_image, layer_mask,
|
||||
invert_mask, blend_mode, opacity, distance_x, distance_y,
|
||||
grow, blur, shadow_color,
|
||||
):
|
||||
distance_x = -distance_x
|
||||
distance_y = -distance_y
|
||||
# 处理阴影mask
|
||||
if invert_mask:
|
||||
layer_mask = 1 - layer_mask
|
||||
_layer = tensor2pil(layer_image)
|
||||
_mask = mask2image(layer_mask)
|
||||
if distance_x != 0 or distance_y != 0:
|
||||
_mask = shift_image(_mask, distance_x, distance_y) # 位移
|
||||
shadow_mask = expand_mask(image2mask(_mask), grow, blur, 0) #扩边,模糊,膨胀
|
||||
|
||||
# 合成阴影
|
||||
shadow_color = Image.new("RGB", _layer.size, color=shadow_color)
|
||||
alpha = tensor2pil(shadow_mask).convert('L')
|
||||
_canvas = tensor2pil(background_image)
|
||||
_shadow = chop_image(tensor2pil(background_image), shadow_color, blend_mode, opacity)
|
||||
_canvas.paste(_shadow, mask=alpha)
|
||||
|
||||
# 合成layer
|
||||
alpha = tensor2pil(layer_mask).convert('L')
|
||||
_canvas.paste(_layer, mask=alpha)
|
||||
|
||||
ret_image = _canvas
|
||||
ret_mask = shadow_mask
|
||||
return (pil2tensor(ret_image), ret_mask,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerStyle_DropShadow": DropShadow
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerStyle_DropShadow": "Layer Style: Drop Shadow"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
numpy
|
||||
pillow
|
||||
torch
|
||||
matplotlib
|
||||
Scipy
|
||||
@@ -0,0 +1,341 @@
|
||||
{
|
||||
"last_node_id": 20,
|
||||
"last_link_id": 49,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
237,
|
||||
-31
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
45
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
46
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"512x512.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "RGB_Picker",
|
||||
"pos": [
|
||||
332,
|
||||
-180
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 94
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
49
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "RGB_Picker"
|
||||
},
|
||||
"widgets_values": [
|
||||
"#2b0303",
|
||||
"HEX"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
955,
|
||||
-13
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 47
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "LayerStyle_DropShadow",
|
||||
"pos": [
|
||||
576,
|
||||
-21
|
||||
],
|
||||
"size": [
|
||||
355.20001220703125,
|
||||
266
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "background_image",
|
||||
"type": "IMAGE",
|
||||
"link": 44
|
||||
},
|
||||
{
|
||||
"name": "layer_image",
|
||||
"type": "IMAGE",
|
||||
"link": 45
|
||||
},
|
||||
{
|
||||
"name": "layer_mask",
|
||||
"type": "MASK",
|
||||
"link": 46
|
||||
},
|
||||
{
|
||||
"name": "shadow_color",
|
||||
"type": "STRING",
|
||||
"link": 49,
|
||||
"widget": {
|
||||
"name": "shadow_color"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
47
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "shadow_mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
48
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerStyle_DropShadow"
|
||||
},
|
||||
"widgets_values": [
|
||||
true,
|
||||
"multply",
|
||||
52,
|
||||
43,
|
||||
45,
|
||||
2,
|
||||
20,
|
||||
"#000000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-102,
|
||||
-33
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
44
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"512x512bkgd.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
957,
|
||||
282
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 26
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 48
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
11
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1188,
|
||||
-15
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
11,
|
||||
10,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
44,
|
||||
3,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
45,
|
||||
2,
|
||||
0,
|
||||
20,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
46,
|
||||
2,
|
||||
1,
|
||||
20,
|
||||
2,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
47,
|
||||
20,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
48,
|
||||
20,
|
||||
1,
|
||||
10,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
49,
|
||||
17,
|
||||
0,
|
||||
20,
|
||||
3,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
Reference in New Issue
Block a user