diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js
index 428d52e..1bf07af 100644
--- a/js/CanvasRenderer.js
+++ b/js/CanvasRenderer.js
@@ -38,6 +38,27 @@ export class CanvasRenderer {
});
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() {
if (this.renderAnimationFrame) {
this.isDirty = true;
@@ -148,13 +169,11 @@ export class CanvasRenderer {
const interaction = this.canvas.interaction;
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
const rect = interaction.canvasResizeRect;
- ctx.save();
- ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
- ctx.lineWidth = 2 / this.canvas.viewport.zoom;
- ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
- ctx.setLineDash([]);
- ctx.restore();
+ this.drawStyledRect(ctx, rect, {
+ strokeStyle: 'rgba(0, 255, 0, 0.8)',
+ lineWidth: 2,
+ dashPattern: [8, 4]
+ });
if (rect.width > 0 && rect.height > 0) {
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
const textWorldX = rect.x + rect.width / 2;
@@ -166,13 +185,11 @@ export class CanvasRenderer {
}
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
const rect = interaction.canvasMoveRect;
- ctx.save();
- ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
- ctx.lineWidth = 2 / this.canvas.viewport.zoom;
- ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
- ctx.setLineDash([]);
- ctx.restore();
+ this.drawStyledRect(ctx, rect, {
+ strokeStyle: 'rgba(0, 150, 255, 0.8)',
+ lineWidth: 2,
+ dashPattern: [10, 5]
+ });
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
const textWorldX = rect.x + rect.width / 2;
const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);
@@ -327,13 +344,11 @@ export class CanvasRenderer {
width: baseWidth + ext.left + ext.right,
height: baseHeight + ext.top + ext.bottom
};
- ctx.save();
- ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; // Yellow color for preview
- ctx.lineWidth = 3 / this.canvas.viewport.zoom;
- ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
- ctx.strokeRect(previewBounds.x, previewBounds.y, previewBounds.width, previewBounds.height);
- ctx.setLineDash([]);
- ctx.restore();
+ this.drawStyledRect(ctx, previewBounds, {
+ strokeStyle: 'rgba(255, 255, 0, 0.8)',
+ lineWidth: 3,
+ dashPattern: [8, 4]
+ });
}
drawPendingGenerationAreas(ctx) {
const areasToDraw = [];
@@ -354,12 +369,11 @@ export class CanvasRenderer {
}
// 3. Draw all collected areas
areasToDraw.forEach(area => {
- ctx.save();
- ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color
- ctx.lineWidth = 3 / this.canvas.viewport.zoom;
- ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
- ctx.strokeRect(area.x, area.y, area.width, area.height);
- ctx.restore();
+ this.drawStyledRect(ctx, area, {
+ strokeStyle: 'rgba(0, 150, 255, 0.9)',
+ lineWidth: 3,
+ dashPattern: [12, 6]
+ });
});
}
drawMaskAreaBounds(ctx) {
@@ -375,12 +389,11 @@ export class CanvasRenderer {
width: maskTool.getMask().width,
height: maskTool.getMask().height
};
- ctx.save();
- ctx.strokeStyle = 'rgba(255, 100, 100, 0.7)'; // Red color for mask area bounds
- ctx.lineWidth = 2 / this.canvas.viewport.zoom;
- ctx.setLineDash([6 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
- ctx.strokeRect(maskBounds.x, maskBounds.y, maskBounds.width, maskBounds.height);
- ctx.setLineDash([]);
+ this.drawStyledRect(ctx, maskBounds, {
+ strokeStyle: 'rgba(255, 100, 100, 0.7)',
+ lineWidth: 2,
+ dashPattern: [6, 6]
+ });
// Add text label to show this is the mask drawing area
const textWorldX = maskBounds.x + maskBounds.width / 2;
const textWorldY = maskBounds.y - (10 / this.canvas.viewport.zoom);
@@ -389,6 +402,5 @@ export class CanvasRenderer {
backgroundColor: "rgba(255, 100, 100, 0.8)",
padding: 8
});
- ctx.restore();
}
}
diff --git a/js/CanvasView.js b/js/CanvasView.js
index ea3aeab..d532fa0 100644
--- a/js/CanvasView.js
+++ b/js/CanvasView.js
@@ -65,20 +65,14 @@ async function createCanvasWidget(node, widget, app) {
},
}, [
$el("div.painter-button-group", {}, [
- $el("button.painter-button", {
+ $el("button.painter-button.icon-button", {
id: `open-editor-btn-${node.id}`,
textContent: "⛶",
title: "Open in Editor",
- style: { minWidth: "40px", maxWidth: "40px", fontWeight: "bold" },
}),
- $el("button.painter-button", {
+ $el("button.painter-button.icon-button", {
textContent: "?",
title: "Show shortcuts",
- style: {
- minWidth: "30px",
- maxWidth: "30px",
- fontWeight: "bold",
- },
onmouseenter: (e) => {
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
showTooltip(e.target, content);
@@ -131,38 +125,63 @@ async function createCanvasWidget(node, widget, app) {
canvas.canvasLayers.handlePaste(addMode);
}
}),
- $el("button.painter-button", {
- id: `clipboard-toggle-${node.id}`,
- textContent: "📋 System",
- title: "Toggle clipboard source: System Clipboard",
- style: {
- minWidth: "100px",
- fontSize: "11px",
- backgroundColor: "#4a4a4a"
- },
- onclick: (e) => {
- const button = e.target;
- if (canvas.canvasLayers.clipboardPreference === 'system') {
- canvas.canvasLayers.clipboardPreference = 'clipspace';
- button.textContent = "📋 Clipspace";
- button.title = "Toggle clipboard source: ComfyUI Clipspace";
- button.style.backgroundColor = "#4a6cd4";
+ (() => {
+ // Modern clipboard switch
+ // Initial state: checked = clipspace, unchecked = system
+ const isClipspace = canvas.canvasLayers.clipboardPreference === 'clipspace';
+ const switchId = `clipboard-switch-${node.id}`;
+ const switchEl = $el("label.clipboard-switch", { id: switchId }, [
+ $el("input", {
+ type: "checkbox",
+ checked: isClipspace,
+ onchange: (e) => {
+ const checked = e.target.checked;
+ canvas.canvasLayers.clipboardPreference = checked ? 'clipspace' : 'system';
+ // For accessibility, update ARIA label
+ switchEl.setAttribute('aria-label', checked ? "Clipboard: Clipspace" : "Clipboard: System");
+ log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
+ }
+ }),
+ $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 {
- canvas.canvasLayers.clipboardPreference = 'system';
- button.textContent = "📋 System";
- button.title = "Toggle clipboard source: System Clipboard";
- button.style.backgroundColor = "#4a4a4a";
+ knobIcon.textContent = isClipspace ? "🗂️" : "📋";
}
- log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
- },
- onmouseenter: (e) => {
- const currentPreference = canvas.canvasLayers.clipboardPreference;
- const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip;
- showTooltip(e.target, tooltipContent);
- },
- onmouseleave: hideTooltip
- })
+ };
+ input.addEventListener('change', () => updateSwitchView(input.checked));
+ // Initial state
+ iconLoader.preloadToolIcons().then(() => {
+ updateSwitchView(isClipspace);
+ });
+ return switchEl;
+ })()
]),
]),
$el("div.painter-separator"),
@@ -440,8 +459,15 @@ async function createCanvasWidget(node, widget, app) {
min: "1",
max: "200",
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("label", { for: "brush-strength-slider", textContent: "Strength:" }),
@@ -452,8 +478,15 @@ async function createCanvasWidget(node, widget, app) {
max: "1",
step: "0.05",
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("label", { for: "brush-hardness-slider", textContent: "Hardness:" }),
@@ -464,8 +497,15 @@ async function createCanvasWidget(node, widget, app) {
max: "1",
step: "0.05",
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", {
textContent: "Clear Mask",
@@ -481,10 +521,9 @@ async function createCanvasWidget(node, widget, app) {
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
- $el("button.painter-button", {
+ $el("button.painter-button.success", {
textContent: "Run GC",
title: "Run Garbage Collection to clean unused images",
- style: { backgroundColor: "#4a7c59", borderColor: "#3a6c49" },
onclick: async () => {
try {
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",
title: "Clear all saved canvas states from browser storage",
- style: { backgroundColor: "#c54747", borderColor: "#a53737" },
onclick: async () => {
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
try {
@@ -736,36 +774,33 @@ async function createCanvasWidget(node, widget, app) {
let backdrop = null;
let originalParent = null;
let isEditorOpen = false;
+ let viewportAdjustment = { x: 0, y: 0 };
/**
- * Adjusts the viewport to keep the content centered when the container size changes.
- * @param rectA The original rectangle.
- * @param rectB The new rectangle.
- * @param direction Determines whether to apply the adjustment for opening (-1) or closing (1).
+ * Adjusts the viewport when entering fullscreen mode.
*/
- const adjustViewportForCentering = (rectA, rectB, direction) => {
- if (!rectA || !rectB)
- return;
- const widthDiff = rectB.width - rectA.width;
- const heightDiff = rectB.height - rectA.height;
+ const adjustViewportOnOpen = (originalRect) => {
+ const fullscreenRect = canvasContainer.getBoundingClientRect();
+ const widthDiff = fullscreenRect.width - originalRect.width;
+ const heightDiff = fullscreenRect.height - originalRect.height;
const adjustX = (widthDiff / 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';
- log.info(`FULLSCREEN ${action} - Viewport adjusted for centering:`, {
- widthDiff, heightDiff, adjustX, adjustY,
- viewport_after: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
- });
+ // Store the adjustment
+ viewportAdjustment = { x: adjustX, y: adjustY };
+ // 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 = () => {
- // 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) {
originalParent.appendChild(mainContainer);
document.body.removeChild(backdrop);
@@ -776,12 +811,7 @@ async function createCanvasWidget(node, widget, app) {
// Remove ESC key listener when editor closes
document.removeEventListener('keydown', handleEscKey);
setTimeout(() => {
- // Use the actual canvas container for centering calculation
- 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);
+ adjustViewportOnClose();
canvas.render();
if (node.onResize) {
node.onResize();
@@ -794,7 +824,6 @@ async function createCanvasWidget(node, widget, app) {
e.preventDefault();
e.stopPropagation();
closeEditor();
- log.info("Fullscreen editor closed via ESC key");
}
};
openEditorBtn.onclick = () => {
@@ -802,6 +831,7 @@ async function createCanvasWidget(node, widget, app) {
closeEditor();
return;
}
+ const originalRect = canvasContainer.getBoundingClientRect();
originalParent = mainContainer.parentElement;
if (!originalParent) {
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
document.addEventListener('keydown', handleEscKey);
setTimeout(() => {
- // Use the actual canvas container for centering calculation
- 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);
+ adjustViewportOnOpen(originalRect);
canvas.render();
if (node.onResize) {
node.onResize();
diff --git a/js/css/canvas_view.css b/js/css/canvas_view.css
index ff2ab75..3c27a68 100644
--- a/js/css/canvas_view.css
+++ b/js/css/canvas_view.css
@@ -1,54 +1,96 @@
.painter-button {
- background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
- border: 1px solid #2a2a2a;
- border-radius: 4px;
- color: #ffffff;
- padding: 6px 12px;
+ background-color: #444;
+ border: 1px solid #555;
+ border-radius: 5px;
+ color: #e0e0e0;
+ padding: 6px 14px;
font-size: 12px;
+ font-weight: 500;
cursor: pointer;
- transition: all 0.2s ease;
+ transition: all 0.2s ease-in-out;
min-width: 80px;
text-align: center;
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 {
- background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+ background-color: #555;
+ border-color: #666;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.15);
+ transform: translateY(-1px);
}
.painter-button:active {
- background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
- transform: translateY(1px);
+ background-color: #3a3a3a;
+ transform: translateY(0);
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
}
.painter-button:disabled,
.painter-button:disabled:hover {
- background: #555;
- color: #888;
+ background-color: #3a3a3a;
+ color: #777;
cursor: not-allowed;
transform: none;
box-shadow: none;
- border-color: #444;
+ border-color: #4a4a4a;
+ opacity: 0.6;
}
.painter-button.primary {
- background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
- border-color: #2a4cb4;
+ background-color: #3a76d6;
+ border-color: #2a6ac4;
+ color: #fff;
+ text-shadow: none;
}
.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 {
- background: linear-gradient(to bottom, #404040, #383838);
- border-bottom: 1px solid #2a2a2a;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- padding: 8px;
+ background-color: #2f2f2f;
+ border-bottom: 1px solid #202020;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+ padding: 10px;
display: flex;
- gap: 6px;
+ gap: 8px;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
@@ -56,57 +98,198 @@
.painter-slider-container {
display: flex;
+ flex-direction: column;
align-items: center;
- gap: 8px;
+ gap: 4px;
color: #fff;
font-size: 12px;
+ min-width: 100px;
}
.painter-slider-container input[type="range"] {
+ -webkit-appearance: none;
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 {
display: flex;
align-items: center;
- gap: 6px;
- background-color: rgba(0,0,0,0.2);
- padding: 4px;
+ gap: 4px;
+ background-color: transparent;
+ padding: 0;
border-radius: 6px;
}
.painter-clipboard-group {
display: flex;
align-items: center;
- gap: 2px;
- 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;
+ gap: 4px;
}
.painter-clipboard-group .painter-button {
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 {
width: 1px;
- height: 28px;
- background-color: #2a2a2a;
+ height: 24px;
+ background-color: #444;
margin: 0 8px;
}
diff --git a/js/utils/IconLoader.js b/js/utils/IconLoader.js
index 26e909f..95dc4e6 100644
--- a/js/utils/IconLoader.js
+++ b/js/utils/IconLoader.js
@@ -16,10 +16,16 @@ export const LAYERFORGE_TOOLS = {
BRUSH: 'brush',
ERASER: 'eraser',
SHAPE: 'shape',
- SETTINGS: 'settings'
+ SETTINGS: 'settings',
+ SYSTEM_CLIPBOARD: 'system_clipboard',
+ CLIPSPACE: 'clipspace',
};
// SVG Icons for LayerForge tools
+const SYSTEM_CLIPBOARD_ICON_SVG = ``;
+const CLIPSPACE_ICON_SVG = ``;
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('')}`,
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
[LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts
index 9e7fb31..2c32b88 100644
--- a/src/CanvasRenderer.ts
+++ b/src/CanvasRenderer.ts
@@ -59,6 +59,37 @@ export class CanvasRenderer {
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() {
if (this.renderAnimationFrame) {
this.isDirty = true;
@@ -187,13 +218,11 @@ export class CanvasRenderer {
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
const rect = interaction.canvasResizeRect;
- ctx.save();
- ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
- ctx.lineWidth = 2 / this.canvas.viewport.zoom;
- ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
- ctx.setLineDash([]);
- ctx.restore();
+ this.drawStyledRect(ctx, rect, {
+ strokeStyle: 'rgba(0, 255, 0, 0.8)',
+ lineWidth: 2,
+ dashPattern: [8, 4]
+ });
if (rect.width > 0 && rect.height > 0) {
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
const textWorldX = rect.x + rect.width / 2;
@@ -207,13 +236,11 @@ export class CanvasRenderer {
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
const rect = interaction.canvasMoveRect;
- ctx.save();
- ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
- ctx.lineWidth = 2 / this.canvas.viewport.zoom;
- ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
- ctx.setLineDash([]);
- ctx.restore();
+ this.drawStyledRect(ctx, rect, {
+ strokeStyle: 'rgba(0, 150, 255, 0.8)',
+ lineWidth: 2,
+ dashPattern: [10, 5]
+ });
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
const textWorldX = rect.x + rect.width / 2;
@@ -397,13 +424,11 @@ export class CanvasRenderer {
height: baseHeight + ext.top + ext.bottom
};
- ctx.save();
- ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; // Yellow color for preview
- ctx.lineWidth = 3 / this.canvas.viewport.zoom;
- ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
- ctx.strokeRect(previewBounds.x, previewBounds.y, previewBounds.width, previewBounds.height);
- ctx.setLineDash([]);
- ctx.restore();
+ this.drawStyledRect(ctx, previewBounds, {
+ strokeStyle: 'rgba(255, 255, 0, 0.8)',
+ lineWidth: 3,
+ dashPattern: [8, 4]
+ });
}
drawPendingGenerationAreas(ctx: any) {
@@ -429,12 +454,11 @@ export class CanvasRenderer {
// 3. Draw all collected areas
areasToDraw.forEach(area => {
- ctx.save();
- ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color
- ctx.lineWidth = 3 / this.canvas.viewport.zoom;
- ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
- ctx.strokeRect(area.x, area.y, area.width, area.height);
- ctx.restore();
+ this.drawStyledRect(ctx, area, {
+ strokeStyle: 'rgba(0, 150, 255, 0.9)',
+ lineWidth: 3,
+ dashPattern: [12, 6]
+ });
});
}
@@ -454,12 +478,11 @@ export class CanvasRenderer {
height: maskTool.getMask().height
};
- ctx.save();
- ctx.strokeStyle = 'rgba(255, 100, 100, 0.7)'; // Red color for mask area bounds
- ctx.lineWidth = 2 / this.canvas.viewport.zoom;
- ctx.setLineDash([6 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
- ctx.strokeRect(maskBounds.x, maskBounds.y, maskBounds.width, maskBounds.height);
- ctx.setLineDash([]);
+ this.drawStyledRect(ctx, maskBounds, {
+ strokeStyle: 'rgba(255, 100, 100, 0.7)',
+ lineWidth: 2,
+ dashPattern: [6, 6]
+ });
// Add text label to show this is the mask drawing area
const textWorldX = maskBounds.x + maskBounds.width / 2;
@@ -470,7 +493,5 @@ export class CanvasRenderer {
backgroundColor: "rgba(255, 100, 100, 0.8)",
padding: 8
});
-
- ctx.restore();
}
}
diff --git a/src/CanvasView.ts b/src/CanvasView.ts
index 3e07575..050de0b 100644
--- a/src/CanvasView.ts
+++ b/src/CanvasView.ts
@@ -90,20 +90,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
},
}, [
$el("div.painter-button-group", {}, [
- $el("button.painter-button", {
+ $el("button.painter-button.icon-button", {
id: `open-editor-btn-${node.id}`,
textContent: "⛶",
title: "Open in Editor",
- style: {minWidth: "40px", maxWidth: "40px", fontWeight: "bold"},
}),
- $el("button.painter-button", {
+ $el("button.painter-button.icon-button", {
textContent: "?",
title: "Show shortcuts",
- style: {
- minWidth: "30px",
- maxWidth: "30px",
- fontWeight: "bold",
- },
onmouseenter: (e: MouseEvent) => {
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
showTooltip(e.target as HTMLElement, content);
@@ -155,37 +149,68 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
canvas.canvasLayers.handlePaste(addMode);
}
}),
- $el("button.painter-button", {
- id: `clipboard-toggle-${node.id}`,
- textContent: "📋 System",
- title: "Toggle clipboard source: System Clipboard",
- style: {
- minWidth: "100px",
- fontSize: "11px",
- backgroundColor: "#4a4a4a"
- },
- onclick: (e: MouseEvent) => {
- const button = e.target as HTMLButtonElement;
- if (canvas.canvasLayers.clipboardPreference === 'system') {
- canvas.canvasLayers.clipboardPreference = 'clipspace';
- button.textContent = "📋 Clipspace";
- button.title = "Toggle clipboard source: ComfyUI Clipspace";
- button.style.backgroundColor = "#4a6cd4";
- } else {
- canvas.canvasLayers.clipboardPreference = 'system';
- button.textContent = "📋 System";
- button.title = "Toggle clipboard source: System Clipboard";
- button.style.backgroundColor = "#4a4a4a";
- }
- log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
- },
- onmouseenter: (e: MouseEvent) => {
- const currentPreference = canvas.canvasLayers.clipboardPreference;
- const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip;
- showTooltip(e.target as HTMLElement, tooltipContent);
- },
- onmouseleave: hideTooltip
- })
+(() => {
+ // Modern clipboard switch
+ // Initial state: checked = clipspace, unchecked = system
+ const isClipspace = canvas.canvasLayers.clipboardPreference === 'clipspace';
+ const switchId = `clipboard-switch-${node.id}`;
+ const switchEl = $el("label.clipboard-switch", { id: switchId }, [
+ $el("input", {
+ type: "checkbox",
+ checked: isClipspace,
+ onchange: (e: Event) => {
+ const checked = (e.target as HTMLInputElement).checked;
+ canvas.canvasLayers.clipboardPreference = checked ? 'clipspace' : 'system';
+ // For accessibility, update ARIA label
+ switchEl.setAttribute('aria-label', checked ? "Clipboard: Clipspace" : "Clipboard: System");
+ log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
+ }
+ }),
+ $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: MouseEvent) => {
+ 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",
max: "200",
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("label", {for: "brush-strength-slider", textContent: "Strength:"}),
@@ -489,8 +520,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
max: "1",
step: "0.05",
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("label", {for: "brush-hardness-slider", textContent: "Hardness:"}),
@@ -501,8 +538,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
max: "1",
step: "0.05",
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", {
textContent: "Clear Mask",
@@ -519,10 +562,9 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
- $el("button.painter-button", {
+ $el("button.painter-button.success", {
textContent: "Run GC",
title: "Run Garbage Collection to clean unused images",
- style: {backgroundColor: "#4a7c59", borderColor: "#3a6c49"},
onclick: async () => {
try {
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",
title: "Clear all saved canvas states from browser storage",
- style: {backgroundColor: "#c54747", borderColor: "#a53737"},
onclick: async () => {
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
try {
@@ -796,43 +837,41 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
let backdrop: HTMLDivElement | null = null;
let originalParent: HTMLElement | null = null;
let isEditorOpen = false;
+ let viewportAdjustment = { x: 0, y: 0 };
/**
- * Adjusts the viewport to keep the content centered when the container size changes.
- * @param rectA The original rectangle.
- * @param rectB The new rectangle.
- * @param direction Determines whether to apply the adjustment for opening (-1) or closing (1).
+ * Adjusts the viewport when entering fullscreen mode.
*/
- const adjustViewportForCentering = (rectA: DOMRect, rectB: DOMRect, direction: 1 | -1) => {
- if (!rectA || !rectB) return;
+ const adjustViewportOnOpen = (originalRect: DOMRect) => {
+ 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 adjustY = (heightDiff / 2) / canvas.viewport.zoom;
-
- canvas.viewport.x -= adjustX * direction;
- canvas.viewport.y -= adjustY * direction;
- const action = direction === 1 ? 'OPENING' : 'CLOSING';
- log.info(`FULLSCREEN ${action} - Viewport adjusted for centering:`, {
- widthDiff, heightDiff, adjustX, adjustY,
- viewport_after: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
- });
- }
+ // Store the adjustment
+ viewportAdjustment = { x: adjustX, y: adjustY };
+
+ // 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 = () => {
- // 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) {
originalParent.appendChild(mainContainer);
document.body.removeChild(backdrop);
@@ -846,12 +885,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
document.removeEventListener('keydown', handleEscKey);
setTimeout(() => {
- // Use the actual canvas container for centering calculation
- 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);
+ adjustViewportOnClose();
canvas.render();
if (node.onResize) {
node.onResize();
@@ -865,7 +899,6 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
e.preventDefault();
e.stopPropagation();
closeEditor();
- log.info("Fullscreen editor closed via ESC key");
}
};
@@ -875,13 +908,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
return;
}
+ const originalRect = canvasContainer.getBoundingClientRect();
+
originalParent = mainContainer.parentElement;
if (!originalParent) {
log.error("Could not find original parent of the canvas container!");
return;
}
-
backdrop = $el("div.painter-modal-backdrop") 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);
setTimeout(() => {
- // Use the actual canvas container for centering calculation
- 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);
+ adjustViewportOnOpen(originalRect);
+
canvas.render();
if (node.onResize) {
node.onResize();
diff --git a/src/css/canvas_view.css b/src/css/canvas_view.css
index ff2ab75..3c27a68 100644
--- a/src/css/canvas_view.css
+++ b/src/css/canvas_view.css
@@ -1,54 +1,96 @@
.painter-button {
- background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
- border: 1px solid #2a2a2a;
- border-radius: 4px;
- color: #ffffff;
- padding: 6px 12px;
+ background-color: #444;
+ border: 1px solid #555;
+ border-radius: 5px;
+ color: #e0e0e0;
+ padding: 6px 14px;
font-size: 12px;
+ font-weight: 500;
cursor: pointer;
- transition: all 0.2s ease;
+ transition: all 0.2s ease-in-out;
min-width: 80px;
text-align: center;
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 {
- background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+ background-color: #555;
+ border-color: #666;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.15);
+ transform: translateY(-1px);
}
.painter-button:active {
- background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
- transform: translateY(1px);
+ background-color: #3a3a3a;
+ transform: translateY(0);
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
}
.painter-button:disabled,
.painter-button:disabled:hover {
- background: #555;
- color: #888;
+ background-color: #3a3a3a;
+ color: #777;
cursor: not-allowed;
transform: none;
box-shadow: none;
- border-color: #444;
+ border-color: #4a4a4a;
+ opacity: 0.6;
}
.painter-button.primary {
- background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
- border-color: #2a4cb4;
+ background-color: #3a76d6;
+ border-color: #2a6ac4;
+ color: #fff;
+ text-shadow: none;
}
.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 {
- background: linear-gradient(to bottom, #404040, #383838);
- border-bottom: 1px solid #2a2a2a;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- padding: 8px;
+ background-color: #2f2f2f;
+ border-bottom: 1px solid #202020;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+ padding: 10px;
display: flex;
- gap: 6px;
+ gap: 8px;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
@@ -56,57 +98,198 @@
.painter-slider-container {
display: flex;
+ flex-direction: column;
align-items: center;
- gap: 8px;
+ gap: 4px;
color: #fff;
font-size: 12px;
+ min-width: 100px;
}
.painter-slider-container input[type="range"] {
+ -webkit-appearance: none;
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 {
display: flex;
align-items: center;
- gap: 6px;
- background-color: rgba(0,0,0,0.2);
- padding: 4px;
+ gap: 4px;
+ background-color: transparent;
+ padding: 0;
border-radius: 6px;
}
.painter-clipboard-group {
display: flex;
align-items: center;
- gap: 2px;
- 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;
+ gap: 4px;
}
.painter-clipboard-group .painter-button {
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 {
width: 1px;
- height: 28px;
- background-color: #2a2a2a;
+ height: 24px;
+ background-color: #444;
margin: 0 8px;
}
diff --git a/src/utils/IconLoader.ts b/src/utils/IconLoader.ts
index 1146406..992666f 100644
--- a/src/utils/IconLoader.ts
+++ b/src/utils/IconLoader.ts
@@ -18,11 +18,18 @@ export const LAYERFORGE_TOOLS = {
BRUSH: 'brush',
ERASER: 'eraser',
SHAPE: 'shape',
- SETTINGS: 'settings'
+ SETTINGS: 'settings',
+ SYSTEM_CLIPBOARD: 'system_clipboard',
+ CLIPSPACE: 'clipspace',
} as const;
// SVG Icons for LayerForge tools
+const SYSTEM_CLIPBOARD_ICON_SVG = ``;
+const CLIPSPACE_ICON_SVG = ``;
+
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('')}`,
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,