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('')}`,