Files
L33chKing-ComfyUI_Channel_Ops/web/layer_blend_preview.js
T

303 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Layer Blending - In-node live preview (canvas)
// Displays the blended result of two images with selected blend mode and opacity.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const EXT_NAME = "Layer_Blending.Preview";
(function(){
function drawPreview(node, ctx){
const state = node._layerBlendState;
if(!state) return;
const w = node.size[0];
const h = node.size[1];
const pad = 11;
function widgetsBottomY(n){
const start = (n.widgets_start_y ?? n.widgetsStartY ?? 0);
let y = start;
if(Array.isArray(n.widgets)){
for(const wg of n.widgets){
if(!wg) continue;
let wh = 0;
try{
if(typeof wg.computeSize === 'function'){
const sz = wg.computeSize(w);
if(Array.isArray(sz)) wh = sz[1] || 0; else if(typeof sz === 'number') wh = sz;
} else if(typeof wg.height === 'number') {
wh = wg.height;
} else { wh = 24; }
}catch(_){ wh = 24; }
y += (wh || 0) + 6;
}
}
const minTop = 24;
return Math.max(y, minTop + 6);
}
const safeTop = widgetsBottomY(node) + 18;
const availableH = Math.max(0, h - safeTop - pad);
const dh = Math.max(24, availableH);
const dw = Math.max(0, w - pad*2);
const x = pad;
const y = Math.max(safeTop, h - dh - pad);
ctx.save();
ctx.fillStyle = "#111";
ctx.fillRect(x, y, dw, dh);
ctx.strokeStyle = "rgba(255,255,255,0.1)";
ctx.strokeRect(x, y, dw, dh);
if(!state.outReady){
ctx.fillStyle = "#bbb";
ctx.font = "11px sans-serif";
const msg = "Run node once to seed preview";
ctx.fillText(msg, x+12, y+Math.floor(dh/2));
ctx.restore();
return;
}
const iw = state.outCanvas.width, ih = state.outCanvas.height;
const scale = Math.min(dw/iw, dh/ih);
const rw = Math.max(1, Math.floor(iw*scale));
const rh = Math.max(1, Math.floor(ih*scale));
const ox = x + Math.floor((dw - rw)/2);
const oy = y + Math.floor((dh - rh)/2);
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
ctx.drawImage(state.outCanvas, 0, 0, iw, ih, ox, oy, rw, rh);
ctx.restore();
}
function ensureBehavior(node){
if(node._layerBlendPreviewAdded) return;
node._layerBlendPreviewAdded = true;
const state = node._layerBlendState = {
bgImg: new Image(),
fgImg: new Image(),
bgReady: false,
fgReady: false,
bgUrl: null,
fgUrl: null,
loadToken: 0,
outCanvas: document.createElement('canvas'),
outCtx: null,
};
state.outCtx = state.outCanvas.getContext('2d');
function revokeUrl(kind){
try{
if(kind==='bg' && state.bgUrl){ URL.revokeObjectURL(state.bgUrl); state.bgUrl = null; }
if(kind==='fg' && state.fgUrl){ URL.revokeObjectURL(state.fgUrl); state.fgUrl = null; }
}catch(_){ }
}
function getWidget(name){
if(!node.widgets) return null;
return node.widgets.find(w => (w && (w.name===name || w.label===name)));
}
function getVal(name){ const w = getWidget(name); return w ? w.value : null; }
function setPreviewId(){
let w = getWidget('preview_id');
if(!w && node.addWidget){ w = node.addWidget('string','preview_id', String(node.id ?? '0'), null, {serialize:true}); }
if(w){
w.computeSize = () => [0,0]; w.draw = () => {};
const setVal = (v)=>{ try{ w.value = String(v ?? ''); }catch(_){ };
if(Array.isArray(node.widgets_values)){
const idx = node.widgets ? node.widgets.indexOf(w) : -1;
if(idx>=0) node.widgets_values[idx] = w.value;
}
};
setVal(node.id); setTimeout(()=>setVal(node.id),0);
}
}
function buildPaths(pid, ts){
const q = `?ts=${ts}`;
const base = [
`/extensions/ComfyUI_Channel_Ops/layer_blend_bg_${pid}.png${q}`,
`/extensions/Channel_Ops/layer_blend_bg_${pid}.png${q}`,
`/extensions/ChannelOps/layer_blend_bg_${pid}.png${q}`,
];
const top = [
`/extensions/ComfyUI_Channel_Ops/layer_blend_fg_${pid}.png${q}`,
`/extensions/Channel_Ops/layer_blend_fg_${pid}.png${q}`,
`/extensions/ChannelOps/layer_blend_fg_${pid}.png${q}`,
];
return {base, top};
}
async function fetchAsImage(urls, kind, token){
try{
for(const u of urls){
const resp = await fetch(u, {cache:'no-store'});
if(resp.ok){
const blob = await resp.blob();
const obj = URL.createObjectURL(blob);
revokeUrl(kind);
if(kind==='bg'){ state.bgUrl = obj; state.bgImg.src = obj; }
else { state.fgUrl = obj; state.fgImg.src = obj; }
return true;
}
}
}catch(_){ }
return false;
}
function loadBoth(tsOverride){
const wid = getWidget('preview_id');
const pid = String((wid && wid.value) || node.id || 'A').replace(/[^a-zA-Z0-9_-]/g, "_");
const ts = (typeof tsOverride==='number' && isFinite(tsOverride)) ? tsOverride : Date.now();
const paths = buildPaths(pid, ts);
const token = ++state.loadToken;
state.bgReady = false; state.fgReady = false;
state.bgImg.onload = ()=>{ if(token!==state.loadToken) return; state.bgReady = true; render(); };
state.fgImg.onload = ()=>{ if(token!==state.loadToken) return; state.fgReady = true; render(); };
state.bgImg.onerror = ()=>{ if(token!==state.loadToken) return; state.bgReady = false; render(); };
state.fgImg.onerror = ()=>{ if(token!==state.loadToken) return; state.fgReady = false; render(); };
// fire and forget
fetchAsImage(paths.base, 'bg', token);
fetchAsImage(paths.top, 'fg', token);
}
function clamp01(x){ return Math.min(1, Math.max(0, x)); }
function blendPixel(a, b, mode){
// a,b in [0,1]
switch(mode){
case 'multiply': return a*b;
case 'screen': return 1 - (1-a)*(1-b);
case 'overlay': return (a<=0.5) ? (2*a*b) : (1 - 2*(1-a)*(1-b));
case 'hard light': return (b<=0.5) ? (2*a*b) : (1 - 2*(1-a)*(1-b));
case 'soft light': return (1 - 2*b)*a*a + 2*b*a;
case 'darken': return Math.min(a,b);
case 'lighten': return Math.max(a,b);
case 'color dodge': return (a / Math.max(1e-8, 1-b));
case 'color burn': return (1 - ((1-a) / Math.max(1e-8, b)));
case 'linear burn': return a + b - 1;
case 'vivid light': return (b<0.5) ? (1 - ((1-a)/Math.max(1e-8, 2*b))) : (a/Math.max(1e-8, 2*(1-b)));
case 'linear light': return a + 2*b - 1;
case 'pin light': return (b<0.5) ? Math.min(a, Math.min(1, 2*b)) : Math.max(a, Math.max(0, 2*b-1));
case 'difference': return Math.abs(a-b);
case 'exclusion': return a + b - 2*a*b;
case 'divide': return a / Math.max(1e-8, b);
case 'hard mix': return (a + b < 1) ? 0 : 1;
case 'linear dodge':
case 'add': return a + b;
case 'subtract': return a - b;
case 'normal':
default: return b;
}
}
function render(){
const opacity = (parseFloat(getVal('opacity')) || 0) / 255.0;
const mode = String(getVal('mode') || 'Normal').toLowerCase();
const ready = state.bgReady && state.fgReady;
state.outReady = false;
if(!ready){ app.graph.setDirtyCanvas(true,true); return; }
const w = Math.max(1, Math.min(state.bgImg.naturalWidth, state.fgImg.naturalWidth));
const h = Math.max(1, Math.min(state.bgImg.naturalHeight, state.fgImg.naturalHeight));
state.outCanvas.width = w; state.outCanvas.height = h;
const ctx = state.outCtx;
// Draw inputs resampled into temp offscreens
const cA = document.createElement('canvas'); cA.width=w; cA.height=h; const xA = cA.getContext('2d'); xA.drawImage(state.bgImg, 0,0,w,h);
const cB = document.createElement('canvas'); cB.width=w; cB.height=h; const xB = cB.getContext('2d'); xB.drawImage(state.fgImg, 0,0,w,h);
const aData = xA.getImageData(0,0,w,h).data;
const bData = xB.getImageData(0,0,w,h).data;
const out = ctx.createImageData(w, h);
const odata = out.data;
for(let i=0;i<odata.length;i+=4){
const ar=aData[i]/255, ag=aData[i+1]/255, ab=aData[i+2]/255;
const br=bData[i]/255, bg=bData[i+1]/255, bb=bData[i+2]/255;
let rr, gg, bb2;
const m = mode;
if(m === 'darker color'){
const suma = ar+ag+ab, sumb = br+bg+bb;
if(sumb < suma){ rr=br; gg=bg; bb2=bb; } else { rr=ar; gg=ag; bb2=ab; }
} else if(m === 'lighter color'){
const suma = ar+ag+ab, sumb = br+bg+bb;
if(sumb > suma){ rr=br; gg=bg; bb2=bb; } else { rr=ar; gg=ag; bb2=ab; }
} else {
rr = blendPixel(ar, br, m); gg = blendPixel(ag, bg, m); bb2 = blendPixel(ab, bb, m);
}
// opacity composite over A
rr = clamp01(ar*(1-opacity) + rr*opacity);
gg = clamp01(ag*(1-opacity) + gg*opacity);
bb2 = clamp01(ab*(1-opacity) + bb2*opacity);
odata[i] = Math.round(rr*255);
odata[i+1] = Math.round(gg*255);
odata[i+2] = Math.round(bb2*255);
odata[i+3] = 255;
}
ctx.putImageData(out, 0, 0);
state.outReady = true;
app.graph.setDirtyCanvas(true,true);
}
function bindChanges(){
if(!node.widgets) return;
node.widgets.forEach(w => {
const orig = w.callback || w.onChange;
const cb = function(){ if(orig) try{ orig.apply(this, arguments); }catch(_e){} render(); };
w.callback = cb; w.onChange = cb;
});
}
function applyTooltips(){
const wMode = getWidget('mode');
const wOpac = getWidget('opacity');
if(wMode){ wMode.tooltip = wMode.description = 'Blend modes: Normal, Multiply, Screen, Overlay, Soft Light, Add, Subtract'; }
if(wOpac){ wOpac.tooltip = wOpac.description = 'Opacity: 0–255 (0 transparent, 255 fully applied)'; }
}
setPreviewId();
bindChanges();
applyTooltips();
loadBoth();
node._layerBlendLoad = loadBoth;
const prevExec = node.onExecuted;
node.onExecuted = function(){ if(prevExec) try{ prevExec.apply(this, arguments); }catch(_e){} try{ loadBoth(); }catch(_e){} };
}
app.registerExtension({
name: EXT_NAME,
async setup(){
try{
api.addEventListener('layer_blend_preview', (ev)=>{
const detail = ev?.detail ?? ev;
const pid = String(detail?.preview_id || '');
const ts = (typeof detail?.ts === 'number') ? Math.floor(detail.ts*1000) : Date.now();
if(!pid) return;
const nodes = (app?.graph?._nodes || []).filter(n => (n.comfyClass || n.type || '').toString().includes('LayerBlendingNode'));
for(const n of nodes){
try{
if(!n._layerBlendPreviewAdded) ensureBehavior(n);
let w = n.widgets ? n.widgets.find(w => w && (w.name==='preview_id' || w.label==='preview_id')) : null;
const widVal = w ? String(w.value || '') : '';
const nodeIdOk = (typeof n.id === 'number' && n.id >= 0) ? String(n.id) : '';
const matches = (widVal && widVal===pid) || (nodeIdOk && nodeIdOk===pid);
if(matches && n._layerBlendLoad) n._layerBlendLoad(ts);
}catch(_e){}
}
});
}catch(_e){}
},
async beforeRegisterNodeDef(nodeType, nodeData){
const nm = (nodeData?.name || nodeType?.name || '').toString();
if(nm.includes('LayerBlendingNode')){
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function(){ if(origCreated) try{ origCreated.apply(this, arguments); }catch(_e){} try{ ensureBehavior(this); }catch(_e){} };
const origDraw = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx){ if(origDraw) try{ origDraw.apply(this, arguments); }catch(_e){} try{ if(this._layerBlendPreviewAdded) drawPreview(this, ctx); }catch(_e){} };
}
},
});
})();