diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js
index 0885bfa..cb53a27 100644
--- a/js/CanvasInteractions.js
+++ b/js/CanvasInteractions.js
@@ -240,7 +240,7 @@ export class CanvasInteractions {
this.canvas.viewport.zoom = Math.max(0.1, Math.min(10, newZoom));
this.canvas.viewport.x = worldCoords.x - (mouseBufferX / this.canvas.viewport.zoom);
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
- } else if (this.canvas.selectedLayer) {
+ } else if (this.canvas.canvasSelection.selectedLayer) {
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left
diff --git a/js/CanvasView.js b/js/CanvasView.js
index 1371500..3661fcc 100644
--- a/js/CanvasView.js
+++ b/js/CanvasView.js
@@ -1,6 +1,7 @@
import {app} from "../../scripts/app.js";
import {api} from "../../scripts/api.js";
import {$el} from "../../scripts/ui.js";
+import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js";
import {Canvas} from "./Canvas.js";
import {clearAllCanvasStates} from "./db.js";
@@ -16,466 +17,16 @@ async function createCanvasWidget(node, widget, app) {
});
const imageCache = new ImageCache();
- const style = document.createElement('style');
- style.textContent = `
- .painter-button {
- background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
- border: 1px solid #2a2a2a;
- border-radius: 4px;
- color: #ffffff;
- padding: 6px 12px;
- font-size: 12px;
- cursor: pointer;
- transition: all 0.2s ease;
- min-width: 80px;
- text-align: center;
- margin: 2px;
- text-shadow: 0 1px 1px rgba(0,0,0,0.2);
- }
-
- .painter-button:hover {
- background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
- }
-
- .painter-button:active {
- background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
- transform: translateY(1px);
- }
-
- .painter-button:disabled,
- .painter-button:disabled:hover {
- background: #555;
- color: #888;
- cursor: not-allowed;
- transform: none;
- box-shadow: none;
- border-color: #444;
- }
-
- .painter-button.primary {
- background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
- border-color: #2a4cb4;
- }
-
- .painter-button.primary:hover {
- background: linear-gradient(to bottom, #5a7ce4, #4a6cd4);
- }
-
- .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;
- display: flex;
- gap: 6px;
- flex-wrap: wrap;
- align-items: center;
- justify-content: flex-start;
- }
-
- .painter-slider-container {
- display: flex;
- align-items: center;
- gap: 8px;
- color: #fff;
- font-size: 12px;
- }
-
- .painter-slider-container input[type="range"] {
- width: 80px;
- }
-
-
- .painter-button-group {
- display: flex;
- align-items: center;
- gap: 6px;
- background-color: rgba(0,0,0,0.2);
- padding: 4px;
- 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;
- }
-
- .painter-clipboard-group .painter-button {
- margin: 1px;
- }
-
- .painter-separator {
- width: 1px;
- height: 28px;
- background-color: #2a2a2a;
- margin: 0 8px;
- }
-
- .painter-container {
- background: #607080; /* 带蓝色的灰色背景 */
- border: 1px solid #4a5a6a;
- border-radius: 6px;
- box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
- transition: border-color 0.3s ease; /* Dodano dla płynnej zmiany ramki */
- }
-
- .painter-container.drag-over {
- border-color: #00ff00; /* Zielona ramka podczas przeciągania */
- border-style: dashed;
- }
-
- .painter-dialog {
- background: #404040;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
- padding: 20px;
- color: #ffffff;
- }
-
- .painter-dialog input {
- background: #303030;
- border: 1px solid #505050;
- border-radius: 4px;
- color: #ffffff;
- padding: 4px 8px;
- margin: 4px;
- width: 80px;
- }
-
- .painter-dialog button {
- background: #505050;
- border: 1px solid #606060;
- border-radius: 4px;
- color: #ffffff;
- padding: 4px 12px;
- margin: 4px;
- cursor: pointer;
- }
-
- .painter-dialog button:hover {
- background: #606060;
- }
-
- .blend-opacity-slider {
- width: 100%;
- margin: 5px 0;
- display: none;
- }
-
- .blend-mode-active .blend-opacity-slider {
- display: block;
- }
-
- .blend-mode-item {
- padding: 5px;
- cursor: pointer;
- position: relative;
- }
-
- .blend-mode-item.active {
- background-color: rgba(0,0,0,0.1);
- }
-
- .blend-mode-item.active {
- background-color: rgba(0,0,0,0.1);
- }
-
- .painter-tooltip {
- position: fixed;
- display: none;
- background: #3a3a3a;
- color: #f0f0f0;
- border: 1px solid #555;
- border-radius: 8px;
- padding: 12px 18px;
- z-index: 9999;
- font-size: 13px;
- line-height: 1.7;
- width: auto;
- max-width: min(500px, calc(100vw - 40px));
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
- pointer-events: none;
- transform-origin: top left;
- transition: transform 0.2s ease;
- will-change: transform;
- }
-
- .painter-tooltip.scale-down {
- transform: scale(0.9);
- transform-origin: top;
- }
-
- .painter-tooltip.scale-down-more {
- transform: scale(0.8);
- transform-origin: top;
- }
-
- .painter-tooltip table {
- width: 100%;
- border-collapse: collapse;
- margin: 8px 0;
- }
-
- .painter-tooltip table td {
- padding: 2px 8px;
- vertical-align: middle;
- }
-
- .painter-tooltip table td:first-child {
- width: auto;
- white-space: nowrap;
- min-width: fit-content;
- }
-
- .painter-tooltip table td:last-child {
- width: auto;
- }
-
- .painter-tooltip table tr:nth-child(odd) td {
- background-color: rgba(0,0,0,0.1);
- }
-
- @media (max-width: 600px) {
- .painter-tooltip {
- font-size: 11px;
- padding: 8px 12px;
- }
- .painter-tooltip table td {
- padding: 2px 4px;
- }
- .painter-tooltip kbd {
- padding: 1px 4px;
- font-size: 10px;
- }
- .painter-tooltip table td:first-child {
- width: 40%;
- }
- .painter-tooltip table td:last-child {
- width: 60%;
- }
- .painter-tooltip h4 {
- font-size: 12px;
- margin-top: 8px;
- margin-bottom: 4px;
- }
- }
-
- @media (max-width: 400px) {
- .painter-tooltip {
- font-size: 10px;
- padding: 6px 8px;
- }
- .painter-tooltip table td {
- padding: 1px 3px;
- }
- .painter-tooltip kbd {
- padding: 0px 3px;
- font-size: 9px;
- }
- .painter-tooltip table td:first-child {
- width: 35%;
- }
- .painter-tooltip table td:last-child {
- width: 65%;
- }
- .painter-tooltip h4 {
- font-size: 11px;
- margin-top: 6px;
- margin-bottom: 3px;
- }
- }
-
- .painter-tooltip::-webkit-scrollbar {
- width: 8px;
- }
-
- .painter-tooltip::-webkit-scrollbar-track {
- background: #2a2a2a;
- border-radius: 4px;
- }
-
- .painter-tooltip::-webkit-scrollbar-thumb {
- background: #555;
- border-radius: 4px;
- }
-
- .painter-tooltip::-webkit-scrollbar-thumb:hover {
- background: #666;
- }
-
- .painter-tooltip h4 {
- margin-top: 10px;
- margin-bottom: 5px;
- color: #4a90e2; /* Jasnoniebieski akcent */
- border-bottom: 1px solid #555;
- padding-bottom: 4px;
- }
-
- .painter-tooltip ul {
- list-style: none;
- padding-left: 10px;
- margin: 0;
- }
-
- .painter-tooltip kbd {
- background-color: #2a2a2a;
- border: 1px solid #1a1a1a;
- border-radius: 3px;
- padding: 2px 6px;
- font-family: monospace;
- font-size: 12px;
- color: #d0d0d0;
- }
-
- .painter-container.has-focus {
- /* Używamy box-shadow, aby stworzyć efekt zewnętrznej ramki,
- która nie wpłynie na rozmiar ani pozycję elementu. */
- box-shadow: 0 0 0 2px white;
- /* Możesz też zmienić kolor istniejącej ramki, ale box-shadow jest bardziej wyrazisty */
- /* border-color: white; */
- }
-
- .painter-button.matting-button {
- position: relative;
- transition: all 0.3s ease;
- }
-
- .painter-button.matting-button.loading {
- padding-right: 36px; /* Make space for spinner */
- cursor: wait;
- }
-
- .painter-button.matting-button .matting-spinner {
- display: none;
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- width: 16px;
- height: 16px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-radius: 50%;
- border-top-color: #fff;
- animation: matting-spin 1s linear infinite;
- }
-
- .painter-button.matting-button.loading .matting-spinner {
- display: block;
- }
-
- @keyframes matting-spin {
- to {
- transform: translateY(-50%) rotate(360deg);
- }
- }
- `;
- style.textContent += `
- .painter-modal-backdrop {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background-color: rgba(0, 0, 0, 0.8);
- z-index: 111;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .painter-modal-content {
- width: 90vw;
- height: 90vh;
- background-color: #353535;
- border: 1px solid #222;
- border-radius: 8px;
- box-shadow: 0 5px 25px rgba(0,0,0,0.5);
- display: flex;
- flex-direction: column;
- position: relative;
- }
-
-
- `;
- document.head.appendChild(style);
-
const helpTooltip = $el("div.painter-tooltip", {
id: `painter-help-tooltip-${node.id}`,
});
- const standardShortcuts = `
-
Canvas Control
-
- | Click + Drag | Pan canvas view |
- | Mouse Wheel | Zoom view in/out |
- | Shift + Click (background) | Start resizing canvas area |
- | Shift + Ctrl + Click | Start moving entire canvas |
- | Single Click (background) | Deselect all layers |
-
-
- Clipboard & I/O
-
- | Ctrl + C | Copy selected layer(s) |
- | Ctrl + V | Paste from clipboard (image or internal layers) |
- | Drag & Drop Image File | Add image as a new layer |
-
-
- Layer Interaction
-
- | Click + Drag | Move selected layer(s) |
- | Ctrl + Click | Add/Remove layer from selection |
- | Alt + Drag | Clone selected layer(s) |
- | Right Click | Show blend mode & opacity menu |
- | Mouse Wheel | Scale layer (snaps to grid) |
- | Ctrl + Mouse Wheel | Fine-scale layer |
- | Shift + Mouse Wheel | Rotate layer by 5° steps |
- | Shift + Ctrl + Mouse Wheel | Snap rotation to 5° increments |
- | Arrow Keys | Nudge layer by 1px |
- | Shift + Arrow Keys | Nudge layer by 10px |
- | [ or ] | Rotate by 1° |
- | Shift + [ or ] | Rotate by 10° |
- | Delete | Delete selected layer(s) |
-
-
- Transform Handles (on selected layer)
-
- | Drag Corner/Side | Resize layer |
- | Drag Rotation Handle | Rotate layer |
- | Hold Shift | Keep aspect ratio / Snap rotation to 15° |
- | Hold Ctrl | Snap to grid |
-
- `;
-
- const maskShortcuts = `
- Mask Mode
-
- | Click + Drag | Paint on the mask |
- | Middle Mouse Button + Drag | Pan canvas view |
- | Mouse Wheel | Zoom view in/out |
- | Brush Controls | Use sliders to control brush Size, Strength, and Hardness |
- | Clear Mask | Remove the entire mask |
- | Exit Mode | Click the "Draw Mask" button again |
-
- `;
+ const [standardShortcuts, maskShortcuts, systemClipboardTooltip, clipspaceClipboardTooltip] = await Promise.all([
+ loadTemplate('./templates/standard_shortcuts.html', import.meta.url),
+ loadTemplate('./templates/mask_shortcuts.html', import.meta.url),
+ loadTemplate('./templates/system_clipboard_tooltip.html', import.meta.url),
+ loadTemplate('./templates/clipspace_clipboard_tooltip.html', import.meta.url)
+ ]);
document.body.appendChild(helpTooltip);
@@ -617,45 +168,7 @@ async function createCanvasWidget(node, widget, app) {
},
onmouseenter: (e) => {
const currentPreference = canvas.canvasLayers.clipboardPreference;
- let tooltipContent = '';
-
- if (currentPreference === 'system') {
- tooltipContent = `
- 📋 System Clipboard Mode
-
- | Ctrl + C | Copy selected layers to internal clipboard + system clipboard as flattened image |
- | Ctrl + V | Priority: |
- | 1️⃣ Internal clipboard (copied layers) |
- | 2️⃣ System clipboard (images, screenshots) |
- | 3️⃣ System clipboard (file paths, URLs) |
- | Paste Image | Same as Ctrl+V but respects fit_on_add setting |
- | Drag & Drop | Load images directly from files |
-
-
- ⚠️ Security Note: "Paste Image" button for external images may not work due to browser security restrictions. Use Ctrl+V instead or Drag & Drop.
-
-
- 💡 Best for: Working with screenshots, copied images, file paths, and urls.
-
- `;
- } else {
- tooltipContent = `
- 📋 ComfyUI Clipspace Mode
-
- | Ctrl + C | Copy selected layers to internal clipboard + ComfyUI Clipspace as flattened image |
- | Ctrl + V | Priority: |
- | 1️⃣ Internal clipboard (copied layers) |
- | 2️⃣ ComfyUI Clipspace (workflow images) |
- | 3️⃣ System clipboard (fallback) |
- | Paste Image | Same as Ctrl+V but respects fit_on_add setting |
- | Drag & Drop | Load images directly from files |
-
-
- 💡 Best for: ComfyUI workflow integration and node-to-node image transfer
-
- `;
- }
-
+ const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip;
showTooltip(e.target, tooltipContent);
},
onmouseleave: hideTooltip
@@ -1122,8 +635,6 @@ async function createCanvasWidget(node, widget, app) {
const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer);
- node.size = [500, 500];
-
const openEditorBtn = controlPanel.querySelector(`#open-editor-btn-${node.id}`);
let backdrop = null;
let modalContent = null;
@@ -1224,6 +735,7 @@ app.registerExtension({
name: "Comfy.CanvasNode",
init() {
+ addStylesheet(getUrl('./css/canvas_view.css', import.meta.url));
const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = async function (number, prompt) {
@@ -1270,9 +782,8 @@ app.registerExtension({
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
log.debug("CanvasNode onNodeCreated: Base widget setup.");
-
const r = onNodeCreated?.apply(this, arguments);
-
+ this.size = [1150, 1000];
return r;
};
@@ -1301,6 +812,11 @@ app.registerExtension({
const canvasWidget = await createCanvasWidget(this, null, app);
canvasNodeInstances.set(this.id, canvasWidget);
log.info(`Registered CanvasNode instance for ID: ${this.id}`);
+
+ // Use a timeout to ensure the DOM has updated before we redraw.
+ setTimeout(() => {
+ this.setDirtyCanvas(true, true);
+ }, 100);
};
const onRemoved = nodeType.prototype.onRemoved;
diff --git a/js/css/canvas_view.css b/js/css/canvas_view.css
new file mode 100644
index 0000000..2ad41f1
--- /dev/null
+++ b/js/css/canvas_view.css
@@ -0,0 +1,393 @@
+.painter-button {
+ background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
+ border: 1px solid #2a2a2a;
+ border-radius: 4px;
+ color: #ffffff;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 80px;
+ text-align: center;
+ margin: 2px;
+ text-shadow: 0 1px 1px rgba(0,0,0,0.2);
+}
+
+.painter-button:hover {
+ background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+
+.painter-button:active {
+ background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
+ transform: translateY(1px);
+}
+
+.painter-button:disabled,
+.painter-button:disabled:hover {
+ background: #555;
+ color: #888;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ border-color: #444;
+}
+
+.painter-button.primary {
+ background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
+ border-color: #2a4cb4;
+}
+
+.painter-button.primary:hover {
+ background: linear-gradient(to bottom, #5a7ce4, #4a6cd4);
+}
+
+.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;
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.painter-slider-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #fff;
+ font-size: 12px;
+}
+
+.painter-slider-container input[type="range"] {
+ width: 80px;
+}
+
+
+.painter-button-group {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background-color: rgba(0,0,0,0.2);
+ padding: 4px;
+ 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;
+}
+
+.painter-clipboard-group .painter-button {
+ margin: 1px;
+}
+
+.painter-separator {
+ width: 1px;
+ height: 28px;
+ background-color: #2a2a2a;
+ margin: 0 8px;
+}
+
+.painter-container {
+ background: #607080; /* 带蓝色的灰色背景 */
+ border: 1px solid #4a5a6a;
+ border-radius: 6px;
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
+ transition: border-color 0.3s ease; /* Dodano dla płynnej zmiany ramki */
+}
+
+.painter-container.drag-over {
+ border-color: #00ff00; /* Zielona ramka podczas przeciągania */
+ border-style: dashed;
+}
+
+.painter-dialog {
+ background: #404040;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ padding: 20px;
+ color: #ffffff;
+}
+
+.painter-dialog input {
+ background: #303030;
+ border: 1px solid #505050;
+ border-radius: 4px;
+ color: #ffffff;
+ padding: 4px 8px;
+ margin: 4px;
+ width: 80px;
+}
+
+.painter-dialog button {
+ background: #505050;
+ border: 1px solid #606060;
+ border-radius: 4px;
+ color: #ffffff;
+ padding: 4px 12px;
+ margin: 4px;
+ cursor: pointer;
+}
+
+.painter-dialog button:hover {
+ background: #606060;
+}
+
+.blend-opacity-slider {
+ width: 100%;
+ margin: 5px 0;
+ display: none;
+}
+
+.blend-mode-active .blend-opacity-slider {
+ display: block;
+}
+
+.blend-mode-item {
+ padding: 5px;
+ cursor: pointer;
+ position: relative;
+}
+
+.blend-mode-item.active {
+ background-color: rgba(0,0,0,0.1);
+}
+
+.blend-mode-item.active {
+ background-color: rgba(0,0,0,0.1);
+}
+
+.painter-tooltip {
+ position: fixed;
+ display: none;
+ background: #3a3a3a;
+ color: #f0f0f0;
+ border: 1px solid #555;
+ border-radius: 8px;
+ padding: 12px 18px;
+ z-index: 9999;
+ font-size: 13px;
+ line-height: 1.7;
+ width: auto;
+ max-width: min(500px, calc(100vw - 40px));
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ pointer-events: none;
+ transform-origin: top left;
+ transition: transform 0.2s ease;
+ will-change: transform;
+}
+
+.painter-tooltip.scale-down {
+ transform: scale(0.9);
+ transform-origin: top;
+}
+
+.painter-tooltip.scale-down-more {
+ transform: scale(0.8);
+ transform-origin: top;
+}
+
+.painter-tooltip table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 8px 0;
+}
+
+.painter-tooltip table td {
+ padding: 2px 8px;
+ vertical-align: middle;
+}
+
+.painter-tooltip table td:first-child {
+ width: auto;
+ white-space: nowrap;
+ min-width: fit-content;
+}
+
+.painter-tooltip table td:last-child {
+ width: auto;
+}
+
+.painter-tooltip table tr:nth-child(odd) td {
+ background-color: rgba(0,0,0,0.1);
+}
+
+@media (max-width: 600px) {
+ .painter-tooltip {
+ font-size: 11px;
+ padding: 8px 12px;
+ }
+ .painter-tooltip table td {
+ padding: 2px 4px;
+ }
+ .painter-tooltip kbd {
+ padding: 1px 4px;
+ font-size: 10px;
+ }
+ .painter-tooltip table td:first-child {
+ width: 40%;
+ }
+ .painter-tooltip table td:last-child {
+ width: 60%;
+ }
+ .painter-tooltip h4 {
+ font-size: 12px;
+ margin-top: 8px;
+ margin-bottom: 4px;
+ }
+}
+
+@media (max-width: 400px) {
+ .painter-tooltip {
+ font-size: 10px;
+ padding: 6px 8px;
+ }
+ .painter-tooltip table td {
+ padding: 1px 3px;
+ }
+ .painter-tooltip kbd {
+ padding: 0px 3px;
+ font-size: 9px;
+ }
+ .painter-tooltip table td:first-child {
+ width: 35%;
+ }
+ .painter-tooltip table td:last-child {
+ width: 65%;
+ }
+ .painter-tooltip h4 {
+ font-size: 11px;
+ margin-top: 6px;
+ margin-bottom: 3px;
+ }
+}
+
+.painter-tooltip::-webkit-scrollbar {
+ width: 8px;
+}
+
+.painter-tooltip::-webkit-scrollbar-track {
+ background: #2a2a2a;
+ border-radius: 4px;
+}
+
+.painter-tooltip::-webkit-scrollbar-thumb {
+ background: #555;
+ border-radius: 4px;
+}
+
+.painter-tooltip::-webkit-scrollbar-thumb:hover {
+ background: #666;
+}
+
+.painter-tooltip h4 {
+ margin-top: 10px;
+ margin-bottom: 5px;
+ color: #4a90e2; /* Jasnoniebieski akcent */
+ border-bottom: 1px solid #555;
+ padding-bottom: 4px;
+}
+
+.painter-tooltip ul {
+ list-style: none;
+ padding-left: 10px;
+ margin: 0;
+}
+
+.painter-tooltip kbd {
+ background-color: #2a2a2a;
+ border: 1px solid #1a1a1a;
+ border-radius: 3px;
+ padding: 2px 6px;
+ font-family: monospace;
+ font-size: 12px;
+ color: #d0d0d0;
+}
+
+.painter-container.has-focus {
+ /* Używamy box-shadow, aby stworzyć efekt zewnętrznej ramki,
+ która nie wpłynie na rozmiar ani pozycję elementu. */
+ box-shadow: 0 0 0 2px white;
+ /* Możesz też zmienić kolor istniejącej ramki, ale box-shadow jest bardziej wyrazisty */
+ /* border-color: white; */
+}
+
+.painter-button.matting-button {
+ position: relative;
+ transition: all 0.3s ease;
+}
+
+.painter-button.matting-button.loading {
+ padding-right: 36px; /* Make space for spinner */
+ cursor: wait;
+}
+
+.painter-button.matting-button .matting-spinner {
+ display: none;
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 16px;
+ height: 16px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: #fff;
+ animation: matting-spin 1s linear infinite;
+}
+
+.painter-button.matting-button.loading .matting-spinner {
+ display: block;
+}
+
+@keyframes matting-spin {
+ to {
+ transform: translateY(-50%) rotate(360deg);
+ }
+}
+.painter-modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.8);
+ z-index: 111;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.painter-modal-content {
+ width: 90vw;
+ height: 90vh;
+ background-color: #353535;
+ border: 1px solid #222;
+ border-radius: 8px;
+ box-shadow: 0 5px 25px rgba(0,0,0,0.5);
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
diff --git a/js/templates/clipspace_clipboard_tooltip.html b/js/templates/clipspace_clipboard_tooltip.html
new file mode 100644
index 0000000..c46fbbc
--- /dev/null
+++ b/js/templates/clipspace_clipboard_tooltip.html
@@ -0,0 +1,13 @@
+📋 ComfyUI Clipspace Mode
+
+ | Ctrl + C | Copy selected layers to internal clipboard + ComfyUI Clipspace as flattened image |
+ | Ctrl + V | Priority: |
+ | 1️⃣ Internal clipboard (copied layers) |
+ | 2️⃣ ComfyUI Clipspace (workflow images) |
+ | 3️⃣ System clipboard (fallback) |
+ | Paste Image | Same as Ctrl+V but respects fit_on_add setting |
+ | Drag & Drop | Load images directly from files |
+
+
+ 💡 Best for: ComfyUI workflow integration and node-to-node image transfer
+
diff --git a/js/templates/mask_shortcuts.html b/js/templates/mask_shortcuts.html
new file mode 100644
index 0000000..69721cc
--- /dev/null
+++ b/js/templates/mask_shortcuts.html
@@ -0,0 +1,9 @@
+Mask Mode
+
+ | Click + Drag | Paint on the mask |
+ | Middle Mouse Button + Drag | Pan canvas view |
+ | Mouse Wheel | Zoom view in/out |
+ | Brush Controls | Use sliders to control brush Size, Strength, and Hardness |
+ | Clear Mask | Remove the entire mask |
+ | Exit Mode | Click the "Draw Mask" button again |
+
diff --git a/js/templates/standard_shortcuts.html b/js/templates/standard_shortcuts.html
new file mode 100644
index 0000000..ded155e
--- /dev/null
+++ b/js/templates/standard_shortcuts.html
@@ -0,0 +1,40 @@
+Canvas Control
+
+ | Click + Drag | Pan canvas view |
+ | Mouse Wheel | Zoom view in/out |
+ | Shift + Click (background) | Start resizing canvas area |
+ | Shift + Ctrl + Click | Start moving entire canvas |
+ | Single Click (background) | Deselect all layers |
+
+
+Clipboard & I/O
+
+ | Ctrl + C | Copy selected layer(s) |
+ | Ctrl + V | Paste from clipboard (image or internal layers) |
+ | Drag & Drop Image File | Add image as a new layer |
+
+
+Layer Interaction
+
+ | Click + Drag | Move selected layer(s) |
+ | Ctrl + Click | Add/Remove layer from selection |
+ | Alt + Drag | Clone selected layer(s) |
+ | Right Click | Show blend mode & opacity menu |
+ | Mouse Wheel | Scale layer (snaps to grid) |
+ | Ctrl + Mouse Wheel | Fine-scale layer |
+ | Shift + Mouse Wheel | Rotate layer by 5° steps |
+ | Shift + Ctrl + Mouse Wheel | Snap rotation to 5° increments |
+ | Arrow Keys | Nudge layer by 1px |
+ | Shift + Arrow Keys | Nudge layer by 10px |
+ | [ or ] | Rotate by 1° |
+ | Shift + [ or ] | Rotate by 10° |
+ | Delete | Delete selected layer(s) |
+
+
+Transform Handles (on selected layer)
+
+ | Drag Corner/Side | Resize layer |
+ | Drag Rotation Handle | Rotate layer |
+ | Hold Shift | Keep aspect ratio / Snap rotation to 15° |
+ | Hold Ctrl | Snap to grid |
+
diff --git a/js/templates/system_clipboard_tooltip.html b/js/templates/system_clipboard_tooltip.html
new file mode 100644
index 0000000..b3be8eb
--- /dev/null
+++ b/js/templates/system_clipboard_tooltip.html
@@ -0,0 +1,16 @@
+📋 System Clipboard Mode
+
+ | Ctrl + C | Copy selected layers to internal clipboard + system clipboard as flattened image |
+ | Ctrl + V | Priority: |
+ | 1️⃣ Internal clipboard (copied layers) |
+ | 2️⃣ System clipboard (images, screenshots) |
+ | 3️⃣ System clipboard (file paths, URLs) |
+ | Paste Image | Same as Ctrl+V but respects fit_on_add setting |
+ | Drag & Drop | Load images directly from files |
+
+
+ ⚠️ Security Note: "Paste Image" button for external images may not work due to browser security restrictions. Use Ctrl+V instead or Drag & Drop.
+
+
+ 💡 Best for: Working with screenshots, copied images, file paths, and urls.
+
diff --git a/js/utils/ResourceManager.js b/js/utils/ResourceManager.js
new file mode 100644
index 0000000..b7313cd
--- /dev/null
+++ b/js/utils/ResourceManager.js
@@ -0,0 +1,30 @@
+import {$el} from "../../../scripts/ui.js";
+
+export function addStylesheet(url) {
+ if (url.endsWith(".js")) {
+ url = url.substr(0, url.length - 2) + "css";
+ }
+ $el("link", {
+ parent: document.head,
+ rel: "stylesheet",
+ type: "text/css",
+ href: url.startsWith("http") ? url : getUrl(url),
+ });
+}
+
+export function getUrl(path, baseUrl) {
+ if (baseUrl) {
+ return new URL(path, baseUrl).toString();
+ } else {
+ return new URL("../" + path, import.meta.url).toString();
+ }
+}
+
+export async function loadTemplate(path, baseUrl) {
+ const url = getUrl(path, baseUrl);
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to load template: ${url}`);
+ }
+ return await response.text();
+}