Layout change

This commit is contained in:
Dariusz L
2025-07-28 15:57:28 +02:00
parent bb687a768b
commit f57b9f6b58
8 changed files with 805 additions and 338 deletions

View File

@@ -38,6 +38,27 @@ export class CanvasRenderer {
}); });
ctx.restore(); ctx.restore();
} }
/**
* Helper function to draw rectangle with stroke style
* @param ctx Canvas context
* @param rect Rectangle bounds {x, y, width, height}
* @param options Styling options
*/
drawStyledRect(ctx, rect, options = {}) {
const { strokeStyle = "rgba(255, 255, 255, 0.8)", lineWidth = 2, dashPattern = null } = options;
ctx.save();
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth / this.canvas.viewport.zoom;
if (dashPattern) {
const scaledDash = dashPattern.map((d) => d / this.canvas.viewport.zoom);
ctx.setLineDash(scaledDash);
}
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
if (dashPattern) {
ctx.setLineDash([]);
}
ctx.restore();
}
render() { render() {
if (this.renderAnimationFrame) { if (this.renderAnimationFrame) {
this.isDirty = true; this.isDirty = true;
@@ -148,13 +169,11 @@ export class CanvasRenderer {
const interaction = this.canvas.interaction; const interaction = this.canvas.interaction;
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) { if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
const rect = interaction.canvasResizeRect; const rect = interaction.canvasResizeRect;
ctx.save(); this.drawStyledRect(ctx, rect, {
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)'; strokeStyle: 'rgba(0, 255, 0, 0.8)',
ctx.lineWidth = 2 / this.canvas.viewport.zoom; lineWidth: 2,
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]); dashPattern: [8, 4]
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); });
ctx.setLineDash([]);
ctx.restore();
if (rect.width > 0 && rect.height > 0) { if (rect.width > 0 && rect.height > 0) {
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`; const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
const textWorldX = rect.x + rect.width / 2; const textWorldX = rect.x + rect.width / 2;
@@ -166,13 +185,11 @@ export class CanvasRenderer {
} }
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) { if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
const rect = interaction.canvasMoveRect; const rect = interaction.canvasMoveRect;
ctx.save(); this.drawStyledRect(ctx, rect, {
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)'; strokeStyle: 'rgba(0, 150, 255, 0.8)',
ctx.lineWidth = 2 / this.canvas.viewport.zoom; lineWidth: 2,
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]); dashPattern: [10, 5]
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); });
ctx.setLineDash([]);
ctx.restore();
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`; const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
const textWorldX = rect.x + rect.width / 2; const textWorldX = rect.x + rect.width / 2;
const textWorldY = rect.y - (20 / this.canvas.viewport.zoom); const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);
@@ -327,13 +344,11 @@ export class CanvasRenderer {
width: baseWidth + ext.left + ext.right, width: baseWidth + ext.left + ext.right,
height: baseHeight + ext.top + ext.bottom height: baseHeight + ext.top + ext.bottom
}; };
ctx.save(); this.drawStyledRect(ctx, previewBounds, {
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; // Yellow color for preview strokeStyle: 'rgba(255, 255, 0, 0.8)',
ctx.lineWidth = 3 / this.canvas.viewport.zoom; lineWidth: 3,
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]); dashPattern: [8, 4]
ctx.strokeRect(previewBounds.x, previewBounds.y, previewBounds.width, previewBounds.height); });
ctx.setLineDash([]);
ctx.restore();
} }
drawPendingGenerationAreas(ctx) { drawPendingGenerationAreas(ctx) {
const areasToDraw = []; const areasToDraw = [];
@@ -354,12 +369,11 @@ export class CanvasRenderer {
} }
// 3. Draw all collected areas // 3. Draw all collected areas
areasToDraw.forEach(area => { areasToDraw.forEach(area => {
ctx.save(); this.drawStyledRect(ctx, area, {
ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color strokeStyle: 'rgba(0, 150, 255, 0.9)',
ctx.lineWidth = 3 / this.canvas.viewport.zoom; lineWidth: 3,
ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]); dashPattern: [12, 6]
ctx.strokeRect(area.x, area.y, area.width, area.height); });
ctx.restore();
}); });
} }
drawMaskAreaBounds(ctx) { drawMaskAreaBounds(ctx) {
@@ -375,12 +389,11 @@ export class CanvasRenderer {
width: maskTool.getMask().width, width: maskTool.getMask().width,
height: maskTool.getMask().height height: maskTool.getMask().height
}; };
ctx.save(); this.drawStyledRect(ctx, maskBounds, {
ctx.strokeStyle = 'rgba(255, 100, 100, 0.7)'; // Red color for mask area bounds strokeStyle: 'rgba(255, 100, 100, 0.7)',
ctx.lineWidth = 2 / this.canvas.viewport.zoom; lineWidth: 2,
ctx.setLineDash([6 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]); dashPattern: [6, 6]
ctx.strokeRect(maskBounds.x, maskBounds.y, maskBounds.width, maskBounds.height); });
ctx.setLineDash([]);
// Add text label to show this is the mask drawing area // Add text label to show this is the mask drawing area
const textWorldX = maskBounds.x + maskBounds.width / 2; const textWorldX = maskBounds.x + maskBounds.width / 2;
const textWorldY = maskBounds.y - (10 / this.canvas.viewport.zoom); const textWorldY = maskBounds.y - (10 / this.canvas.viewport.zoom);
@@ -389,6 +402,5 @@ export class CanvasRenderer {
backgroundColor: "rgba(255, 100, 100, 0.8)", backgroundColor: "rgba(255, 100, 100, 0.8)",
padding: 8 padding: 8
}); });
ctx.restore();
} }
} }

View File

@@ -65,20 +65,14 @@ async function createCanvasWidget(node, widget, app) {
}, },
}, [ }, [
$el("div.painter-button-group", {}, [ $el("div.painter-button-group", {}, [
$el("button.painter-button", { $el("button.painter-button.icon-button", {
id: `open-editor-btn-${node.id}`, id: `open-editor-btn-${node.id}`,
textContent: "⛶", textContent: "⛶",
title: "Open in Editor", title: "Open in Editor",
style: { minWidth: "40px", maxWidth: "40px", fontWeight: "bold" },
}), }),
$el("button.painter-button", { $el("button.painter-button.icon-button", {
textContent: "?", textContent: "?",
title: "Show shortcuts", title: "Show shortcuts",
style: {
minWidth: "30px",
maxWidth: "30px",
fontWeight: "bold",
},
onmouseenter: (e) => { onmouseenter: (e) => {
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts; const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
showTooltip(e.target, content); showTooltip(e.target, content);
@@ -131,38 +125,63 @@ async function createCanvasWidget(node, widget, app) {
canvas.canvasLayers.handlePaste(addMode); canvas.canvasLayers.handlePaste(addMode);
} }
}), }),
$el("button.painter-button", { (() => {
id: `clipboard-toggle-${node.id}`, // Modern clipboard switch
textContent: "📋 System", // Initial state: checked = clipspace, unchecked = system
title: "Toggle clipboard source: System Clipboard", const isClipspace = canvas.canvasLayers.clipboardPreference === 'clipspace';
style: { const switchId = `clipboard-switch-${node.id}`;
minWidth: "100px", const switchEl = $el("label.clipboard-switch", { id: switchId }, [
fontSize: "11px", $el("input", {
backgroundColor: "#4a4a4a" type: "checkbox",
}, checked: isClipspace,
onclick: (e) => { onchange: (e) => {
const button = e.target; const checked = e.target.checked;
if (canvas.canvasLayers.clipboardPreference === 'system') { canvas.canvasLayers.clipboardPreference = checked ? 'clipspace' : 'system';
canvas.canvasLayers.clipboardPreference = 'clipspace'; // For accessibility, update ARIA label
button.textContent = "📋 Clipspace"; switchEl.setAttribute('aria-label', checked ? "Clipboard: Clipspace" : "Clipboard: System");
button.title = "Toggle clipboard source: ComfyUI Clipspace"; log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
button.style.backgroundColor = "#4a6cd4"; }
}),
$el("span.switch-track"),
$el("span.switch-labels", {}, [
$el("span.text-clipspace", {}, ["Clipspace"]),
$el("span.text-system", {}, ["System"])
]),
$el("span.switch-knob", {}, [
$el("span.switch-icon")
])
]);
// Tooltip logic
switchEl.addEventListener("mouseenter", (e) => {
const checked = switchEl.querySelector('input[type="checkbox"]').checked;
const tooltipContent = checked ? clipspaceClipboardTooltip : systemClipboardTooltip;
showTooltip(switchEl, tooltipContent);
});
switchEl.addEventListener("mouseleave", hideTooltip);
// Dynamic icon and text update on toggle
const input = switchEl.querySelector('input[type="checkbox"]');
const knobIcon = switchEl.querySelector('.switch-knob .switch-icon');
const updateSwitchView = (isClipspace) => {
const iconTool = isClipspace ? LAYERFORGE_TOOLS.CLIPSPACE : LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD;
const icon = iconLoader.getIcon(iconTool);
if (icon instanceof HTMLImageElement) {
knobIcon.innerHTML = '';
const clonedIcon = icon.cloneNode();
clonedIcon.style.width = '20px';
clonedIcon.style.height = '20px';
knobIcon.appendChild(clonedIcon);
} }
else { else {
canvas.canvasLayers.clipboardPreference = 'system'; knobIcon.textContent = isClipspace ? "🗂️" : "📋";
button.textContent = "📋 System";
button.title = "Toggle clipboard source: System Clipboard";
button.style.backgroundColor = "#4a4a4a";
} }
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`); };
}, input.addEventListener('change', () => updateSwitchView(input.checked));
onmouseenter: (e) => { // Initial state
const currentPreference = canvas.canvasLayers.clipboardPreference; iconLoader.preloadToolIcons().then(() => {
const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip; updateSwitchView(isClipspace);
showTooltip(e.target, tooltipContent); });
}, return switchEl;
onmouseleave: hideTooltip })()
})
]), ]),
]), ]),
$el("div.painter-separator"), $el("div.painter-separator"),
@@ -440,8 +459,15 @@ async function createCanvasWidget(node, widget, app) {
min: "1", min: "1",
max: "200", max: "200",
value: "20", value: "20",
oninput: (e) => canvas.maskTool.setBrushSize(parseInt(e.target.value)) oninput: (e) => {
}) const value = e.target.value;
canvas.maskTool.setBrushSize(parseInt(value));
const valueEl = document.getElementById('brush-size-value');
if (valueEl)
valueEl.textContent = `${value}px`;
}
}),
$el("div.slider-value", { id: "brush-size-value" }, ["20px"])
]), ]),
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [ $el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
$el("label", { for: "brush-strength-slider", textContent: "Strength:" }), $el("label", { for: "brush-strength-slider", textContent: "Strength:" }),
@@ -452,8 +478,15 @@ async function createCanvasWidget(node, widget, app) {
max: "1", max: "1",
step: "0.05", step: "0.05",
value: "0.5", value: "0.5",
oninput: (e) => canvas.maskTool.setBrushStrength(parseFloat(e.target.value)) oninput: (e) => {
}) const value = e.target.value;
canvas.maskTool.setBrushStrength(parseFloat(value));
const valueEl = document.getElementById('brush-strength-value');
if (valueEl)
valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
}
}),
$el("div.slider-value", { id: "brush-strength-value" }, ["50%"])
]), ]),
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [ $el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
$el("label", { for: "brush-hardness-slider", textContent: "Hardness:" }), $el("label", { for: "brush-hardness-slider", textContent: "Hardness:" }),
@@ -464,8 +497,15 @@ async function createCanvasWidget(node, widget, app) {
max: "1", max: "1",
step: "0.05", step: "0.05",
value: "0.5", value: "0.5",
oninput: (e) => canvas.maskTool.setBrushHardness(parseFloat(e.target.value)) oninput: (e) => {
}) const value = e.target.value;
canvas.maskTool.setBrushHardness(parseFloat(value));
const valueEl = document.getElementById('brush-hardness-value');
if (valueEl)
valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
}
}),
$el("div.slider-value", { id: "brush-hardness-value" }, ["50%"])
]), ]),
$el("button.painter-button.mask-control", { $el("button.painter-button.mask-control", {
textContent: "Clear Mask", textContent: "Clear Mask",
@@ -481,10 +521,9 @@ async function createCanvasWidget(node, widget, app) {
]), ]),
$el("div.painter-separator"), $el("div.painter-separator"),
$el("div.painter-button-group", {}, [ $el("div.painter-button-group", {}, [
$el("button.painter-button", { $el("button.painter-button.success", {
textContent: "Run GC", textContent: "Run GC",
title: "Run Garbage Collection to clean unused images", title: "Run Garbage Collection to clean unused images",
style: { backgroundColor: "#4a7c59", borderColor: "#3a6c49" },
onclick: async () => { onclick: async () => {
try { try {
const stats = canvas.imageReferenceManager.getStats(); const stats = canvas.imageReferenceManager.getStats();
@@ -500,10 +539,9 @@ async function createCanvasWidget(node, widget, app) {
} }
} }
}), }),
$el("button.painter-button", { $el("button.painter-button.danger", {
textContent: "Clear Cache", textContent: "Clear Cache",
title: "Clear all saved canvas states from browser storage", title: "Clear all saved canvas states from browser storage",
style: { backgroundColor: "#c54747", borderColor: "#a53737" },
onclick: async () => { onclick: async () => {
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) { if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
try { try {
@@ -736,36 +774,33 @@ async function createCanvasWidget(node, widget, app) {
let backdrop = null; let backdrop = null;
let originalParent = null; let originalParent = null;
let isEditorOpen = false; let isEditorOpen = false;
let viewportAdjustment = { x: 0, y: 0 };
/** /**
* Adjusts the viewport to keep the content centered when the container size changes. * Adjusts the viewport when entering fullscreen mode.
* @param rectA The original rectangle.
* @param rectB The new rectangle.
* @param direction Determines whether to apply the adjustment for opening (-1) or closing (1).
*/ */
const adjustViewportForCentering = (rectA, rectB, direction) => { const adjustViewportOnOpen = (originalRect) => {
if (!rectA || !rectB) const fullscreenRect = canvasContainer.getBoundingClientRect();
return; const widthDiff = fullscreenRect.width - originalRect.width;
const widthDiff = rectB.width - rectA.width; const heightDiff = fullscreenRect.height - originalRect.height;
const heightDiff = rectB.height - rectA.height;
const adjustX = (widthDiff / 2) / canvas.viewport.zoom; const adjustX = (widthDiff / 2) / canvas.viewport.zoom;
const adjustY = (heightDiff / 2) / canvas.viewport.zoom; const adjustY = (heightDiff / 2) / canvas.viewport.zoom;
canvas.viewport.x -= adjustX * direction; // Store the adjustment
canvas.viewport.y -= adjustY * direction; viewportAdjustment = { x: adjustX, y: adjustY };
const action = direction === 1 ? 'OPENING' : 'CLOSING'; // Apply the adjustment
log.info(`FULLSCREEN ${action} - Viewport adjusted for centering:`, { canvas.viewport.x -= viewportAdjustment.x;
widthDiff, heightDiff, adjustX, adjustY, canvas.viewport.y -= viewportAdjustment.y;
viewport_after: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom } };
}); /**
* Restores the viewport when exiting fullscreen mode.
*/
const adjustViewportOnClose = () => {
// Apply the stored adjustment in reverse
canvas.viewport.x += viewportAdjustment.x;
canvas.viewport.y += viewportAdjustment.y;
// Reset adjustment
viewportAdjustment = { x: 0, y: 0 };
}; };
const closeEditor = () => { const closeEditor = () => {
// Get fullscreen rect BEFORE removing from DOM
const fullscreenRect = backdrop?.querySelector('.painter-modal-content')?.getBoundingClientRect();
const currentRect = originalParent?.getBoundingClientRect();
log.info(`FULLSCREEN CLOSING - Window sizes:`, {
current: { width: currentRect?.width, height: currentRect?.height },
fullscreen: { width: fullscreenRect?.width, height: fullscreenRect?.height },
viewport_before: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
});
if (originalParent && backdrop) { if (originalParent && backdrop) {
originalParent.appendChild(mainContainer); originalParent.appendChild(mainContainer);
document.body.removeChild(backdrop); document.body.removeChild(backdrop);
@@ -776,12 +811,7 @@ async function createCanvasWidget(node, widget, app) {
// Remove ESC key listener when editor closes // Remove ESC key listener when editor closes
document.removeEventListener('keydown', handleEscKey); document.removeEventListener('keydown', handleEscKey);
setTimeout(() => { setTimeout(() => {
// Use the actual canvas container for centering calculation adjustViewportOnClose();
const currentCanvasContainer = originalParent.querySelector('.painterCanvasContainer.painter-container');
const fullscreenCanvasContainer = backdrop.querySelector('.painterCanvasContainer.painter-container');
const currentRect = currentCanvasContainer.getBoundingClientRect();
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
adjustViewportForCentering(currentRect, fullscreenRect, -1);
canvas.render(); canvas.render();
if (node.onResize) { if (node.onResize) {
node.onResize(); node.onResize();
@@ -794,7 +824,6 @@ async function createCanvasWidget(node, widget, app) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
closeEditor(); closeEditor();
log.info("Fullscreen editor closed via ESC key");
} }
}; };
openEditorBtn.onclick = () => { openEditorBtn.onclick = () => {
@@ -802,6 +831,7 @@ async function createCanvasWidget(node, widget, app) {
closeEditor(); closeEditor();
return; return;
} }
const originalRect = canvasContainer.getBoundingClientRect();
originalParent = mainContainer.parentElement; originalParent = mainContainer.parentElement;
if (!originalParent) { if (!originalParent) {
log.error("Could not find original parent of the canvas container!"); log.error("Could not find original parent of the canvas container!");
@@ -818,12 +848,7 @@ async function createCanvasWidget(node, widget, app) {
// Add ESC key listener when editor opens // Add ESC key listener when editor opens
document.addEventListener('keydown', handleEscKey); document.addEventListener('keydown', handleEscKey);
setTimeout(() => { setTimeout(() => {
// Use the actual canvas container for centering calculation adjustViewportOnOpen(originalRect);
const originalCanvasContainer = originalParent.querySelector('.painterCanvasContainer.painter-container');
const fullscreenCanvasContainer = modalContent.querySelector('.painterCanvasContainer.painter-container');
const originalRect = originalCanvasContainer.getBoundingClientRect();
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
adjustViewportForCentering(originalRect, fullscreenRect, 1);
canvas.render(); canvas.render();
if (node.onResize) { if (node.onResize) {
node.onResize(); node.onResize();

View File

@@ -1,54 +1,96 @@
.painter-button { .painter-button {
background: linear-gradient(to bottom, #4a4a4a, #3a3a3a); background-color: #444;
border: 1px solid #2a2a2a; border: 1px solid #555;
border-radius: 4px; border-radius: 5px;
color: #ffffff; color: #e0e0e0;
padding: 6px 12px; padding: 6px 14px;
font-size: 12px; font-size: 12px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease-in-out;
min-width: 80px; min-width: 80px;
text-align: center; text-align: center;
margin: 2px; margin: 2px;
text-shadow: 0 1px 1px rgba(0,0,0,0.2); box-shadow: 0 1px 2px rgba(0,0,0,0.1);
} }
.painter-button:hover { .painter-button:hover {
background: linear-gradient(to bottom, #5a5a5a, #4a4a4a); background-color: #555;
box-shadow: 0 1px 3px rgba(0,0,0,0.2); border-color: #666;
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
transform: translateY(-1px);
} }
.painter-button:active { .painter-button:active {
background: linear-gradient(to bottom, #3a3a3a, #4a4a4a); background-color: #3a3a3a;
transform: translateY(1px); transform: translateY(0);
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
} }
.painter-button:disabled, .painter-button:disabled,
.painter-button:disabled:hover { .painter-button:disabled:hover {
background: #555; background-color: #3a3a3a;
color: #888; color: #777;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
box-shadow: none; box-shadow: none;
border-color: #444; border-color: #4a4a4a;
opacity: 0.6;
} }
.painter-button.primary { .painter-button.primary {
background: linear-gradient(to bottom, #4a6cd4, #3a5cc4); background-color: #3a76d6;
border-color: #2a4cb4; border-color: #2a6ac4;
color: #fff;
text-shadow: none;
} }
.painter-button.primary:hover { .painter-button.primary:hover {
background: linear-gradient(to bottom, #5a7ce4, #4a6cd4); background-color: #4a86e4;
border-color: #3a76d6;
}
.painter-button.success {
background-color: #4a7c59;
border-color: #3a6c49;
color: #fff;
}
.painter-button.success:hover {
background-color: #5a8c69;
border-color: #4a7c59;
}
.painter-button.danger {
background-color: #c54747;
border-color: #a53737;
color: #fff;
}
.painter-button.danger:hover {
background-color: #d55757;
border-color: #c54747;
}
.painter-button.icon-button {
width: 30px;
height: 30px;
min-width: 30px;
padding: 0;
font-size: 16px;
line-height: 30px; /* Match height */
display: flex;
align-items: center;
justify-content: center;
} }
.painter-controls { .painter-controls {
background: linear-gradient(to bottom, #404040, #383838); background-color: #2f2f2f;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #202020;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 1px 3px rgba(0,0,0,0.2);
padding: 8px; padding: 10px;
display: flex; display: flex;
gap: 6px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
@@ -56,57 +98,198 @@
.painter-slider-container { .painter-slider-container {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 8px; gap: 4px;
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
min-width: 100px;
} }
.painter-slider-container input[type="range"] { .painter-slider-container input[type="range"] {
-webkit-appearance: none;
width: 80px; width: 80px;
height: 4px;
background: #555;
border-radius: 2px;
outline: none;
padding: 0;
margin: 0;
} }
.painter-slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
border: 2px solid #555;
transition: background 0.2s;
}
.painter-slider-container input[type="range"]::-webkit-slider-thumb:hover {
background: #fff;
}
.painter-slider-container input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
border: 2px solid #555;
}
.slider-value {
font-size: 11px;
color: #bbb;
margin-top: 2px;
min-height: 14px;
text-align: center;
}
.painter-button-group { .painter-button-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
background-color: rgba(0,0,0,0.2); background-color: transparent;
padding: 4px; padding: 0;
border-radius: 6px; border-radius: 6px;
} }
.painter-clipboard-group { .painter-clipboard-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 4px;
background-color: rgba(0,0,0,0.15);
padding: 3px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.1);
position: relative;
}
.painter-clipboard-group::before {
content: "";
position: absolute;
top: -2px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(74, 108, 212, 0.6), transparent);
border-radius: 1px;
} }
.painter-clipboard-group .painter-button { .painter-clipboard-group .painter-button {
margin: 1px; margin: 1px;
height: 30px; /* Match switch height */
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
} }
/* --- Clipboard Switch Modern --- */
.clipboard-switch {
position: relative;
width: 90px;
height: 30px;
box-sizing: border-box;
background-color: #444;
border-radius: 5px;
border: 1px solid #555;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
user-select: none;
padding: 0;
font-family: inherit;
font-size: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.clipboard-switch:hover {
background-color: #555;
border-color: #666;
}
.clipboard-switch:active {
background-color: #3a3a3a;
}
.clipboard-switch input[type="checkbox"] {
display: none;
}
.clipboard-switch .switch-track {
display: none;
}
.clipboard-switch .switch-knob {
position: absolute;
top: 2px;
left: 2px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: #5a5a5a;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
z-index: 2;
}
.clipboard-switch .switch-labels {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
color: #e0e0e0;
pointer-events: none;
z-index: 1;
transition: opacity 0.3s ease-in-out;
}
.clipboard-switch .switch-labels .text-clipspace,
.clipboard-switch .switch-labels .text-system {
position: absolute;
transition: opacity 0.2s ease-in-out;
}
.clipboard-switch .switch-labels .text-clipspace { opacity: 0; }
.clipboard-switch .switch-labels .text-system { opacity: 1; padding-left: 20px; }
.clipboard-switch .switch-knob .switch-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.clipboard-switch .switch-knob .switch-icon img {
width: 100%;
height: 100%;
}
/* Checked state */
.clipboard-switch:has(input:checked) {
background-color: #3a76d6;
border-color: #2a6ac4;
}
.clipboard-switch:has(input:checked):hover {
background-color: #4a86e4;
border-color: #3a76d6;
}
.clipboard-switch input:checked ~ .switch-knob {
left: calc(100% - 26px);
background-color: #fff;
}
.clipboard-switch input:checked ~ .switch-knob .switch-icon img {
filter: invert(35%) sepia(100%) saturate(1500%) hue-rotate(200deg) brightness(90%) contrast(100%);
}
.clipboard-switch input:checked ~ .switch-labels .text-clipspace {
opacity: 1;
color: #fff;
padding-right: 20px;
}
.clipboard-switch input:checked ~ .switch-labels .text-system {
opacity: 0;
}
.painter-separator { .painter-separator {
width: 1px; width: 1px;
height: 28px; height: 24px;
background-color: #2a2a2a; background-color: #444;
margin: 0 8px; margin: 0 8px;
} }

View File

@@ -16,10 +16,16 @@ export const LAYERFORGE_TOOLS = {
BRUSH: 'brush', BRUSH: 'brush',
ERASER: 'eraser', ERASER: 'eraser',
SHAPE: 'shape', SHAPE: 'shape',
SETTINGS: 'settings' SETTINGS: 'settings',
SYSTEM_CLIPBOARD: 'system_clipboard',
CLIPSPACE: 'clipspace',
}; };
// SVG Icons for LayerForge tools // SVG Icons for LayerForge tools
const SYSTEM_CLIPBOARD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 15H7v-2h10v2zm0-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>`;
const CLIPSPACE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M17 7H7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm0 12H7V9h10v10z"/><path d="M19 3H9c-1.1 0-2 .9-2 2v2h2V5h10v10h-2v2h2c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`;
const LAYERFORGE_TOOL_ICONS = { const LAYERFORGE_TOOL_ICONS = {
[LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SYSTEM_CLIPBOARD_ICON_SVG)}`,
[LAYERFORGE_TOOLS.CLIPSPACE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CLIPSPACE_ICON_SVG)}`,
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`, [LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`, [LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
[LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z"/></svg>')}`, [LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z"/></svg>')}`,

View File

@@ -59,6 +59,37 @@ export class CanvasRenderer {
ctx.restore(); ctx.restore();
} }
/**
* Helper function to draw rectangle with stroke style
* @param ctx Canvas context
* @param rect Rectangle bounds {x, y, width, height}
* @param options Styling options
*/
drawStyledRect(ctx: any, rect: any, options: any = {}) {
const {
strokeStyle = "rgba(255, 255, 255, 0.8)",
lineWidth = 2,
dashPattern = null
} = options;
ctx.save();
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth / this.canvas.viewport.zoom;
if (dashPattern) {
const scaledDash = dashPattern.map((d: number) => d / this.canvas.viewport.zoom);
ctx.setLineDash(scaledDash);
}
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
if (dashPattern) {
ctx.setLineDash([]);
}
ctx.restore();
}
render() { render() {
if (this.renderAnimationFrame) { if (this.renderAnimationFrame) {
this.isDirty = true; this.isDirty = true;
@@ -187,13 +218,11 @@ export class CanvasRenderer {
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) { if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
const rect = interaction.canvasResizeRect; const rect = interaction.canvasResizeRect;
ctx.save(); this.drawStyledRect(ctx, rect, {
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)'; strokeStyle: 'rgba(0, 255, 0, 0.8)',
ctx.lineWidth = 2 / this.canvas.viewport.zoom; lineWidth: 2,
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]); dashPattern: [8, 4]
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); });
ctx.setLineDash([]);
ctx.restore();
if (rect.width > 0 && rect.height > 0) { if (rect.width > 0 && rect.height > 0) {
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`; const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
const textWorldX = rect.x + rect.width / 2; const textWorldX = rect.x + rect.width / 2;
@@ -207,13 +236,11 @@ export class CanvasRenderer {
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) { if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
const rect = interaction.canvasMoveRect; const rect = interaction.canvasMoveRect;
ctx.save(); this.drawStyledRect(ctx, rect, {
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)'; strokeStyle: 'rgba(0, 150, 255, 0.8)',
ctx.lineWidth = 2 / this.canvas.viewport.zoom; lineWidth: 2,
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]); dashPattern: [10, 5]
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); });
ctx.setLineDash([]);
ctx.restore();
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`; const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
const textWorldX = rect.x + rect.width / 2; const textWorldX = rect.x + rect.width / 2;
@@ -397,13 +424,11 @@ export class CanvasRenderer {
height: baseHeight + ext.top + ext.bottom height: baseHeight + ext.top + ext.bottom
}; };
ctx.save(); this.drawStyledRect(ctx, previewBounds, {
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; // Yellow color for preview strokeStyle: 'rgba(255, 255, 0, 0.8)',
ctx.lineWidth = 3 / this.canvas.viewport.zoom; lineWidth: 3,
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]); dashPattern: [8, 4]
ctx.strokeRect(previewBounds.x, previewBounds.y, previewBounds.width, previewBounds.height); });
ctx.setLineDash([]);
ctx.restore();
} }
drawPendingGenerationAreas(ctx: any) { drawPendingGenerationAreas(ctx: any) {
@@ -429,12 +454,11 @@ export class CanvasRenderer {
// 3. Draw all collected areas // 3. Draw all collected areas
areasToDraw.forEach(area => { areasToDraw.forEach(area => {
ctx.save(); this.drawStyledRect(ctx, area, {
ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color strokeStyle: 'rgba(0, 150, 255, 0.9)',
ctx.lineWidth = 3 / this.canvas.viewport.zoom; lineWidth: 3,
ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]); dashPattern: [12, 6]
ctx.strokeRect(area.x, area.y, area.width, area.height); });
ctx.restore();
}); });
} }
@@ -454,12 +478,11 @@ export class CanvasRenderer {
height: maskTool.getMask().height height: maskTool.getMask().height
}; };
ctx.save(); this.drawStyledRect(ctx, maskBounds, {
ctx.strokeStyle = 'rgba(255, 100, 100, 0.7)'; // Red color for mask area bounds strokeStyle: 'rgba(255, 100, 100, 0.7)',
ctx.lineWidth = 2 / this.canvas.viewport.zoom; lineWidth: 2,
ctx.setLineDash([6 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]); dashPattern: [6, 6]
ctx.strokeRect(maskBounds.x, maskBounds.y, maskBounds.width, maskBounds.height); });
ctx.setLineDash([]);
// Add text label to show this is the mask drawing area // Add text label to show this is the mask drawing area
const textWorldX = maskBounds.x + maskBounds.width / 2; const textWorldX = maskBounds.x + maskBounds.width / 2;
@@ -470,7 +493,5 @@ export class CanvasRenderer {
backgroundColor: "rgba(255, 100, 100, 0.8)", backgroundColor: "rgba(255, 100, 100, 0.8)",
padding: 8 padding: 8
}); });
ctx.restore();
} }
} }

View File

@@ -90,20 +90,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
}, },
}, [ }, [
$el("div.painter-button-group", {}, [ $el("div.painter-button-group", {}, [
$el("button.painter-button", { $el("button.painter-button.icon-button", {
id: `open-editor-btn-${node.id}`, id: `open-editor-btn-${node.id}`,
textContent: "⛶", textContent: "⛶",
title: "Open in Editor", title: "Open in Editor",
style: {minWidth: "40px", maxWidth: "40px", fontWeight: "bold"},
}), }),
$el("button.painter-button", { $el("button.painter-button.icon-button", {
textContent: "?", textContent: "?",
title: "Show shortcuts", title: "Show shortcuts",
style: {
minWidth: "30px",
maxWidth: "30px",
fontWeight: "bold",
},
onmouseenter: (e: MouseEvent) => { onmouseenter: (e: MouseEvent) => {
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts; const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
showTooltip(e.target as HTMLElement, content); showTooltip(e.target as HTMLElement, content);
@@ -155,37 +149,68 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
canvas.canvasLayers.handlePaste(addMode); canvas.canvasLayers.handlePaste(addMode);
} }
}), }),
$el("button.painter-button", { (() => {
id: `clipboard-toggle-${node.id}`, // Modern clipboard switch
textContent: "📋 System", // Initial state: checked = clipspace, unchecked = system
title: "Toggle clipboard source: System Clipboard", const isClipspace = canvas.canvasLayers.clipboardPreference === 'clipspace';
style: { const switchId = `clipboard-switch-${node.id}`;
minWidth: "100px", const switchEl = $el("label.clipboard-switch", { id: switchId }, [
fontSize: "11px", $el("input", {
backgroundColor: "#4a4a4a" type: "checkbox",
}, checked: isClipspace,
onclick: (e: MouseEvent) => { onchange: (e: Event) => {
const button = e.target as HTMLButtonElement; const checked = (e.target as HTMLInputElement).checked;
if (canvas.canvasLayers.clipboardPreference === 'system') { canvas.canvasLayers.clipboardPreference = checked ? 'clipspace' : 'system';
canvas.canvasLayers.clipboardPreference = 'clipspace'; // For accessibility, update ARIA label
button.textContent = "📋 Clipspace"; switchEl.setAttribute('aria-label', checked ? "Clipboard: Clipspace" : "Clipboard: System");
button.title = "Toggle clipboard source: ComfyUI Clipspace"; log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
button.style.backgroundColor = "#4a6cd4"; }
} else { }),
canvas.canvasLayers.clipboardPreference = 'system'; $el("span.switch-track"),
button.textContent = "📋 System"; $el("span.switch-labels", {}, [
button.title = "Toggle clipboard source: System Clipboard"; $el("span.text-clipspace", {}, ["Clipspace"]),
button.style.backgroundColor = "#4a4a4a"; $el("span.text-system", {}, ["System"])
} ]),
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`); $el("span.switch-knob", {}, [
}, $el("span.switch-icon")
onmouseenter: (e: MouseEvent) => { ])
const currentPreference = canvas.canvasLayers.clipboardPreference; ]);
const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip;
showTooltip(e.target as HTMLElement, tooltipContent); // Tooltip logic
}, switchEl.addEventListener("mouseenter", (e: MouseEvent) => {
onmouseleave: hideTooltip const checked = (switchEl.querySelector('input[type="checkbox"]') as HTMLInputElement).checked;
}) const tooltipContent = checked ? clipspaceClipboardTooltip : systemClipboardTooltip;
showTooltip(switchEl, tooltipContent);
});
switchEl.addEventListener("mouseleave", hideTooltip);
// Dynamic icon and text update on toggle
const input = switchEl.querySelector('input[type="checkbox"]') as HTMLInputElement;
const knobIcon = switchEl.querySelector('.switch-knob .switch-icon') as HTMLElement;
const updateSwitchView = (isClipspace: boolean) => {
const iconTool = isClipspace ? LAYERFORGE_TOOLS.CLIPSPACE : LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD;
const icon = iconLoader.getIcon(iconTool);
if (icon instanceof HTMLImageElement) {
knobIcon.innerHTML = '';
const clonedIcon = icon.cloneNode() as HTMLImageElement;
clonedIcon.style.width = '20px';
clonedIcon.style.height = '20px';
knobIcon.appendChild(clonedIcon);
} else {
knobIcon.textContent = isClipspace ? "🗂️" : "📋";
}
};
input.addEventListener('change', () => updateSwitchView(input.checked));
// Initial state
iconLoader.preloadToolIcons().then(() => {
updateSwitchView(isClipspace);
});
return switchEl;
})()
]), ]),
]), ]),
@@ -477,8 +502,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
min: "1", min: "1",
max: "200", max: "200",
value: "20", value: "20",
oninput: (e: Event) => canvas.maskTool.setBrushSize(parseInt((e.target as HTMLInputElement).value)) oninput: (e: Event) => {
}) const value = (e.target as HTMLInputElement).value;
canvas.maskTool.setBrushSize(parseInt(value));
const valueEl = document.getElementById('brush-size-value');
if (valueEl) valueEl.textContent = `${value}px`;
}
}),
$el("div.slider-value", {id: "brush-size-value"}, ["20px"])
]), ]),
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [ $el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
$el("label", {for: "brush-strength-slider", textContent: "Strength:"}), $el("label", {for: "brush-strength-slider", textContent: "Strength:"}),
@@ -489,8 +520,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
max: "1", max: "1",
step: "0.05", step: "0.05",
value: "0.5", value: "0.5",
oninput: (e: Event) => canvas.maskTool.setBrushStrength(parseFloat((e.target as HTMLInputElement).value)) oninput: (e: Event) => {
}) const value = (e.target as HTMLInputElement).value;
canvas.maskTool.setBrushStrength(parseFloat(value));
const valueEl = document.getElementById('brush-strength-value');
if (valueEl) valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
}
}),
$el("div.slider-value", {id: "brush-strength-value"}, ["50%"])
]), ]),
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [ $el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
$el("label", {for: "brush-hardness-slider", textContent: "Hardness:"}), $el("label", {for: "brush-hardness-slider", textContent: "Hardness:"}),
@@ -501,8 +538,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
max: "1", max: "1",
step: "0.05", step: "0.05",
value: "0.5", value: "0.5",
oninput: (e: Event) => canvas.maskTool.setBrushHardness(parseFloat((e.target as HTMLInputElement).value)) oninput: (e: Event) => {
}) const value = (e.target as HTMLInputElement).value;
canvas.maskTool.setBrushHardness(parseFloat(value));
const valueEl = document.getElementById('brush-hardness-value');
if (valueEl) valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
}
}),
$el("div.slider-value", {id: "brush-hardness-value"}, ["50%"])
]), ]),
$el("button.painter-button.mask-control", { $el("button.painter-button.mask-control", {
textContent: "Clear Mask", textContent: "Clear Mask",
@@ -519,10 +562,9 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
$el("div.painter-separator"), $el("div.painter-separator"),
$el("div.painter-button-group", {}, [ $el("div.painter-button-group", {}, [
$el("button.painter-button", { $el("button.painter-button.success", {
textContent: "Run GC", textContent: "Run GC",
title: "Run Garbage Collection to clean unused images", title: "Run Garbage Collection to clean unused images",
style: {backgroundColor: "#4a7c59", borderColor: "#3a6c49"},
onclick: async () => { onclick: async () => {
try { try {
const stats = canvas.imageReferenceManager.getStats(); const stats = canvas.imageReferenceManager.getStats();
@@ -540,10 +582,9 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
} }
} }
}), }),
$el("button.painter-button", { $el("button.painter-button.danger", {
textContent: "Clear Cache", textContent: "Clear Cache",
title: "Clear all saved canvas states from browser storage", title: "Clear all saved canvas states from browser storage",
style: {backgroundColor: "#c54747", borderColor: "#a53737"},
onclick: async () => { onclick: async () => {
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) { if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
try { try {
@@ -796,43 +837,41 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
let backdrop: HTMLDivElement | null = null; let backdrop: HTMLDivElement | null = null;
let originalParent: HTMLElement | null = null; let originalParent: HTMLElement | null = null;
let isEditorOpen = false; let isEditorOpen = false;
let viewportAdjustment = { x: 0, y: 0 };
/** /**
* Adjusts the viewport to keep the content centered when the container size changes. * Adjusts the viewport when entering fullscreen mode.
* @param rectA The original rectangle.
* @param rectB The new rectangle.
* @param direction Determines whether to apply the adjustment for opening (-1) or closing (1).
*/ */
const adjustViewportForCentering = (rectA: DOMRect, rectB: DOMRect, direction: 1 | -1) => { const adjustViewportOnOpen = (originalRect: DOMRect) => {
if (!rectA || !rectB) return; const fullscreenRect = canvasContainer.getBoundingClientRect();
const widthDiff = fullscreenRect.width - originalRect.width;
const heightDiff = fullscreenRect.height - originalRect.height;
const widthDiff = rectB.width - rectA.width;
const heightDiff = rectB.height - rectA.height;
const adjustX = (widthDiff / 2) / canvas.viewport.zoom; const adjustX = (widthDiff / 2) / canvas.viewport.zoom;
const adjustY = (heightDiff / 2) / canvas.viewport.zoom; const adjustY = (heightDiff / 2) / canvas.viewport.zoom;
canvas.viewport.x -= adjustX * direction;
canvas.viewport.y -= adjustY * direction;
const action = direction === 1 ? 'OPENING' : 'CLOSING'; // Store the adjustment
log.info(`FULLSCREEN ${action} - Viewport adjusted for centering:`, { viewportAdjustment = { x: adjustX, y: adjustY };
widthDiff, heightDiff, adjustX, adjustY,
viewport_after: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom } // Apply the adjustment
}); canvas.viewport.x -= viewportAdjustment.x;
} canvas.viewport.y -= viewportAdjustment.y;
};
/**
* Restores the viewport when exiting fullscreen mode.
*/
const adjustViewportOnClose = () => {
// Apply the stored adjustment in reverse
canvas.viewport.x += viewportAdjustment.x;
canvas.viewport.y += viewportAdjustment.y;
// Reset adjustment
viewportAdjustment = { x: 0, y: 0 };
};
const closeEditor = () => { const closeEditor = () => {
// Get fullscreen rect BEFORE removing from DOM
const fullscreenRect = backdrop?.querySelector('.painter-modal-content')?.getBoundingClientRect();
const currentRect = originalParent?.getBoundingClientRect();
log.info(`FULLSCREEN CLOSING - Window sizes:`, {
current: { width: currentRect?.width, height: currentRect?.height },
fullscreen: { width: fullscreenRect?.width, height: fullscreenRect?.height },
viewport_before: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
});
if (originalParent && backdrop) { if (originalParent && backdrop) {
originalParent.appendChild(mainContainer); originalParent.appendChild(mainContainer);
document.body.removeChild(backdrop); document.body.removeChild(backdrop);
@@ -846,12 +885,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
document.removeEventListener('keydown', handleEscKey); document.removeEventListener('keydown', handleEscKey);
setTimeout(() => { setTimeout(() => {
// Use the actual canvas container for centering calculation adjustViewportOnClose();
const currentCanvasContainer = originalParent!.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
const fullscreenCanvasContainer = backdrop!.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
const currentRect = currentCanvasContainer.getBoundingClientRect();
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
adjustViewportForCentering(currentRect, fullscreenRect, -1);
canvas.render(); canvas.render();
if (node.onResize) { if (node.onResize) {
node.onResize(); node.onResize();
@@ -865,7 +899,6 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
closeEditor(); closeEditor();
log.info("Fullscreen editor closed via ESC key");
} }
}; };
@@ -875,13 +908,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
return; return;
} }
const originalRect = canvasContainer.getBoundingClientRect();
originalParent = mainContainer.parentElement; originalParent = mainContainer.parentElement;
if (!originalParent) { if (!originalParent) {
log.error("Could not find original parent of the canvas container!"); log.error("Could not find original parent of the canvas container!");
return; return;
} }
backdrop = $el("div.painter-modal-backdrop") as HTMLDivElement; backdrop = $el("div.painter-modal-backdrop") as HTMLDivElement;
const modalContent = $el("div.painter-modal-content") as HTMLDivElement; const modalContent = $el("div.painter-modal-content") as HTMLDivElement;
@@ -897,12 +931,8 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
document.addEventListener('keydown', handleEscKey); document.addEventListener('keydown', handleEscKey);
setTimeout(() => { setTimeout(() => {
// Use the actual canvas container for centering calculation adjustViewportOnOpen(originalRect);
const originalCanvasContainer = originalParent!.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
const fullscreenCanvasContainer = modalContent.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
const originalRect = originalCanvasContainer.getBoundingClientRect();
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
adjustViewportForCentering(originalRect, fullscreenRect, 1);
canvas.render(); canvas.render();
if (node.onResize) { if (node.onResize) {
node.onResize(); node.onResize();

View File

@@ -1,54 +1,96 @@
.painter-button { .painter-button {
background: linear-gradient(to bottom, #4a4a4a, #3a3a3a); background-color: #444;
border: 1px solid #2a2a2a; border: 1px solid #555;
border-radius: 4px; border-radius: 5px;
color: #ffffff; color: #e0e0e0;
padding: 6px 12px; padding: 6px 14px;
font-size: 12px; font-size: 12px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease-in-out;
min-width: 80px; min-width: 80px;
text-align: center; text-align: center;
margin: 2px; margin: 2px;
text-shadow: 0 1px 1px rgba(0,0,0,0.2); box-shadow: 0 1px 2px rgba(0,0,0,0.1);
} }
.painter-button:hover { .painter-button:hover {
background: linear-gradient(to bottom, #5a5a5a, #4a4a4a); background-color: #555;
box-shadow: 0 1px 3px rgba(0,0,0,0.2); border-color: #666;
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
transform: translateY(-1px);
} }
.painter-button:active { .painter-button:active {
background: linear-gradient(to bottom, #3a3a3a, #4a4a4a); background-color: #3a3a3a;
transform: translateY(1px); transform: translateY(0);
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
} }
.painter-button:disabled, .painter-button:disabled,
.painter-button:disabled:hover { .painter-button:disabled:hover {
background: #555; background-color: #3a3a3a;
color: #888; color: #777;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
box-shadow: none; box-shadow: none;
border-color: #444; border-color: #4a4a4a;
opacity: 0.6;
} }
.painter-button.primary { .painter-button.primary {
background: linear-gradient(to bottom, #4a6cd4, #3a5cc4); background-color: #3a76d6;
border-color: #2a4cb4; border-color: #2a6ac4;
color: #fff;
text-shadow: none;
} }
.painter-button.primary:hover { .painter-button.primary:hover {
background: linear-gradient(to bottom, #5a7ce4, #4a6cd4); background-color: #4a86e4;
border-color: #3a76d6;
}
.painter-button.success {
background-color: #4a7c59;
border-color: #3a6c49;
color: #fff;
}
.painter-button.success:hover {
background-color: #5a8c69;
border-color: #4a7c59;
}
.painter-button.danger {
background-color: #c54747;
border-color: #a53737;
color: #fff;
}
.painter-button.danger:hover {
background-color: #d55757;
border-color: #c54747;
}
.painter-button.icon-button {
width: 30px;
height: 30px;
min-width: 30px;
padding: 0;
font-size: 16px;
line-height: 30px; /* Match height */
display: flex;
align-items: center;
justify-content: center;
} }
.painter-controls { .painter-controls {
background: linear-gradient(to bottom, #404040, #383838); background-color: #2f2f2f;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #202020;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 1px 3px rgba(0,0,0,0.2);
padding: 8px; padding: 10px;
display: flex; display: flex;
gap: 6px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
@@ -56,57 +98,198 @@
.painter-slider-container { .painter-slider-container {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 8px; gap: 4px;
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
min-width: 100px;
} }
.painter-slider-container input[type="range"] { .painter-slider-container input[type="range"] {
-webkit-appearance: none;
width: 80px; width: 80px;
height: 4px;
background: #555;
border-radius: 2px;
outline: none;
padding: 0;
margin: 0;
} }
.painter-slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
border: 2px solid #555;
transition: background 0.2s;
}
.painter-slider-container input[type="range"]::-webkit-slider-thumb:hover {
background: #fff;
}
.painter-slider-container input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
border: 2px solid #555;
}
.slider-value {
font-size: 11px;
color: #bbb;
margin-top: 2px;
min-height: 14px;
text-align: center;
}
.painter-button-group { .painter-button-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
background-color: rgba(0,0,0,0.2); background-color: transparent;
padding: 4px; padding: 0;
border-radius: 6px; border-radius: 6px;
} }
.painter-clipboard-group { .painter-clipboard-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 4px;
background-color: rgba(0,0,0,0.15);
padding: 3px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.1);
position: relative;
}
.painter-clipboard-group::before {
content: "";
position: absolute;
top: -2px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(74, 108, 212, 0.6), transparent);
border-radius: 1px;
} }
.painter-clipboard-group .painter-button { .painter-clipboard-group .painter-button {
margin: 1px; margin: 1px;
height: 30px; /* Match switch height */
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
} }
/* --- Clipboard Switch Modern --- */
.clipboard-switch {
position: relative;
width: 90px;
height: 30px;
box-sizing: border-box;
background-color: #444;
border-radius: 5px;
border: 1px solid #555;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
user-select: none;
padding: 0;
font-family: inherit;
font-size: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.clipboard-switch:hover {
background-color: #555;
border-color: #666;
}
.clipboard-switch:active {
background-color: #3a3a3a;
}
.clipboard-switch input[type="checkbox"] {
display: none;
}
.clipboard-switch .switch-track {
display: none;
}
.clipboard-switch .switch-knob {
position: absolute;
top: 2px;
left: 2px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: #5a5a5a;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
z-index: 2;
}
.clipboard-switch .switch-labels {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
color: #e0e0e0;
pointer-events: none;
z-index: 1;
transition: opacity 0.3s ease-in-out;
}
.clipboard-switch .switch-labels .text-clipspace,
.clipboard-switch .switch-labels .text-system {
position: absolute;
transition: opacity 0.2s ease-in-out;
}
.clipboard-switch .switch-labels .text-clipspace { opacity: 0; }
.clipboard-switch .switch-labels .text-system { opacity: 1; padding-left: 20px; }
.clipboard-switch .switch-knob .switch-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.clipboard-switch .switch-knob .switch-icon img {
width: 100%;
height: 100%;
}
/* Checked state */
.clipboard-switch:has(input:checked) {
background-color: #3a76d6;
border-color: #2a6ac4;
}
.clipboard-switch:has(input:checked):hover {
background-color: #4a86e4;
border-color: #3a76d6;
}
.clipboard-switch input:checked ~ .switch-knob {
left: calc(100% - 26px);
background-color: #fff;
}
.clipboard-switch input:checked ~ .switch-knob .switch-icon img {
filter: invert(35%) sepia(100%) saturate(1500%) hue-rotate(200deg) brightness(90%) contrast(100%);
}
.clipboard-switch input:checked ~ .switch-labels .text-clipspace {
opacity: 1;
color: #fff;
padding-right: 20px;
}
.clipboard-switch input:checked ~ .switch-labels .text-system {
opacity: 0;
}
.painter-separator { .painter-separator {
width: 1px; width: 1px;
height: 28px; height: 24px;
background-color: #2a2a2a; background-color: #444;
margin: 0 8px; margin: 0 8px;
} }

View File

@@ -18,11 +18,18 @@ export const LAYERFORGE_TOOLS = {
BRUSH: 'brush', BRUSH: 'brush',
ERASER: 'eraser', ERASER: 'eraser',
SHAPE: 'shape', SHAPE: 'shape',
SETTINGS: 'settings' SETTINGS: 'settings',
SYSTEM_CLIPBOARD: 'system_clipboard',
CLIPSPACE: 'clipspace',
} as const; } as const;
// SVG Icons for LayerForge tools // SVG Icons for LayerForge tools
const SYSTEM_CLIPBOARD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 15H7v-2h10v2zm0-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>`;
const CLIPSPACE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M17 7H7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm0 12H7V9h10v10z"/><path d="M19 3H9c-1.1 0-2 .9-2 2v2h2V5h10v10h-2v2h2c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`;
const LAYERFORGE_TOOL_ICONS = { const LAYERFORGE_TOOL_ICONS = {
[LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SYSTEM_CLIPBOARD_ICON_SVG)}`,
[LAYERFORGE_TOOLS.CLIPSPACE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CLIPSPACE_ICON_SVG)}`,
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`, [LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`, [LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,