diff --git a/README.md b/README.md index 18869a8..0685877 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ Flow is currently in the early stages of development, so expect bugs and ongoing + + @@ -104,7 +106,8 @@ http://127.0.0.1:8188/flow - Enhanced Customization Options ### Feature Support -- [ ] Inpainting Functionality +- [x] Canvas / Masking / Inpainting Functionality +- - [ ] Improved Canvas / Masking / Inpainting Functionality - [ ] Enhanced Media Handling - [x] Live Preview - [ ] Prompt Tracking diff --git a/__init__.py b/__init__.py index c901f33..4a27190 100644 --- a/__init__.py +++ b/__init__.py @@ -38,7 +38,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {} APP_CONFIGS: List[AppConfig] = [] APP_NAME: str = "Flow" -APP_VERSION: str = "0.2.3" +APP_VERSION: str = "0.3.0" PURPLE = "\033[38;5;129m" RESET = "\033[0m" FLOWMSG = f"{PURPLE}Flow{RESET}" diff --git a/pyproject.toml b/pyproject.toml index 62e64ba..5fe447c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-disty-flow" description = "Flow is a custom node designed to provide a more user-friendly interface for ComfyUI by acting as an alternative user interface for running workflows. It is not a replacement for workflow creation.\nFlow is currently in the early stages of development, so expect bugs and ongoing feature enhancements. With your support and feedback, Flow will settle into a steady stream." -version = "0.2.3" +version = "0.3.0" license = {file = "LICENSE"} [project.urls] diff --git a/web/core/content.html b/web/core/content.html index 5aec955..2b0c433 100644 --- a/web/core/content.html +++ b/web/core/content.html @@ -1,13 +1,17 @@
-
+
+
+
diff --git a/web/core/css/main.css b/web/core/css/main.css index cc04068..bfbb339 100644 --- a/web/core/css/main.css +++ b/web/core/css/main.css @@ -174,7 +174,7 @@ header .right { display: flex; flex: 1; text-align: left; - overflow-y: auto; + overflow: hidden; } .content div { @@ -194,6 +194,10 @@ header .right { flex: 1.1; background-color: var(--color-background); } +.content .right-col { + flex: 0.5; + background-color: var(--color-background); +} .content .mid-col { flex: 2.5; @@ -201,11 +205,64 @@ header .right { border-right: 1px dashed var(--color-border); justify-content: space-around; min-height: 100%; + padding: 10px; + /* padding: 4px 6px; */ + } -.content .right-col { - flex: 0.5; - background-color: var(--color-background); +.canvas-container { + display: flex; + justify-content: center; + align-items: center; + /* height: 100% !important; */ + /* width: 100% !important; */ + /* background: var(--color-button-primary-active); */ +} + +#canvasContainer:focus { + border: 2px solid #4A90E2; +} + +#imageCanvas { + /* width: 100%; */ + /* height: 100%; */ + display: block; + /* position: absolute; */ + /* top: 0; */ + /* left: 0; */ + /* background: green; */ + overflow: hidden; + padding: 0; + +} + +/* Green dashed border around the canvas wrapper */ +#canvasWrapper { + /* position: relative; */ + flex: 1; + /* width: 100%; */ + /* height: 100%; */ + overflow: hidden; + /* background-color: var(--color-background-secondary); */ + border: 1px dashed var(--color-border); + box-sizing: border-box; + /* width: 800px; */ + /* height: 600px; */ + position: relative; + margin: 0; /* Remove default margins */ + padding: 10px; +} + +.brush-ui-container { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + background-color: var(--color-button-primary-text-hover); + color: var(--color-primary-text); + border: 1px solid var(--color-border); + border-radius: 5px; + margin-bottom: 10px; } #control button { @@ -676,7 +733,7 @@ select option:hover { height: 100%; position: relative; object-fit: contain; - border: 2px dashed var(--color-border); + border: 1px dashed var(--color-border); border-radius: 5px; } #batch-images-container { @@ -1546,4 +1603,356 @@ html:not(.css-loading) body { .multi-component-container { background: var(--color-background-secondary); margin-bottom: 4px; -} \ No newline at end of file +} +.plugin-ui-container { + background: var(--color-background-secondary); + padding: 4px; + /* position: absolute; */ + /* top: 100px; */ + /* left: 19%; */ + z-index: 1000; + transition: width 0.3s, height 0.3s; + display: flex; + flex-direction: row; + /* justify-content: space-between; */ + align-items: stretch; + /* border: 1px dashed var(--color-border); */ + margin: 5px; +} +/* Style for the plugin UI container */ +#pluginUIContainer { + /* padding: 10px; */ + /* background-color: var(--color-background-secondary); */ + /* Adjust as needed */ +} + +/* Parent container for the canvas */ +/* #canvasContainer { + position: relative; + width: 100%; + height: calc(100% - 60px); + border: 1px solid #ccc; + box-sizing: border-box; +} */ + +/* Make the canvas fill its parent container */ +/* #imageCanvas { + width: 100% !important; + height: 100% !important; + display: block; + background: var(--color-progress-background); + +} */ + + /* CSS Isolation: Prefix all classes with 'cbp-' */ + .cbp-brush-ui-container { + background-color: var(--color-background); + user-select: none; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + font-family: Arial, sans-serif; + font-size: 14px; + color: var(--color-text); + position: absolute; + top: 100px; + left: 19%; + z-index: 1000; + transition: width 0.3s, height 0.3s; + display: flex; + flex-direction: column; + align-items: stretch; + + border: 1px dashed var(--color-border); + } + .cbp-brush-ui-header { + cursor: move; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background-color: var(--color-header-background); + border-bottom: 1px dashed var(--color-border); + border-top-left-radius: 8px; + border-top-right-radius: 8px; + } + .cbp-brush-ui-title { + font-weight: bold; + } + .cbp-brush-ui-minimize-btn { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + width: 24px; + height: 24px; + padding: 0; + color: var(--color-text); + display: none; + } + .cbp-brush-ui-content { + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + box-sizing: border-box; + } + .cbp-brush-ui-content label { + display: flex; + flex-direction: column; + font-size: 12px; + gap: 4px; + } + .cbp-brush-ui-content input[type="range"], + .cbp-brush-ui-content input[type="number"], + .cbp-brush-ui-content input[type="color"], + .cbp-brush-ui-content button { + width: 100%; + height: 30px; + box-sizing: border-box; + padding: 4px; + /* border: 1px solid #ccc; */ + /* border-radius: 4px; */ + font-size: 14px; + } + + .cbp-brush-ui-toggle-btn { + flex: 2; + display: flex; + justify-content: center; + align-items: center; + background: var(--color-button-primary); + border: none; + cursor: pointer; + padding: 4px !important; + height: 40px !important; + width: auto; + } + #cbp-color-picker { + flex: 1; + height: auto; + + } + #cbp-color-picker:hover { + border: 1px dashed var(--color-button-primary-text); + } + #cbp-toggle-btn-icon { + width: 70%; + height: 100%; + /* display: block; Ensures the SVG takes up space */ + } + #cbp-toggle-drawing-mode-btn.active { + /* background: var(--color-button-primary-text); */ + border: 1px dashed var(--color-button-primary-text) !important; + } + #ui-toggle-btn.active { + background: var(--color-button-primary-text); + /* border: 1px dashed var(--color-button-secondary-text-active) !important; */ + + } + .cbp-brush-ui-color-picker { + flex: 1; + display: flex; + flex-direction: row; + gap: 10px; + + width: 100%; + height: 100%; + } + + .cbp-brush-ui-content input[type="color"] { + padding: 0; + /* border: none; */ + background-color: var(--color-background); + outline: none; + -webkit-appearance: none; /* Removes default styling in WebKit browsers */ + border-bottom: 1px dashed var(--color-border); + + } + + /* Remove the color swatch border in WebKit browsers */ + .cbp-brush-ui-content input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + border: none; + } + + .cbp-brush-ui-content input[type="color"]::-webkit-color-swatch { + border: none; + } + + /* Remove the color swatch border in Firefox */ + .cbp-brush-ui-content input[type="color"]::-moz-color-swatch { + border: none; + } + .cbp-brush-ui-minimized .cbp-brush-ui-content { + display: none; + } + .cbp-brush-ui-full { + width: 155px; + height: auto; + } + .cbp-brush-ui-mini { + width: 80px; + height: auto; + } + .cbp-brush-ui-minimized { + width: 60px; + height: auto; + } + .cbp-brush-ui-input-group { + display: flex; + align-items: center; + gap: 10px; + } + .cbp-brush-ui-input-group input { + flex: 1; + } + .cbp-hidden-cursor { + cursor: none !important; + } + + + + + .mbp-container * { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } + + .mbp-container { + /* --mbp-primary: var(--color-button-primary); + --mbp-primary-hover: var(--color-button-primary-hover); + --mbp-bg: var(--color-background-secondary); + --mbp-border: var(--color-border); + --mbp-text: var(--color-primary-text); + --mbp-icon: var(--color-primary); */ + padding: 0.5rem; + } + + .mbp-header { + display: flex; + gap: 0.5rem; + margin-bottom: 0.75rem; + } + + .mbp-header .mbp-select { + flex: 1; + } + + .mbp-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + border: 1px solid var(--mbp-border); + background: var(--color-button-primary) ; + /* border-radius: 0.375rem; */ + cursor: pointer; + transition: all 0.2s; + color: var(--mbp-icon); + min-width: 36px; + height: 36px; + width: 100%; + } + + .mbp-button:hover { + background: var(--color-button-primary-hover); + color: var(--mbp-primary); + } + + .mbp-button svg { + width: 1.25rem; + height: 1.25rem; + } + + .mbp-select { + padding: 0.5rem; + border: none; + background: var(--color-background); + color: var(--mbp-text); + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.5rem center; + background-size: 1.25rem; + padding-right: 2.5rem; + height: 36px; + outline: none; + } + .mbp-select:hover { + /* background: var(--color-button-primary-hover); */ + border-top: solid 1px var(--color-button-primary-hover); + } + + .mbp-toggle-wrapper { + display: flex; + align-items: center; + gap: 0.75rem; + margin: 0.75rem 0; + padding: 0.25rem 0; + display: none !important; + + } + + .mbp-toggle { + position: relative; + display: inline-block; + width: 3.5rem; + height: 1.75rem; + margin: 0; + } + + .mbp-toggle input { + opacity: 0; + width: 0; + height: 0; + margin: 0; + } + + .mbp-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + /* background-color: #e5e7eb; */ + transition: .3s; + border-radius: 2rem; + } + + .mbp-toggle-slider:before { + position: absolute; + content: ""; + height: 1.25rem; + width: 1.25rem; + left: 0.25rem; + bottom: 0.25rem; + /* background-color: white; */ + transition: .3s; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .mbp-toggle input:checked + .mbp-toggle-slider { + background-color: var(--mbp-primary); + } + + .mbp-toggle input:checked + .mbp-toggle-slider:before { + transform: translateX(1.75rem); + } + + .mbp-label { + font-size: 0.75rem; + color: var(--mbp-text); + user-select: none; + padding: 0 6px; + } + + .mbp-button-group { + display: flex; + gap: 0.5rem; + margin: 0.75rem 0; + + } \ No newline at end of file diff --git a/web/core/js/common/components/CanvasComponent.js b/web/core/js/common/components/CanvasComponent.js new file mode 100644 index 0000000..8fb3fbb --- /dev/null +++ b/web/core/js/common/components/CanvasComponent.js @@ -0,0 +1,147 @@ +import { showSpinner, hideSpinner } from './utils.js'; +import { updateWorkflow } from './workflowManager.js'; + +function dataURLToBlob(dataURL) { + const [header, data] = dataURL.split(','); + const mimeMatch = header.match(/:(.*?);/); + if (!mimeMatch) { + throw new Error('Invalid data URL.'); + } + const mime = mimeMatch[1]; + const binary = atob(data); + const array = []; + for (let i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + return new Blob([new Uint8Array(array)], { type: mime }); +} + +async function processAndUpload(items, getImage, uploadDescription, defaultErrorMessage, successMessagePrefix, workflow) { + for (const item of items) { + const { id, label, nodePath } = item; + + try { + showSpinner(); + + const imageDataURL = getImage(); + // console.log(`${uploadDescription} Data URL for ${id}:`, imageDataURL); + if (!imageDataURL) { + throw new Error(`${uploadDescription} data is unavailable.`); + } + + const blob = dataURLToBlob(imageDataURL); + + const formData = new FormData(); + formData.append('image', blob, `${id}.png`); + + const response = await fetch('/upload/image', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + let errorMessage = defaultErrorMessage; + try { + const errorResponse = await response.json(); + errorMessage = errorResponse.message || errorMessage; + } catch (e) { + console.error('Failed to parse error response:', e); + } + throw new Error(errorMessage); + } + + let result; + try { + result = await response.json(); + console.log(`Server Response for ${id}:`, result); + } catch (e) { + throw new Error('Invalid JSON response from server.'); + } + + console.log(`${successMessagePrefix} ${id} uploaded successfully:`, result); + + if (result && result.name) { + updateWorkflow(workflow, nodePath, result.name); + console.log(`Workflow updated at nodePath ${nodePath} with imageUrl: ${result.name}`); + } else { + throw new Error('Server response did not include imageUrl or imageName.'); + } + } catch (error) { + console.error(`Error uploading ${uploadDescription.toLowerCase()} ${id}:`, error); + alert(`Error uploading ${label}: ${error.message}`); + } finally { + hideSpinner(); + } + } +} + +export default async function CanvasComponent(flowConfig, workflow, canvasLoader) { + if (flowConfig.canvasOutputs && Array.isArray(flowConfig.canvasOutputs)) { + await processAndUpload( + flowConfig.canvasOutputs, + () => canvasLoader.getCanvasOutImage(), + 'Canvas Output Image', + 'Canvas upload failed.', + 'Canvas', + workflow + ); + } + + // **Process canvasLoadedImages and canvasSelectedMaskOutputs (Case 1)** + if ( + flowConfig.canvasLoadedImages && Array.isArray(flowConfig.canvasLoadedImages) && + flowConfig.canvasSelectedMaskOutputs && Array.isArray(flowConfig.canvasSelectedMaskOutputs) + ) { + await processAndUpload( + flowConfig.canvasLoadedImages, + () => canvasLoader.getOriginalImage(), + 'Original Image', + 'Original image upload failed.', + 'Original Image', + workflow + ); + + await processAndUpload( + flowConfig.canvasSelectedMaskOutputs, + () => canvasLoader.getSelectedMaskAlphaOnImage(), + 'Selected Mask Alpha Image', + 'Selected mask alpha image upload failed.', + 'Selected Mask Alpha Image', + workflow + ); + } + + if (flowConfig.canvasMaskOutputs && Array.isArray(flowConfig.canvasMaskOutputs)) { + await processAndUpload( + flowConfig.canvasMaskOutputs, + () => canvasLoader.getMaskImage(), + 'Mask Image', + 'Mask image upload failed.', + 'Mask Image', + workflow + ); + } + + if (flowConfig.canvasAlphaOutputs && Array.isArray(flowConfig.canvasAlphaOutputs)) { + await processAndUpload( + flowConfig.canvasAlphaOutputs, + () => canvasLoader.getMaskAlphaOnImage(), + 'Mask Alpha Image', + 'Mask alpha image upload failed.', + 'Mask Alpha Image', + workflow + ); + } + + // **Process canvasSelectedMaskOutputs (New Functionality for Case 1)** + if (flowConfig.canvasSelectedMaskOutputs && Array.isArray(flowConfig.canvasSelectedMaskOutputs)) { + await processAndUpload( + flowConfig.canvasSelectedMaskOutputs, + () => canvasLoader.getSelectedMaskAlphaOnImage(), + 'Selected Mask Alpha Image', + 'Selected mask alpha image upload failed.', + 'Selected Mask Alpha Image', + workflow + ); + } +} diff --git a/web/core/js/common/components/canvas/CanvasControlsPlugin.js b/web/core/js/common/components/canvas/CanvasControlsPlugin.js new file mode 100644 index 0000000..831ac8c --- /dev/null +++ b/web/core/js/common/components/canvas/CanvasControlsPlugin.js @@ -0,0 +1,240 @@ +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class CanvasControlsPlugin extends CanvasPlugin { + constructor(options = {}) { + super('CanvasControlsPlugin'); + + this.zoomIn = this.zoomIn.bind(this); + this.zoomOut = this.zoomOut.bind(this); + this.resetZoom = this.resetZoom.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onAfterRender = this.onAfterRender.bind(this); + this.arraysEqual = this.arraysEqual.bind(this); + + this.canvasManager = null; + this.canvas = null; + this.isPanning = false; + this.lastPosX = 0; + this.lastPosY = 0; + this.lastTransform = null; + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.createUI(); + + this.attachEventListeners(); + + this.lastTransform = this.canvas.viewportTransform.slice(); + } + + createUI() { + const styleSheet = document.createElement('style'); + styleSheet.textContent = ` + .cc-container * { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } + + .cc-container { + display: inline-flex; + gap: 0.5rem; + padding: 0.5rem; + /* background: var(--color-background); */ + /* border: 1px dashed var(--color-border); */ + user-select: none; + } + + .cc-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: var(--color-button-primary); + border: none; + cursor: pointer; + transition: all 0.2s; + color: var(--color-primary-text); + min-width: 36px; + height: 36px; + } + + .cc-button:hover { + background: var(--color-button-primary-hover); + } + + .cc-button svg { + width: 1.25rem; + height: 1.25rem; + } + + .cc-button[data-active="true"] { + border: 1px dashed var(--color-button-secondary-text-active); + } + + .cc-divider { + width: 1px; + border-left: 1px dashed var(--color-border); + margin: 0 0.25rem; + display: none; + } + #panBtn { + display: none; + } + `; + document.head.appendChild(styleSheet); + + const temp = document.createElement('div'); + temp.innerHTML = ` +
+ + + + +
+ + + +
+ + +
+ `; + + this.uiContainer = temp.firstElementChild; + this.zoomInBtn = this.uiContainer.querySelector('#zoomInBtn'); + this.zoomOutBtn = this.uiContainer.querySelector('#zoomOutBtn'); + this.resetZoomBtn = this.uiContainer.querySelector('#resetZoomBtn'); + this.panBtn = this.uiContainer.querySelector('#panBtn'); + + const pluginUIContainer = document.getElementById('pluginUIContainer'); + if (pluginUIContainer) { + pluginUIContainer.appendChild(this.uiContainer); + } else { + console.warn('Element with id "pluginUIContainer" not found.'); + } + + this.updatePanButtonState = (isPanning) => { + this.panBtn.dataset.active = isPanning; + }; + } + + attachEventListeners() { + this.zoomInBtn.addEventListener('click', this.zoomIn); + this.zoomOutBtn.addEventListener('click', this.zoomOut); + this.resetZoomBtn.addEventListener('click', this.resetZoom); + + this.canvas.on('mouse:down', this.onMouseDown); + this.canvas.on('mouse:move', this.onMouseMove); + this.canvas.on('mouse:up', this.onMouseUp); + this.canvas.on('mouse:out', this.onMouseUp); + + this.canvas.on('after:render', this.onAfterRender); + } + + detachEventListeners() { + this.zoomInBtn.removeEventListener('click', this.zoomIn); + this.zoomOutBtn.removeEventListener('click', this.zoomOut); + this.resetZoomBtn.removeEventListener('click', this.resetZoom); + + this.canvas.off('mouse:down', this.onMouseDown); + this.canvas.off('mouse:move', this.onMouseMove); + this.canvas.off('mouse:up', this.onMouseUp); + this.canvas.off('mouse:out', this.onMouseUp); + + this.canvas.off('after:render', this.onAfterRender); + } + + arraysEqual(a, b) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + } + + onAfterRender() { + const currentTransform = this.canvas.viewportTransform; + if (!this.arraysEqual(currentTransform, this.lastTransform)) { + this.lastTransform = currentTransform.slice(); + this.canvasManager.emit('viewport:changed', { + transform: this.canvas.viewportTransform + }); + } + } + + zoomIn() { + let zoom = this.canvas.getZoom(); + zoom *= 1.1; + if (zoom > 20) zoom = 20; + this.canvas.zoomToPoint({ x: this.canvas.width / 2, y: this.canvas.height / 2 }, zoom); + } + + zoomOut() { + let zoom = this.canvas.getZoom(); + zoom /= 1.1; + this.canvas.zoomToPoint({ x: this.canvas.width / 2, y: this.canvas.height / 2 }, zoom); + } + + resetZoom() { + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + } + + onMouseDown(opt) { + const evt = opt.e; + if (evt.altKey) { + this.isPanning = true; + this.lastPosX = evt.clientX; + this.lastPosY = evt.clientY; + this.canvas.setCursor('move'); + } + } + + onMouseMove(opt) { + if (this.isPanning) { + const e = opt.e; + const deltaX = e.clientX - this.lastPosX; + const deltaY = e.clientY - this.lastPosY; + const vpt = this.canvas.viewportTransform; + const zoom = this.canvas.getZoom(); + vpt[4] += deltaX / zoom; + vpt[5] += deltaY / zoom; + this.canvas.requestRenderAll(); + this.lastPosX = e.clientX; + this.lastPosY = e.clientY; + } + } + + onMouseUp(opt) { + this.isPanning = false; + this.canvas.setCursor('default'); + } + + destroy() { + if (this.uiContainer && this.uiContainer.parentNode) { + this.uiContainer.parentNode.removeChild(this.uiContainer); + } + + this.detachEventListeners(); + } +} \ No newline at end of file diff --git a/web/core/js/common/components/canvas/CanvasLoader.js b/web/core/js/common/components/canvas/CanvasLoader.js new file mode 100644 index 0000000..99ca1b6 --- /dev/null +++ b/web/core/js/common/components/canvas/CanvasLoader.js @@ -0,0 +1,467 @@ +import { CanvasManager } from './CanvasManager.js'; +import { ImageLoaderPlugin } from './ImageLoaderPlugin.js'; +import { ImageCompareSliderPlugin } from './ImageCompareSliderPlugin.js'; +import { MaskBrushPlugin } from './MaskBrushPlugin.js'; +import { CanvasControlsPlugin } from './CanvasControlsPlugin.js'; +import { CustomBrushPlugin } from './CustomBrushPlugin.js'; +import { UndoRedoPlugin } from './UndoRedoPlugin.js'; +import { ImageAdderPlugin } from './ImageAdderPlugin.js'; +import { CanvasScaleForSavePlugin } from './CanvasScaleForSavePlugin.js'; + +const loadFabric = () => { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = '/core/js/common/components/canvas/fabric.5.2.4.min.js'; + script.async = false; + script.onload = () => { + console.log('Fabric.js loaded successfully'); + resolve(); + }; + script.onerror = () => { + console.error('Failed to load Fabric.js'); + reject(new Error('Fabric.js failed to load')); + }; + document.head.appendChild(script); + }); +}; + +// Floating toolbar functionality - move +const styleSheet = document.createElement('style'); +styleSheet.textContent = ` + .plugin-ui-container { + position: relative; + display: flex; + gap: 0.5rem; + padding: 0.5rem; + padding-right: calc(0.5rem + 36px); + transition: background 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; + will-change: transform; + } + + .plugin-ui-container.floating { + position: fixed; + bottom: 25%; + /* left: 16%px; */ + z-index: 1000; + background: var(--color-background); + border: 1px dashed var(--color-border); + cursor: move; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .dock-toggle { + position: absolute; + right: 0; + top: 0; + height: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: var(--color-button-primary); + border: none; + border-left: 1px dashed var(--color-border); + cursor: pointer; + transition: all 0.2s; + color: var(--color-primary-text); + width: 36px; + } + + .plugin-ui-container.floating .dock-toggle { + position: relative; + height: 36px; + margin-left: 0.5rem; + border: none; + } + + .dock-toggle:hover { + background: var(--color-button-primary-hover); + } + + .dock-toggle svg { + width: 1.25rem; + height: 1.25rem; + transition: transform 0.2s ease; + } + + .dock-toggle:hover svg { + transform: scale(1.1); + } + + .plugin-ui-container.floating .dock-toggle:hover svg { + transform: scale(1.1); + } +`; +document.head.appendChild(styleSheet); + +function initializeFloating() { + const container = document.getElementById('pluginUIContainer'); + const dockButton = document.createElement('button'); + dockButton.className = 'dock-toggle'; + dockButton.title = 'Make toolbar floating'; + dockButton.innerHTML = ` + + + + `; + container.appendChild(dockButton); + + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + let originalPosition = null; + let lastFrameTime = 0; + const frameRate = 1000 / 120; + + function updateDockIcon(isFloating) { + dockButton.title = isFloating ? 'Dock toolbar' : 'Make toolbar floating'; + dockButton.innerHTML = isFloating + ? ` + + ` + : ` + + `; + } + + function updateDockButtonPosition(isFloating) { + if (isFloating) { + container.appendChild(dockButton); + } else { + container.insertBefore(dockButton, container.firstChild); + } + } + + function startDragging(e) { + if (container.classList.contains('floating') && e.target !== dockButton) { + isDragging = true; + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + container.style.transition = 'none'; + document.body.style.cursor = 'move'; + } + } + + function drag(e) { + if (isDragging) { + e.preventDefault(); + + const currentTime = performance.now(); + if (currentTime - lastFrameTime < frameRate) return; + + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + container.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; + + lastFrameTime = currentTime; + } + } + + function stopDragging() { + if (isDragging) { + isDragging = false; + container.style.transition = ''; + document.body.style.cursor = ''; + } + } + + function toggleFloating() { + const isFloating = container.classList.toggle('floating'); + updateDockIcon(isFloating); + updateDockButtonPosition(isFloating); + + if (isFloating) { + originalPosition = { + parent: container.parentNode, + nextSibling: container.nextSibling, + styles: { + position: container.style.position, + top: container.style.top, + left: container.style.left, + transform: container.style.transform, + zIndex: container.style.zIndex + } + }; + + container.addEventListener('mousedown', startDragging); + window.addEventListener('mousemove', drag); + window.addEventListener('mouseup', stopDragging); + window.addEventListener('mouseleave', stopDragging); + + container.style.transform = `translate3d(${xOffset}px, ${yOffset}px, 0)`; + } else { + container.removeEventListener('mousedown', startDragging); + window.removeEventListener('mousemove', drag); + window.removeEventListener('mouseup', stopDragging); + window.removeEventListener('mouseleave', stopDragging); + + container.style.transform = ''; + container.style.position = ''; + container.style.top = ''; + container.style.left = ''; + container.style.zIndex = ''; + xOffset = 0; + yOffset = 0; + } + } + + dockButton.addEventListener('click', toggleFloating); + + container.addEventListener('selectstart', (e) => { + if (isDragging) e.preventDefault(); + }); + + return function cleanup() { + container.removeEventListener('mousedown', startDragging); + window.removeEventListener('mousemove', drag); + window.removeEventListener('mouseup', stopDragging); + window.removeEventListener('mouseleave', stopDragging); + dockButton.removeEventListener('click', toggleFloating); + container.removeEventListener('selectstart', (e) => { + if (isDragging) e.preventDefault(); + }); + if (dockButton.parentNode) { + dockButton.parentNode.removeChild(dockButton); + } + }; +} +const cleanupFloating = initializeFloating(); +// Floating toolbar functionality + +export class CanvasLoader { + constructor(canvasId, flowConfig) { + this.canvasId = canvasId; + this.flowConfig = flowConfig; + this.isInitialized = false; + this.initPromise = this.init(); + } + + determineCanvasOptions(flowConfig) { + const options = { + canvasControls: true, + imageLoader: false, + undo: true, + maskBrush: false, + customBrush: false, + imageCompareSlider: false, + imageAdder: false, + canvasScaleForSave: false, + }; + + // **Case 1**: Load maskBrush if canvasLoadedImages and canvasSelectedMaskOutputs are present + if (flowConfig.canvasLoadedImages && Array.isArray(flowConfig.canvasLoadedImages) && flowConfig.canvasLoadedImages.length > 0 && + flowConfig.canvasSelectedMaskOutputs && Array.isArray(flowConfig.canvasSelectedMaskOutputs) && flowConfig.canvasSelectedMaskOutputs.length > 0) { + options.maskBrush = true; + options.imageLoader = true; + } + + // **Case 2**: Load customBrush if canvasOutputs are present + if (flowConfig.canvasOutputs && Array.isArray(flowConfig.canvasOutputs) && flowConfig.canvasOutputs.length > 0) { + options.customBrush = true; + options.imageLoader = true; + options.canvasScaleForSave = true; + + + } + + return options; + } + + async init() { + try { + this.options = this.determineCanvasOptions(this.flowConfig); + + + if (!this.options.maskBrush && !this.options.customBrush) { + console.warn("No relevant fields in flowConfig. Canvas will not be displayed or initialized."); + this.hideCanvasUI(); + return; + } + + await loadFabric(); + + const canvasWrapper = document.getElementById('canvasWrapper'); + const pluginUIContainer = document.getElementById('pluginUIContainer'); + + if (!canvasWrapper || !pluginUIContainer) { + throw new Error('Required DOM elements (canvasWrapper or pluginUIContainer) are missing.'); + } + + canvasWrapper.style.display = 'block'; + pluginUIContainer.style.display = 'block'; + + this.canvasManager = new CanvasManager({ canvasId: this.canvasId }); + + if (this.options.imageLoader) { + const imageLoaderPlugin = new ImageLoaderPlugin({ + mode: 'Single' // Set to 'Single' or 'Multi' as needed + }); + this.canvasManager.registerPlugin(imageLoaderPlugin); + } + + if (this.options.canvasControls) { + const canvasControlsPlugin = new CanvasControlsPlugin(); + this.canvasManager.registerPlugin(canvasControlsPlugin); + } + + if (this.options.undo) { + const undoRedoPlugin = new UndoRedoPlugin({ + maxHistory: 100, + }); + this.canvasManager.registerPlugin(undoRedoPlugin); + } + if (this.options.canvasScaleForSave) { + const canvasScaleForSave = new CanvasScaleForSavePlugin({ + defaultScale: 1, + showDownloadButton: true, + }); + this.canvasManager.registerPlugin(canvasScaleForSave); + } + if (this.options.maskBrush) { + const maskBrushPlugin = new MaskBrushPlugin({ + brushSize: 75, + brushOpacity: 0.5, + cursorOutlineType: 'dashed', + cursorPrimaryColor: '#000000', + cursorSecondaryColor: '#FFFFFF', + cursorLineWidth: 1, + cursorFill: true, + useBrushColorPrimaryColor: true, + brushColor: '#FF0000', + useBrushColorForCursorRing: false, + }); + this.canvasManager.registerPlugin(maskBrushPlugin); + } + + if (this.options.customBrush) { + const customBrushPlugin = new CustomBrushPlugin({ + brushSize: 25, + brushOpacity: 1, + cursorOutlineType: 'dotted', + cursorPrimaryColor: '#000000', + cursorSecondaryColor: '#FFFFFF', + cursorLineWidth: 1, + cursorFill: true, + useBrushColorPrimaryColor: true, + brushColor: '#64D813', + useBrushColorForCursorRing: false, + }); + this.canvasManager.registerPlugin(customBrushPlugin); + } + + if (this.options.imageCompareSlider) { + const imageCompareSliderPlugin = new ImageCompareSliderPlugin({ + sliderColor: '#570d7b', + sliderWidth: 2, + handleRadius: 18, + handleStrokeWidth: 4, + handleStrokeColor: '#fff', + topMargin: 23, + bottomMargin: 23, + mode: 'Pairs' // 'Pairs', 'Replace', or 'Multi' + }); + this.canvasManager.registerPlugin(imageCompareSliderPlugin); + } + + if (this.options.imageAdder) { + const imageAdderPlugin = new ImageAdderPlugin(); + this.canvasManager.registerPlugin(imageAdderPlugin); + } + + console.log('All selected plugins registered successfully'); + + this.isInitialized = true; + } catch (error) { + console.error('Error initializing CanvasLoader:', error); + this.hideCanvasUI(); + } + } + + hideCanvasUI() { + const canvasWrapper = document.getElementById('canvasWrapper'); + const pluginUIContainer = document.getElementById('pluginUIContainer'); + + if (canvasWrapper) { + canvasWrapper.style.display = 'none'; + } + + if (pluginUIContainer) { + pluginUIContainer.style.display = 'none'; + } + } + + getOriginalImage(id) { + const imageLoaderPlugin = this.canvasManager.getPluginByName('ImageLoaderPlugin'); + if (imageLoaderPlugin && imageLoaderPlugin.getOriginalImage) { + return imageLoaderPlugin.getOriginalImage(id); + } else { + console.error('ImageLoaderPlugin is not registered or does not have getOriginalImage.'); + return null; + } + } + + getMaskImage() { + const maskBrushPlugin = this.canvasManager.getPluginByName('MaskBrushPlugin'); + if (maskBrushPlugin) { + const exportFunction = maskBrushPlugin.getExportFunction('saveAllMasksCombined'); + if (exportFunction) { + return exportFunction(); + } else { + console.error('Failed to get export function for saveMaskAlphaOnImage.'); + return null; + } + } + return null; + } + + getMaskAlphaOnImage() { + const maskBrushPlugin = this.canvasManager.getPluginByName('MaskBrushPlugin'); + if (maskBrushPlugin) { + const exportFunction = maskBrushPlugin.getExportFunction('saveAllMasksCombinedAlphaOnImage'); + if (exportFunction) { + return exportFunction(); + } else { + console.error('Failed to get export function for saveMaskAlphaOnImage.'); + return null; + } + } + return null; + } + + getSelectedMaskAlphaOnImage() { + const maskBrushPlugin = this.canvasManager.getPluginByName('MaskBrushPlugin'); + if (maskBrushPlugin) { + const selectedOption = maskBrushPlugin.saveOptionsSelect.value; + const exportFunction = maskBrushPlugin.getExportFunction(selectedOption); + if (exportFunction) { + return exportFunction(); + } else { + console.error('Unknown save option selected in MaskBrushPlugin.'); + return null; + } + } + } + + getCanvasOutImage() { + return this.getCanvasImage(); + } + + getCanvasImage() { + if (this.canvasManager && this.canvasManager.getCanvasImage) { + return this.canvasManager.getCanvasImage(); + } else { + console.error('CanvasManager is not initialized or getCanvasImage method is unavailable.'); + return null; + } + } + +} diff --git a/web/core/js/common/components/canvas/CanvasManager.js b/web/core/js/common/components/canvas/CanvasManager.js new file mode 100644 index 0000000..ca00eaa --- /dev/null +++ b/web/core/js/common/components/canvas/CanvasManager.js @@ -0,0 +1,117 @@ +import { EventEmitter } from './EventEmitter.js'; +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class CanvasManager extends EventEmitter { + constructor(options = {}) { + super(); + this.canvasId = options.canvasId || 'imageCanvas'; + this.canvas = new fabric.Canvas(this.canvasId, { + selection: false, + defaultCursor: 'default', + preserveObjectStacking: true + }); + + this.plugins = new Set(); + + this.init(); + + window.addEventListener('resize', this.resizeCanvas.bind(this)); + + this.observeContainerResize(); + + this.scaleMultiplier = 1; + + this.on('canvas:scaleChanged', this.onScaleChanged.bind(this)); + } + + init() { + this.resizeCanvas(); + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + } + + resizeCanvas() { + const container = document.getElementById('canvasWrapper'); + if (!container) { + console.warn('canvasWrapper element not found in the DOM.'); + return; + } + + const { clientWidth, clientHeight } = container; + + this.canvas.setWidth(clientWidth); + this.canvas.setHeight(clientHeight); + + this.canvas.renderAll(); + + this.emit('canvas:resized', { width: clientWidth, height: clientHeight }); + } + + observeContainerResize() { + const container = document.getElementById('canvasWrapper'); + if (!container || typeof ResizeObserver === 'undefined') return; + + const resizeObserver = new ResizeObserver(() => { + this.resizeCanvas(); + }); + + resizeObserver.observe(container); + } + + registerPlugin(plugin) { + if (!(plugin instanceof CanvasPlugin)) { + throw new Error('Plugin must extend the CanvasPlugin class.'); + } + plugin.init(this); + this.plugins.add(plugin); + console.log(`CanvasManager: Registered plugin ${plugin.name}`); + } + + unregisterPlugin(plugin) { + if (this.plugins.has(plugin)) { + plugin.destroy(); + this.plugins.delete(plugin); + console.log(`CanvasManager: Unregistered plugin ${plugin.name}`); + } + } + + getCanvasImage() { + return this.canvas.toDataURL({ + format: 'png', + multiplier: this.scaleMultiplier + }); + } + + getPluginByName(name) { + for (let plugin of this.plugins) { + if (plugin.name === name) { + return plugin; + } + } + return null; + } + + onHandleSave() { + const maskBrushPlugin = this.getPluginByName('MaskBrushPlugin'); + if (maskBrushPlugin) { + const selectedOption = maskBrushPlugin.saveOptionsSelect.value; + this.emit('save:trigger', selectedOption); + } else { + console.error('MaskBrushPlugin is not registered. Cannot handle save.'); + } + } + + onScaleChanged(data) { + this.scaleMultiplier = data.scale; + console.log(`CanvasManager: Scale multiplier set to ${this.scaleMultiplier}`); + } + + destroy() { + window.removeEventListener('resize', this.resizeCanvas.bind(this)); + + this.plugins.forEach(plugin => plugin.destroy()); + this.plugins.clear(); + + this.canvas.dispose(); + console.log('CanvasManager destroyed'); + } +} diff --git a/web/core/js/common/components/canvas/CanvasPlugin.js b/web/core/js/common/components/canvas/CanvasPlugin.js new file mode 100644 index 0000000..9f7fb68 --- /dev/null +++ b/web/core/js/common/components/canvas/CanvasPlugin.js @@ -0,0 +1,13 @@ +export class CanvasPlugin { + constructor(name) { + this.name = name; + } + + init(canvasManager) { + throw new Error('init() must be implemented by the plugin.'); + } + + destroy() { + throw new Error('destroy() must be implemented by the plugin.'); + } +} diff --git a/web/core/js/common/components/canvas/CanvasScaleForSavePlugin.js b/web/core/js/common/components/canvas/CanvasScaleForSavePlugin.js new file mode 100644 index 0000000..289a0c4 --- /dev/null +++ b/web/core/js/common/components/canvas/CanvasScaleForSavePlugin.js @@ -0,0 +1,225 @@ +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class CanvasScaleForSavePlugin extends CanvasPlugin { + constructor(options = {}) { + super('CanvasScaleForSavePlugin'); + + this.increaseScale = this.increaseScale.bind(this); + this.decreaseScale = this.decreaseScale.bind(this); + this.updateScaleDisplay = this.updateScaleDisplay.bind(this); + this.emitScaleChange = this.emitScaleChange.bind(this); + this.downloadCanvasImage = this.downloadCanvasImage.bind(this); + this.onCanvasResized = this.onCanvasResized.bind(this); + + this.defaultScale = options.defaultScale || 1; + this.showDownloadButton = options.showDownloadButton !== undefined ? options.showDownloadButton : true; + + this.scale = this.defaultScale; + + this.uiContainer = null; + this.scaleDecreaseBtn = null; + this.scaleIncreaseBtn = null; + this.scaleValueDisplay = null; + this.actualSizeDisplay = null; + this.downloadBtn = null; + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.createUI(); + this.attachEventListeners(); + this.updateActualSize(); + } + + createUI() { + const styleSheet = document.createElement('style'); + styleSheet.textContent = ` + .csfs-container * { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } + + .csfs-container { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + /* background: var(--color-background); */ + /* border: 1px dashed var(--color-border); */ + user-select: none; + margin-left: 1rem; /* Adjust as needed to align with existing controls */ + } + + .csfs-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: var(--color-button-primary); + border: none; + cursor: pointer; + transition: all 0.2s; + color: var(--color-primary-text); + min-width: 36px; + height: 36px; + } + + .csfs-button:hover { + background: var(--color-button-primary-hover); + } + + .csfs-button svg { + width: 1.25rem; + height: 1.25rem; + } + + .csfs-button[data-active="true"] { + border: 1px dashed var(--color-button-secondary-text-active); + } + + /* Styles for scale controls */ + .csfs-scale-container { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem; + } + + .csfs-scale-label { + margin-right: 0.25rem; + font-weight: bold; + } + + .csfs-scale-display { + margin-left: 0.5rem; + font-size: 0.9rem; + color: var(--color-secondary-text); + font-weight: bold; + + } + .csfs-scale-value { + padding: 8px 8px; + font-weight: bold; + } + `; + document.head.appendChild(styleSheet); + + const temp = document.createElement('div'); + temp.innerHTML = ` +
+ +
+ + ${this.showDownloadButton ? ` + + + ` : ''} + + + 1x + + 512x512 +
+
+ `; + + this.uiContainer = temp.firstElementChild; + this.scaleDecreaseBtn = this.uiContainer.querySelector('#csfs-scaleDecreaseBtn'); + this.scaleIncreaseBtn = this.uiContainer.querySelector('#csfs-scaleIncreaseBtn'); + this.scaleValueDisplay = this.uiContainer.querySelector('#csfs-scaleValue'); + this.actualSizeDisplay = this.uiContainer.querySelector('#csfs-actualSize'); + this.downloadBtn = this.uiContainer.querySelector('#csfs-downloadBtn'); + + const pluginUIContainer = document.getElementById('pluginUIContainer'); + if (pluginUIContainer) { + pluginUIContainer.appendChild(this.uiContainer); + } else { + console.warn('Element with id "pluginUIContainer" not found.'); + } + } + + attachEventListeners() { + this.scaleIncreaseBtn.addEventListener('click', this.increaseScale); + this.scaleDecreaseBtn.addEventListener('click', this.decreaseScale); + + if (this.downloadBtn) { + this.downloadBtn.addEventListener('click', this.downloadCanvasImage); + } + + this.canvasManager.on('canvas:resized', this.onCanvasResized); + } + + + detachEventListeners() { + this.scaleIncreaseBtn.removeEventListener('click', this.increaseScale); + this.scaleDecreaseBtn.removeEventListener('click', this.decreaseScale); + + if (this.downloadBtn) { + this.downloadBtn.removeEventListener('click', this.downloadCanvasImage); + } + + this.canvasManager.off('canvas:resized', this.onCanvasResized); + } + + increaseScale() { + if (this.scale < 10) { + this.scale += 1; + this.updateScaleDisplay(); + this.emitScaleChange(); + this.updateActualSize(); + } + } + + decreaseScale() { + if (this.scale > 1) { + this.scale -= 1; + this.updateScaleDisplay(); + this.emitScaleChange(); + this.updateActualSize(); + } + } + + updateScaleDisplay() { + this.scaleValueDisplay.textContent = `${this.scale}x`; + } + + emitScaleChange() { + this.canvasManager.emit('canvas:scaleChanged', { scale: this.scale }); + } + + updateActualSize() { + const canvasWidth = this.canvas.getWidth(); + const canvasHeight = this.canvas.getHeight(); + const scaledWidth = Math.round(canvasWidth * this.scale); + const scaledHeight = Math.round(canvasHeight * this.scale); + this.actualSizeDisplay.textContent = `${scaledWidth}x${scaledHeight}`; + } + + onCanvasResized(data) { + this.updateActualSize(); + } + + downloadCanvasImage() { + const dataURL = this.canvasManager.getCanvasImage(); + const link = document.createElement('a'); + link.href = dataURL; + link.download = 'canvas-image.png'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + destroy() { + if (this.uiContainer && this.uiContainer.parentNode) { + this.uiContainer.parentNode.removeChild(this.uiContainer); + } + + this.detachEventListeners(); + } +} diff --git a/web/core/js/common/components/canvas/CustomBrushPlugin.js b/web/core/js/common/components/canvas/CustomBrushPlugin.js new file mode 100644 index 0000000..d7f8681 --- /dev/null +++ b/web/core/js/common/components/canvas/CustomBrushPlugin.js @@ -0,0 +1,824 @@ +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class CustomBrushPlugin extends CanvasPlugin { + constructor(options = {}) { + super(options.name || 'CustomBrushPlugin'); + + this.brushSize = options.brushSize || 25; + this.minBrushSize = options.minBrushSize || 1; + this.maxBrushSize = options.maxBrushSize || 500; + this.brushColor = options.brushColor || '#000000'; + this.brushOpacity = options.brushOpacity !== undefined ? options.brushOpacity : 1; + this.brushTipResizeSpeed = options.brushTipResizeSpeed || 1; + this.cursorOutlineType = options.cursorOutlineType || 'dashed'; + this.cursorPrimaryColor = options.cursorPrimaryColor || '#000000'; + this.cursorSecondaryColor = options.cursorSecondaryColor || '#FFFFFF'; + this.cursorLineWidth = options.cursorLineWidth || 2; + this.cursorFill = options.cursorFill !== undefined ? options.cursorFill : false; + this.useBrushColorPrimaryColor = options.useBrushColorPrimaryColor || false; + this.showSystemCursor = options.showSystemCursor !== undefined ? options.showSystemCursor : false; + this.useBrushColorForCursorRing = options.useBrushColorForCursorRing !== undefined ? options.useBrushColorForCursorRing : false; + this.cursorRingOpacityAffected = options.cursorRingOpacityAffected !== undefined ? options.cursorRingOpacityAffected : false; + + this.canvasManager = null; + this.canvas = null; + this.drawingMode = false; + this.isMouseDown = false; + this.lastPointer = null; + this.currentPath = null; + this.brushIcon = '/core/media/ui/paintree.png'; + + this.originalCanvasProperties = null; + + this.cursorCircle = null; + this.secondaryCircle = null; + + this.uiContainer = null; + this.uiHeader = null; + this.brushSizeInput = null; + this.brushOpacityInput = null; + this.colorPicker = null; + this.toggleDrawBtn = null; + this.minimizeBtn = null; + this.isMinimized = false; + this.currentMode = 'full'; // Modes: 'full', 'mini', 'minimized' + + this.isDragging = false; + this.dragOffsetX = 0; + this.dragOffsetY = 0; + + this.drawnObjects = new Set(); + + this.currentZoom = 1; + + this.hiddenCursorClass = 'custom-brush-plugin-hidden-cursor'; + + this.onBrushSizeChange = this.onBrushSizeChange.bind(this); + this.onBrushOpacityChange = this.onBrushOpacityChange.bind(this); + this.onToggleDrawingMode = this.onToggleDrawingMode.bind(this); + this.onColorChange = this.onColorChange.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.enforceObjectProperties = this.enforceObjectProperties.bind(this); + this.onViewportChanged = this.onViewportChanged.bind(this); + + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.handleMouseWheel = this.handleMouseWheel.bind(this); + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + this.onObjectAdded = this.onObjectAdded.bind(this); + this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this); + this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this); + this.onDocumentMouseUp = this.onDocumentMouseUp.bind(this); + this.onMinimize = this.onMinimize.bind(this); + this.onModeChange = this.onModeChange.bind(this); + this.brushStrokeIdCounter = 0; + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.storeOriginalCanvasProperties(); + + this.disableSelectionBox(); + + this.injectHiddenCursorCSS(); + + this.createUI(); + + this.attachEventListeners(); + + this.brushSizeInput.value = this.brushSize; + this.brushOpacityInput.value = this.brushOpacity * 100; + this.colorPicker.value = this.brushColor; + + this.canvas.on('object:added', this.onObjectAdded); + + this.canvas.getObjects().forEach(this.enforceObjectProperties); + + this.canvasManager.on('viewport:changed', this.onViewportChanged); + } + + injectHiddenCursorCSS() { + if (document.getElementById('custom-brush-plugin-styles')) return; + + const style = document.createElement('style'); + style.id = 'custom-brush-plugin-styles'; + style.type = 'text/css'; + style.innerHTML = ` + + `; + document.head.appendChild(style); + } + + // Helper function to convert HEX to RGBA + getFillColorWithOpacity(hex, alpha) { + // Remove '#' if present + hex = hex.replace('#', ''); + let r, g, b; + if (hex.length === 3) { + r = parseInt(hex[0] + hex[0], 16); + g = parseInt(hex[1] + hex[1], 16); + b = parseInt(hex[2] + hex[2], 16); + } else if (hex.length === 6) { + r = parseInt(hex.substring(0, 2), 16); + g = parseInt(hex.substring(2, 4), 16); + b = parseInt(hex.substring(4, 6), 16); + } else { + // Invalid format + return 'rgba(0,0,0,1)'; + } + return `rgba(${r},${g},${b},${alpha})`; + } + + isDrawnObject(obj) { + return obj.type === 'path'; + } + + onObjectAdded(e) { + const obj = e.target; + if (obj && this.isDrawnObject(obj)) { + this.enforceObjectProperties(obj); + } + } + + createUI() { + this.uiContainer = document.createElement('div'); + this.uiContainer.className = 'cbp-brush-ui-container cbp-brush-ui-full'; + + this.uiContainer.innerHTML = ` +
+ Brush Settings + +
+
+ + +
+ + + +
+
+ `; + + document.body.appendChild(this.uiContainer); + + this.brushSizeInput = this.uiContainer.querySelector('#cbp-brush-size-input'); + this.brushOpacityInput = this.uiContainer.querySelector('#cbp-brush-opacity-input'); + this.colorPicker = this.uiContainer.querySelector('#cbp-color-picker'); + this.toggleDrawBtn = this.uiContainer.querySelector('#cbp-toggle-drawing-mode-btn'); + this.toggleBtnIcon = this.uiContainer.querySelector('#cbp-toggle-btn-icon'); + this.minimizeBtn = this.uiContainer.querySelector('.cbp-brush-ui-minimize-btn'); + this.uiHeader = this.uiContainer.querySelector('.cbp-brush-ui-header'); + + this.uiHeader.addEventListener('mousedown', this.onHeaderMouseDown); + this.minimizeBtn.addEventListener('click', this.onMinimize); + this.toggleDrawBtn.addEventListener('click', this.onToggleDrawingMode); + } + + attachEventListeners() { + this.brushSizeInput.addEventListener('input', this.onBrushSizeChange); + this.brushOpacityInput.addEventListener('input', this.onBrushOpacityChange); + this.colorPicker.addEventListener('input', this.onColorChange); + window.addEventListener('keydown', this.onKeyDown); + } + + detachEventListeners() { + this.brushSizeInput.removeEventListener('input', this.onBrushSizeChange); + this.brushOpacityInput.removeEventListener('input', this.onBrushOpacityChange); + this.colorPicker.removeEventListener('input', this.onColorChange); + window.removeEventListener('keydown', this.onKeyDown); + + this.uiHeader.removeEventListener('mousedown', this.onHeaderMouseDown); + this.minimizeBtn.removeEventListener('click', this.onMinimize); + this.toggleDrawBtn.removeEventListener('click', this.onToggleDrawingMode); + } + + onKeyDown(e) { + // // Toggle Drawing Mode (e.g., press 'D') + // if (e.key.toLowerCase() === 'd') { + // e.preventDefault(); + // this.onToggleDrawingMode(); + // } + } + + onBrushSizeChange(e) { + this.brushSize = parseInt(e.target.value, 10); + if (this.drawingMode) { + this.updateCursorCircle(); + this.canvas.requestRenderAll(); + } + } + + onBrushOpacityChange(e) { + this.brushOpacity = parseInt(e.target.value, 10) / 100; + if (this.drawingMode) { + this.updateCursorCircle(); + this.canvas.requestRenderAll(); + } + } + + onColorChange() { + this.brushColor = this.colorPicker.value; + if (this.drawingMode) { + this.updateCursorCircle(); + } + } + + storeOriginalCanvasProperties() { + this.originalCanvasProperties = { + selection: this.canvas.selection, + selectionColor: this.canvas.selectionColor, + selectionBorderColor: this.canvas.selectionBorderColor, + selectionLineWidth: this.canvas.selectionLineWidth, + hoverCursor: this.canvas.hoverCursor, + moveCursor: this.canvas.moveCursor, + defaultCursor: this.canvas.defaultCursor, + freeDrawingCursor: this.canvas.freeDrawingCursor + }; + } + + disableSelectionBox() { + this.canvas.selection = false; + this.canvas.selectionColor = 'transparent'; + this.canvas.selectionBorderColor = 'transparent'; + this.canvas.selectionLineWidth = 0; + + this.canvas.hoverCursor = 'default'; + this.canvas.moveCursor = 'default'; + this.canvas.freeDrawingCursor = 'none'; + + this.canvas.skipTargetFind = true; + this.canvas.preserveObjectStacking = true; + } + + onToggleDrawingMode() { + this.drawingMode = !this.drawingMode; + + const toggleButton = document.getElementById('cbp-toggle-drawing-mode-btn'); + if (this.drawingMode) { + toggleButton.classList.add('active'); + } else { + toggleButton.classList.remove('active'); + } + + this.toggleBtnIcon.src = this.brushIcon; + + + if (this.drawingMode) { + this.canvas.defaultCursor = this.showSystemCursor ? 'default' : 'none'; + + if (!this.showSystemCursor) { + this.canvas.lowerCanvasEl.classList.add(this.hiddenCursorClass); + this.canvas.upperCanvasEl.classList.add(this.hiddenCursorClass); + } + } else { + this.canvas.defaultCursor = this.originalCanvasProperties.defaultCursor || 'default'; + + this.canvas.lowerCanvasEl.classList.remove(this.hiddenCursorClass); + this.canvas.upperCanvasEl.classList.remove(this.hiddenCursorClass); + } + + this.disableSelectionBox(); + + this.canvas.getObjects().forEach(obj => { + if (this.drawnObjects.has(obj)) { + obj.selectable = false; + obj.evented = false; + obj.hasControls = false; + obj.hasBorders = false; + obj.lockMovementX = true; + obj.lockMovementY = true; + } + }); + + if (this.drawingMode) { + this.attachDrawingEvents(); + this.brushSizeInput.disabled = false; + this.brushOpacityInput.disabled = false; + this.colorPicker.disabled = false; + this.updateCursorCircle(); + this.canvas.requestRenderAll(); + this.setNonDrawnObjectsSelectable(false); + } else { + this.detachDrawingEvents(); + if (this.cursorCircle) { + this.canvas.remove(this.cursorCircle); + this.cursorCircle = null; + } + if (this.secondaryCircle) { + this.canvas.remove(this.secondaryCircle); + this.secondaryCircle = null; + } + this.brushSizeInput.disabled = true; + this.brushOpacityInput.disabled = true; + this.colorPicker.disabled = true; + this.setNonDrawnObjectsSelectable(true); + } + } + + enforceObjectProperties(obj) { + if (obj === this.cursorCircle || obj === this.secondaryCircle) return; + + if (this.isDrawnObject(obj)) { + obj.selectable = false; + obj.evented = false; + obj.hasControls = false; + obj.hasBorders = false; + obj.lockMovementX = true; + obj.lockMovementY = true; + obj.hoverCursor = 'default'; + this.drawnObjects.add(obj); + } + } + + setNonDrawnObjectsSelectable(selectable) { + this.canvas.getObjects().forEach((obj) => { + if (!this.drawnObjects.has(obj) && obj !== this.cursorCircle && obj !== this.secondaryCircle) { + obj.selectable = selectable; + obj.evented = selectable; + obj.hoverCursor = selectable ? 'move' : 'default'; + } + }); + } + + attachDrawingEvents() { + this.canvas.on('mouse:down', this.onMouseDown); + this.canvas.on('mouse:move', this.onMouseMove); + this.canvas.on('mouse:up', this.onMouseUp); + this.canvas.on('mouse:wheel', this.handleMouseWheel); + this.canvas.on('mouse:over', this.handleMouseOver); + this.canvas.on('mouse:out', this.handleMouseOut); + } + + detachDrawingEvents() { + this.canvas.off('mouse:down', this.onMouseDown); + this.canvas.off('mouse:move', this.onMouseMove); + this.canvas.off('mouse:up', this.onMouseUp); + this.canvas.off('mouse:wheel', this.handleMouseWheel); + this.canvas.off('mouse:over', this.handleMouseOver); + this.canvas.off('mouse:out', this.handleMouseOut); + } + + updateCursorCircle() { + const dashArray = this.getStrokeDashArray(); + const isOutlined = this.cursorOutlineType === 'dashed' || this.cursorOutlineType === 'dotted'; + + if (!this.cursorCircle) { + this.cursorCircle = new fabric.Circle({ + radius: this.brushSize / 2, + fill: this.cursorFill + ? (this.useBrushColorPrimaryColor + ? this.getFillColorWithOpacity(this.brushColor, this.brushOpacity) + : this.getFillColorWithOpacity(this.cursorPrimaryColor, this.cursorRingOpacityAffected ? this.brushOpacity : 1)) + : 'transparent', + stroke: this.getCursorStrokeColor(), + strokeWidth: this.cursorOutlineType === 'none' ? 0 : this.cursorLineWidth, + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + originX: 'center', + originY: 'center', + strokeDashArray: dashArray, + hoverCursor: 'none' + }); + + if (isOutlined && !this.useBrushColorForCursorRing) { + this.secondaryCircle = new fabric.Circle({ + radius: this.brushSize / 2, + fill: 'transparent', + stroke: this.cursorSecondaryColor, + strokeWidth: this.cursorLineWidth, + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + originX: 'center', + originY: 'center', + strokeDashArray: dashArray, + strokeDashOffset: this.cursorOutlineType === 'dashed' ? dashArray[0] : dashArray[0], + hoverCursor: 'none' + }); + this.canvas.add(this.secondaryCircle); + } + + this.canvas.add(this.cursorCircle); + if (this.secondaryCircle) { + this.secondaryCircle.bringToFront(); + } + this.cursorCircle.bringToFront(); + } else { + this.cursorCircle.set({ + radius: this.brushSize / 2, + fill: this.cursorFill + ? (this.useBrushColorPrimaryColor + ? this.getFillColorWithOpacity(this.brushColor, this.brushOpacity) + : this.getFillColorWithOpacity(this.cursorPrimaryColor, this.cursorRingOpacityAffected ? this.brushOpacity : 1)) + : 'transparent', + stroke: this.getCursorStrokeColor(), + strokeWidth: this.cursorOutlineType === 'none' ? 0 : this.cursorLineWidth, + strokeDashArray: dashArray + }); + + if (isOutlined && !this.useBrushColorForCursorRing) { + if (!this.secondaryCircle) { + this.secondaryCircle = new fabric.Circle({ + radius: this.brushSize / 2, + fill: 'transparent', + stroke: this.cursorSecondaryColor, + strokeWidth: this.cursorLineWidth, + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + originX: 'center', + originY: 'center', + strokeDashArray: dashArray, + strokeDashOffset: this.cursorOutlineType === 'dashed' ? dashArray[0] : dashArray[0], + hoverCursor: 'none' + }); + this.canvas.add(this.secondaryCircle); + } else { + this.secondaryCircle.set({ + radius: this.brushSize / 2, + stroke: this.cursorSecondaryColor, + strokeWidth: this.cursorLineWidth, + strokeDashArray: dashArray, + strokeDashOffset: this.cursorOutlineType === 'dashed' ? dashArray[0] : dashArray[0] + }); + } + } else if (this.secondaryCircle) { + this.canvas.remove(this.secondaryCircle); + this.secondaryCircle = null; + } + } + + if (this.currentZoom !== 0) { + const scale = 1 / this.currentZoom; + this.cursorCircle.set({ + scaleX: scale, + scaleY: scale + }); + if (this.secondaryCircle) { + this.secondaryCircle.set({ + scaleX: scale, + scaleY: scale + }); + } + } + } + + getCursorStrokeColor() { + if (this.cursorOutlineType === 'none') { + return 'transparent'; + } + + if (this.cursorOutlineType === 'solid') { + return this.useBrushColorForCursorRing ? this.brushColor : this.cursorPrimaryColor; + } + + if (this.cursorOutlineType === 'dashed' || this.cursorOutlineType === 'dotted') { + return this.useBrushColorForCursorRing ? this.brushColor : this.cursorPrimaryColor; + } + + return this.cursorPrimaryColor; + } + + getStrokeDashArray() { + if (this.cursorOutlineType === 'solid') { + return []; + } else if (this.cursorOutlineType === 'dashed') { + return [4, 4]; + } else if (this.cursorOutlineType === 'dotted') { + return [1, 2]; + } else if (this.cursorOutlineType === 'none') { + return []; + } + return []; + } + + handleMouseWheel(opt) { + if (!this.drawingMode) return; + + const delta = opt.e.deltaY; + opt.e.preventDefault(); + opt.e.stopPropagation(); + + let deltaSize = delta < 0 ? 1 : -1; + deltaSize *= this.brushTipResizeSpeed; + this.brushSize = Math.min(this.maxBrushSize, Math.max(this.minBrushSize, this.brushSize + deltaSize)); + this.brushSizeInput.value = this.brushSize; + this.updateCursorCircle(); + this.canvas.requestRenderAll(); + } + + handleMouseOver() { + if (this.drawingMode) { + if (this.cursorCircle) { + this.cursorCircle.visible = true; + } + if (this.secondaryCircle) { + this.secondaryCircle.visible = true; + } + this.canvas.requestRenderAll(); + } + } + + handleMouseOut() { + if (this.drawingMode) { + if (this.cursorCircle) { + this.cursorCircle.visible = false; + } + if (this.secondaryCircle) { + this.secondaryCircle.visible = false; + } + this.canvas.requestRenderAll(); + } + } + + updateCursorPosition(opt) { + if (!this.cursorCircle) return; + + const pointer = this.canvas.getPointer(opt.e); + + const scale = 1 / this.currentZoom; + + this.cursorCircle.set({ + left: pointer.x, + top: pointer.y, + scaleX: scale, + scaleY: scale + }).setCoords(); + + if (this.secondaryCircle) { + this.secondaryCircle.set({ + left: pointer.x, + top: pointer.y, + scaleX: scale, + scaleY: scale + }).setCoords(); + + this.secondaryCircle.bringToFront(); + } + + this.cursorCircle.bringToFront(); + } + + onMouseMove(o) { + const pointer = this.canvas.getPointer(o.e); + + this.updateCursorPosition(o); + + if (this.isMouseDown && this.currentPath) { + const pathData = this.currentPath.path; + pathData.push(['L', pointer.x, pointer.y]); + this.currentPath.set({ path: pathData }); + + this.canvas.requestRenderAll(); + } + } + + onMouseDown(o) { + this.isMouseDown = true; + const pointer = this.canvas.getPointer(o.e); + this.lastPointer = pointer; + + this.currentPath = new fabric.Path(`M ${pointer.x} ${pointer.y}`, { + stroke: this.brushColor, + strokeWidth: this.brushSize / this.currentZoom, + fill: null, + selectable: false, + evented: false, + opacity: this.brushOpacity, + originX: 'center', + originY: 'center', + objectCaching: false, + strokeLineCap: 'round', + strokeLineJoin: 'round', + brushStrokeId: `brush-stroke-${this.brushStrokeIdCounter++}` + }); + + this.canvas.add(this.currentPath); + this.drawnObjects.add(this.currentPath); + } + + onMouseUp() { + if (this.isMouseDown && this.currentPath) { + this.isMouseDown = false; + this.currentPath.setCoords(); + this.currentPath = null; + // No need to manually fire 'object:added'; Fabric.js handles it + } + } + + onViewportChanged(event) { + const { transform } = event; + const zoom = transform[0]; + + this.currentZoom = zoom; + + if (this.cursorCircle) { + const scale = 1 / zoom; + this.cursorCircle.set({ + scaleX: scale, + scaleY: scale + }); + + if (this.secondaryCircle) { + this.secondaryCircle.set({ + scaleX: scale, + scaleY: scale + }); + } + } + } + + onHeaderMouseDown(e) { + e.preventDefault(); + this.isDragging = true; + + const rect = this.uiContainer.getBoundingClientRect(); + this.dragOffsetX = e.clientX - rect.left; + this.dragOffsetY = e.clientY - rect.top; + + document.addEventListener('mousemove', this.onDocumentMouseMove); + document.addEventListener('mouseup', this.onDocumentMouseUp); + } + + onDocumentMouseMove(e) { + if (!this.isDragging) return; + + let newLeft = e.clientX - this.dragOffsetX; + let newTop = e.clientY - this.dragOffsetY; + + // Constrain within the viewport + const containerWidth = this.uiContainer.offsetWidth; + const containerHeight = this.uiContainer.offsetHeight; + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + newLeft = Math.max(0, Math.min(newLeft, windowWidth - containerWidth)); + newTop = Math.max(0, Math.min(newTop, windowHeight - containerHeight)); + + // Update UI container position + this.uiContainer.style.left = `${newLeft}px`; + this.uiContainer.style.top = `${newTop}px`; + this.uiContainer.style.bottom = 'auto'; + this.uiContainer.style.right = 'auto'; + } + + onDocumentMouseUp(e) { + this.isDragging = false; + + document.removeEventListener('mousemove', this.onDocumentMouseMove); + document.removeEventListener('mouseup', this.onDocumentMouseUp); + } + + onMinimize() { + if (this.currentMode === 'minimized') { + this.currentMode = this.previousMode || 'full'; + } else { + this.previousMode = this.currentMode; + this.currentMode = 'minimized'; + } + this.onModeChange(); + } + + onModeChange() { + if (this.currentMode === 'full') { + this.uiContainer.classList.remove('cbp-brush-ui-mini', 'cbp-brush-ui-minimized'); + this.uiContainer.classList.add('cbp-brush-ui-full'); + this.minimizeBtn.title = 'Minimize'; + this.minimizeBtn.textContent = '−'; + this.toggleDrawBtn.style.display = 'block'; + this.uiContainer.querySelector('.cbp-brush-ui-title').textContent = 'Brush Settings'; + this.updateUIForMode(); + } else if (this.currentMode === 'mini') { + this.uiContainer.classList.remove('cbp-brush-ui-full', 'cbp-brush-ui-minimized'); + this.uiContainer.classList.add('cbp-brush-ui-mini'); + this.minimizeBtn.title = 'Minimize'; + this.minimizeBtn.textContent = '−'; + this.toggleDrawBtn.style.display = 'block'; + // this.uiContainer.querySelector('.cbp-brush-ui-title').textContent = 'Brush'; + this.updateUIForMode(); + } else if (this.currentMode === 'minimized') { + this.uiContainer.classList.remove('cbp-brush-ui-full', 'cbp-brush-ui-mini'); + this.uiContainer.classList.add('cbp-brush-ui-minimized'); + this.minimizeBtn.title = 'Maximize'; + this.minimizeBtn.textContent = '+'; + this.toggleDrawBtn.style.display = 'none'; + this.uiContainer.querySelector('.cbp-brush-ui-title').textContent = ''; + } + } + + updateUIForMode() { + const content = this.uiContainer.querySelector('.cbp-brush-ui-content'); + + if (this.currentMode === 'full') { + content.innerHTML = ` + + + + + `; + } else if (this.currentMode === 'mini') { + content.innerHTML = ` +
+ +
+
+ +
+
+ +
+ + `; + } + this.brushSizeInput = this.uiContainer.querySelector('#cbp-brush-size-input'); + this.brushOpacityInput = this.uiContainer.querySelector('#cbp-brush-opacity-input'); + this.colorPicker = this.uiContainer.querySelector('#cbp-color-picker'); + this.toggleDrawBtn = this.uiContainer.querySelector('#cbp-toggle-drawing-mode-btn'); + this.toggleBtnIcon = this.uiContainer.querySelector('#cbp-toggle-btn-icon'); + + this.brushSizeInput.addEventListener('input', this.onBrushSizeChange); + this.brushOpacityInput.addEventListener('input', this.onBrushOpacityChange); + this.colorPicker.addEventListener('input', this.onColorChange); + this.toggleDrawBtn.addEventListener('click', this.onToggleDrawingMode); + } + + switchToMiniMode() { + if (this.currentMode !== 'minimized') { + this.previousMode = this.currentMode; + this.currentMode = 'mini'; + this.onModeChange(); + } + } + + switchToFullMode() { + if (this.currentMode !== 'minimized') { + this.currentMode = 'full'; + this.onModeChange(); + } + } + + destroy() { + if (this.uiContainer) { + document.body.removeChild(this.uiContainer); + this.uiContainer = null; + } + + this.detachEventListeners(); + + this.detachDrawingEvents(); + + if (this.cursorCircle) { + this.canvas.remove(this.cursorCircle); + this.cursorCircle = null; + } + if (this.secondaryCircle) { + this.canvas.remove(this.secondaryCircle); + this.secondaryCircle = null; + } + + this.canvas.off('object:added', this.onObjectAdded); + + // Remove viewport:changed listener + this.canvasManager.off('viewport:changed', this.onViewportChanged); + + // Restore original canvas properties + if (this.originalCanvasProperties) { + this.canvas.selection = this.originalCanvasProperties.selection; + this.canvas.selectionColor = this.originalCanvasProperties.selectionColor; + this.canvas.selectionBorderColor = this.originalCanvasProperties.selectionBorderColor; + this.canvas.selectionLineWidth = this.originalCanvasProperties.selectionLineWidth; + this.canvas.hoverCursor = this.originalCanvasProperties.hoverCursor; + this.canvas.moveCursor = this.originalCanvasProperties.moveCursor; + this.canvas.defaultCursor = this.originalCanvasProperties.defaultCursor; + this.canvas.freeDrawingCursor = this.originalCanvasProperties.freeDrawingCursor; + } + + super.destroy(); + } +} + diff --git a/web/core/js/common/components/canvas/EventEmitter.js b/web/core/js/common/components/canvas/EventEmitter.js new file mode 100644 index 0000000..08dee88 --- /dev/null +++ b/web/core/js/common/components/canvas/EventEmitter.js @@ -0,0 +1,24 @@ +export class EventEmitter { + constructor() { + this.events = {}; + } + + on(event, listener) { + if (!this.events[event]) { + this.events[event] = new Set(); + } + this.events[event].add(listener); + } + + off(event, listener) { + if (!this.events[event]) return; + this.events[event].delete(listener); + } + + emit(event, ...args) { + if (!this.events[event]) return; + for (let listener of this.events[event]) { + listener(...args); + } + } +} diff --git a/web/core/js/common/components/canvas/ImageAdderPlugin.js b/web/core/js/common/components/canvas/ImageAdderPlugin.js new file mode 100644 index 0000000..6a94f0a --- /dev/null +++ b/web/core/js/common/components/canvas/ImageAdderPlugin.js @@ -0,0 +1,129 @@ +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class ImageAdderPlugin extends CanvasPlugin { + constructor(options = {}) { + super('ImageAdderPlugin'); + + this.options = { + ...options + }; + + this.canvasManager = null; + this.canvas = null; + + this.handleAddImage = this.handleAddImage.bind(this); + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.createUI(); + + this.attachEventListeners(); + } + + createUI() { + const html = ` +
+ + + + + + + + + + +
+ `; + + this.uiContainer = document.createElement('div'); + this.uiContainer.innerHTML = html; + this.uiContainer.className = 'image-adder-plugin-container'; + + this.uiContainer.style.padding = '10px'; + this.uiContainer.style.borderRadius = '5px'; + this.uiContainer.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)'; + this.uiContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'; + this.uiContainer.style.marginTop = '10px'; + + const pluginUIContainer = document.getElementById('pluginUIContainer'); + if (pluginUIContainer) { + pluginUIContainer.appendChild(this.uiContainer); + } else { + console.warn('pluginUIContainer element not found in the DOM.'); + } + } + + attachEventListeners() { + const addButton = this.uiContainer.querySelector('#add-image-button'); + addButton.addEventListener('click', this.handleAddImage); + } + + detachEventListeners() { + const addButton = this.uiContainer.querySelector('#add-image-button'); + addButton.removeEventListener('click', this.handleAddImage); + } + + handleAddImage() { + const colorPicker = this.uiContainer.querySelector('#image-color-picker'); + const widthInput = this.uiContainer.querySelector('#image-width-input'); + const heightInput = this.uiContainer.querySelector('#image-height-input'); + + const color = colorPicker.value; + const width = parseInt(widthInput.value, 10); + const height = parseInt(heightInput.value, 10); + + if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) { + alert('Please enter valid width and height values.'); + return; + } + + const rect = new fabric.Rect({ + width: width, + height: height, + fill: color, + left: this.canvas.getWidth() / 2, + top: this.canvas.getHeight() / 2, + originX: 'center', + originY: 'center', + selectable: true, + hasControls: true, + hasBorders: true, + }); + + this.canvas.add(rect); + this.canvas.setActiveObject(rect); + this.canvas.requestRenderAll(); + + this.canvasManager.emit('image:added', { + type: 'rectangle', + object: rect, + color: color, + width: width, + height: height, + }); + } + + destroy() { + if (this.uiContainer && this.uiContainer.parentNode) { + this.uiContainer.parentNode.removeChild(this.uiContainer); + } + this.detachEventListeners(); + } +} diff --git a/web/core/js/common/components/canvas/ImageCompareSliderPlugin.js b/web/core/js/common/components/canvas/ImageCompareSliderPlugin.js new file mode 100644 index 0000000..03d2547 --- /dev/null +++ b/web/core/js/common/components/canvas/ImageCompareSliderPlugin.js @@ -0,0 +1,571 @@ +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class ImageCompareSliderPlugin extends CanvasPlugin { + constructor(options = {}) { + super('ImageCompareSliderPlugin'); + + this.options = { + sliderColor: '#570d7b', + sliderWidth: 2, + handleRadius: 18, + handleStrokeWidth: 4, + handleStrokeColor: '#fff', + topMargin: 23, + bottomMargin: 23, + mode: 'Pairs', // 'Pairs', 'Replace', 'Multi' + ...options + }; + + this.init = this.init.bind(this); + this.destroy = this.destroy.bind(this); + this.onImageLoaded = this.onImageLoaded.bind(this); + this.onImageRemoved = this.onImageRemoved.bind(this); + this.onImageListUpdated = this.onImageListUpdated.bind(this); + this.onViewportChanged = this.onViewportChanged.bind(this); + this.updateSliderElements = this.updateSliderElements.bind(this); + this.updateClipPath = this.updateClipPath.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onMouseDoubleClick = this.onMouseDoubleClick.bind(this); + this.animateSlider = this.animateSlider.bind(this); + this.ensureSliderOnTop = this.ensureSliderOnTop.bind(this); + this.onAfterRender = this.onAfterRender.bind(this); + this.setupSliderWithImages = this.setupSliderWithImages.bind(this); + this.initializeMultiModeSelector = this.initializeMultiModeSelector.bind(this); + this.onMultiModeSelection = this.onMultiModeSelection.bind(this); + + this.canvasManager = null; + this.canvas = null; + this.images = []; + this.sliderLine = null; + this.sliderHandle = null; + this.animating = false; + this.pingPong = true; + this.moveRight = true; + this.isInitialized = false; + this.isDragging = false; + + this.multiModeUI = null; + this.imageListContainer = null; + this.compareButton = null; + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.canvasManager.on('image:loaded', this.onImageLoaded); + this.canvasManager.on('image:removed', this.onImageRemoved); + this.canvasManager.on('image:list:updated', this.onImageListUpdated); + this.canvasManager.on('viewport:changed', this.onViewportChanged); + + this.canvas.on('mouse:down', this.onMouseDown); + this.canvas.on('mouse:move', this.onMouseMove); + this.canvas.on('mouse:up', this.onMouseUp); + this.canvas.on('mouse:out', this.onMouseUp); + this.canvas.on('mouse:dblclick', this.onMouseDoubleClick); + + this.canvas.on('after:render', this.onAfterRender); + + if (this.options.mode === 'Multi') { + this.initializeMultiModeSelector(); + } + + console.log('ImageCompareSliderPlugin initialized'); + } + + onAfterRender() { + this.ensureSliderOnTop(); + } + + ensureSliderOnTop() { + if (!this.isReady()) return; + + const objects = this.canvas.getObjects(); + + const maxIndex = objects.length - 1; + + const lineIndex = objects.indexOf(this.sliderLine); + const handleIndex = objects.indexOf(this.sliderHandle); + + if (lineIndex !== maxIndex - 1) { + this.sliderLine.moveTo(maxIndex - 1); + console.log('sliderLine moved to top -1'); + } + if (handleIndex !== maxIndex) { + this.sliderHandle.moveTo(maxIndex); + console.log('sliderHandle moved to top'); + } + } + + isReady() { + return this.isInitialized && + this.sliderLine && + this.sliderHandle && + this.images.length === 2; + } + + onImageListUpdated({ images }) { + console.log('Image list updated:', images); + this.images = images; + this.updateComparisonImages(); + } + + onImageLoaded({ image, id }) { + console.log(`Image loaded: ID=${id}`); + if (this.options.mode === 'Pairs') { + if (this.images.length > 1) { + const imagesToRemove = this.images.slice(0, this.images.length - 1); + imagesToRemove.forEach(img => { + console.log(`Removing image ID=${img.id} to maintain 'Pairs' mode`); + this.canvasManager.emit('image:remove', { id: img.id }); + }); + } + } else if (this.options.mode === 'Replace') { + if (this.images.length > 2) { + const imagesToRemove = this.images.slice(0, this.images.length - 2); + imagesToRemove.forEach(img => { + console.log(`Removing image ID=${img.id} to maintain 'Replace' mode`); + this.canvasManager.emit('image:remove', { id: img.id }); + }); + } + } + } + + onImageRemoved({ id }) { + console.log(`Image removed: ID=${id}`); + this.images = this.images.filter(img => img.id !== id); + if (this.images.length < 2) { + this.destroySlider(); + } else { + this.updateComparisonImages(); + } + } + + updateComparisonImages() { + console.log('Updating comparison images based on current mode:', this.options.mode); + if (this.images.length < 2) { + this.destroySlider(); + return; + } + + if (this.options.mode === 'Multi') { + this.updateMultiModeUI(); + + } else { + let img1, img2; + + if (this.options.mode === 'Pairs') { + + img1 = this.images[this.images.length - 2]; + img2 = this.images[this.images.length - 1]; + } else if (this.options.mode === 'Replace') { + img1 = this.images[this.images.length - 2]; + img2 = this.images[this.images.length - 1]; + } + if (img1 && img2) { + this.setupSliderWithImages(img1.id, img2.id); + } + } + } + + setupSliderWithImages(id1, id2) { + console.log(`Setting up slider with images ID1=${id1}, ID2=${id2}`); + const imageData1 = this.canvasManager.getImageById(id1); + const imageData2 = this.canvasManager.getImageById(id2); + + if (!imageData1 || !imageData2) { + console.warn('One or both images not found on canvas.'); + return; + } + + this.baseImage = imageData1; + this.comparisonImage = imageData2; + this.initializeSlider(); + } + + initializeSlider() { + if (!this.baseImage || !this.comparisonImage) return; + + this.cleanupSliderElements(); + + const { sliderColor, sliderWidth, handleRadius, handleStrokeWidth, handleStrokeColor } = this.options; + + try { + this.sliderLine = new fabric.Line( + [this.canvas.width / 2, 0, this.canvas.width / 2, this.canvas.height], + { + id: 'sliderLine', + stroke: sliderColor, + strokeWidth: sliderWidth, + selectable: false, + hasControls: false, + evented: false, + originX: 'center', + visible: true + } + ); + + this.sliderHandle = new fabric.Circle({ + id: 'sliderHandle', + left: this.canvas.width / 2, + top: this.canvas.height / 2, + radius: handleRadius, + fill: sliderColor, + stroke: handleStrokeColor, + strokeWidth: handleStrokeWidth, + originX: 'center', + originY: 'center', + selectable: true, + hasBorders: false, + hasControls: false, + evented: true, + hoverCursor: 'move', + perPixelTargetFind: true + }); + + this.canvas.add(this.sliderLine); + this.canvas.add(this.sliderHandle); + + this.ensureSliderOnTop(); + + this.updateClipPath(); + this.isInitialized = true; + + console.log('Slider initialized successfully'); + this.canvasManager.emit('slider:initialized', { + baseImage: this.baseImage, + comparisonImage: this.comparisonImage + }); + } catch (error) { + console.error('Error initializing slider:', error); + this.cleanupSliderElements(); + } + } + + updateSliderElements() { + if (!this.isReady()) return; + + const zoom = this.canvas.getZoom(); + + this.sliderLine.set({ + x1: this.sliderHandle.left, + x2: this.sliderHandle.left, + y1: 0, + y2: this.canvas.height + }); + + const scaledRadius = this.options.handleRadius / zoom; + const scaledStroke = this.options.handleStrokeWidth / zoom; + + this.sliderHandle.set({ + radius: scaledRadius, + strokeWidth: scaledStroke + }); + + this.ensureSliderOnTop(); + } + + updateClipPath() { + if (!this.isReady()) return; + + try { + const imageWidth = this.comparisonImage.getScaledWidth(); + const leftEdge = this.comparisonImage.left - imageWidth / 2; + + this.comparisonImage.clipPath = new fabric.Rect({ + originX: 'left', + originY: 'top', + left: leftEdge, + top: 0, + width: this.sliderHandle.left - leftEdge, + height: this.canvas.height, + absolutePositioned: true + }); + + this.canvas.requestRenderAll(); + console.log('Clip path updated'); + } catch (error) { + console.error('Error updating clip path:', error); + } + } + + onViewportChanged({ transform }) { + if (!this.isReady()) return; + this.updateSliderElements(); + this.updateClipPath(); + this.ensureSliderOnTop(); + } + + onMouseDown(event) { + if (!this.isReady()) return; + + const pointer = this.canvas.getPointer(event.e); + const zoom = this.canvas.getZoom(); + const handleRadius = this.options.handleRadius / zoom; + + const dx = pointer.x - this.sliderHandle.left; + const dy = pointer.y - this.sliderHandle.top; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance <= handleRadius * 1.5) { + this.isDragging = true; + this.canvas.defaultCursor = 'move'; + this.ensureSliderOnTop(); + + event.e.preventDefault(); + event.e.stopPropagation(); + console.log('Slider handle drag started'); + } + } + + onMouseMove(event) { + if (!this.isReady() || !this.isDragging) return; + + const pointer = this.canvas.getPointer(event.e); + + const imageWidth = this.baseImage.getScaledWidth(); + const leftEdge = this.baseImage.left - imageWidth / 2; + const rightEdge = this.baseImage.left + imageWidth / 2; + + const newX = Math.max(leftEdge, Math.min(pointer.x, rightEdge)); + + this.sliderHandle.set({ + left: newX, + top: Math.max( + this.options.topMargin, + Math.min(pointer.y, this.canvas.height - this.options.bottomMargin) + ) + }); + + this.updateSliderElements(); + this.updateClipPath(); + this.ensureSliderOnTop(); + + this.canvas.requestRenderAll(); + event.e.preventDefault(); + event.e.stopPropagation(); + console.log(`Slider handle moved to (${newX}, ${this.sliderHandle.top})`); + } + + onMouseUp(event) { + if (this.isDragging) { + this.isDragging = false; + this.canvas.defaultCursor = 'default'; + this.ensureSliderOnTop(); + this.canvas.requestRenderAll(); + + if (event.e) { + event.e.preventDefault(); + event.e.stopPropagation(); + } + console.log('Slider handle drag ended'); + } + } + + onMouseDoubleClick(e) { + if (!this.isReady()) return; + + const pointer = this.canvas.getPointer(e.e); + const zoom = this.canvas.getZoom(); + const handleRadius = this.options.handleRadius / zoom; + + const dx = pointer.x - this.sliderHandle.left; + const dy = pointer.y - this.sliderHandle.top; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance <= handleRadius * 1.5) { + this.sliderLine.visible = !this.sliderLine.visible; + this.canvas.requestRenderAll(); + console.log(`Slider line visibility toggled to ${this.sliderLine.visible}`); + } else if (!this.animating) { + this.animateSlider(); + } else { + this.animating = false; + console.log('Slider animation stopped'); + } + } + + animateSlider() { + if (!this.isReady()) return; + + this.animating = true; + const imageWidth = this.baseImage.getScaledWidth(); + const leftEdge = this.baseImage.left - imageWidth / 2; + const rightEdge = this.baseImage.left + imageWidth / 2; + + const startPosition = this.sliderHandle.left; + const endValue = this.moveRight ? rightEdge : leftEdge; + const distance = Math.abs(endValue - startPosition); + const duration = (distance / (rightEdge - leftEdge)) * 1500; + + console.log(`Animating slider from ${startPosition} to ${endValue} over ${duration}ms`); + + fabric.util.animate({ + startValue: startPosition, + endValue: endValue, + duration: duration, + onChange: (value) => { + if (!this.animating || !this.isReady()) return; + const adjustedValue = Math.max(leftEdge, Math.min(value, rightEdge)); + this.sliderHandle.set({ left: adjustedValue }); + this.updateSliderElements(); + this.updateClipPath(); + this.ensureSliderOnTop(); + this.canvas.requestRenderAll(); + console.log(`Slider handle animating to (${adjustedValue}, ${this.sliderHandle.top})`); + }, + onComplete: () => { + if (this.pingPong && this.animating && this.isReady()) { + this.moveRight = !this.moveRight; + this.animateSlider(); + } else { + this.animating = false; + this.updateClipPath(); + this.ensureSliderOnTop(); + console.log('Slider animation completed'); + } + } + }); + } + + cleanupSliderElements() { + if (this.sliderLine) { + this.canvas.remove(this.sliderLine); + this.sliderLine = null; + console.log('Slider line removed'); + } + if (this.sliderHandle) { + this.canvas.remove(this.sliderHandle); + this.sliderHandle = null; + console.log('Slider handle removed'); + } + if (this.comparisonImage && this.comparisonImage.clipPath) { + this.comparisonImage.clipPath = null; + console.log('Clip path removed from comparison image'); + } + this.isInitialized = false; + this.isDragging = false; + } + + destroySlider() { + console.log('Destroying slider'); + this.cleanupSliderElements(); + + if (this.options.mode === 'Multi') { + this.resetMultiModeSelection(); + } + } + + destroy() { + this.animating = false; + + this.canvasManager.off('image:loaded', this.onImageLoaded); + this.canvasManager.off('image:removed', this.onImageRemoved); + this.canvasManager.off('image:list:updated', this.onImageListUpdated); + this.canvasManager.off('viewport:changed', this.onViewportChanged); + + this.canvas.off('mouse:down', this.onMouseDown); + this.canvas.off('mouse:move', this.onMouseMove); + this.canvas.off('mouse:up', this.onMouseUp); + this.canvas.off('mouse:out', this.onMouseUp); + this.canvas.off('mouse:dblclick', this.onMouseDoubleClick); + this.canvas.off('after:render', this.onAfterRender); + + this.destroySlider(); + + if (this.multiModeUI) { + this.multiModeUI.remove(); + this.multiModeUI = null; + } + + this.baseImage = null; + this.comparisonImage = null; + this.images = []; + + this.canvas.requestRenderAll(); + console.log('ImageCompareSliderPlugin destroyed'); + } + + + initializeMultiModeSelector() { + this.multiModeUI = document.createElement('div'); + this.multiModeUI.className = 'multi-mode-selector'; + this.multiModeUI.style.position = 'absolute'; + this.multiModeUI.style.top = '10px'; + this.multiModeUI.style.right = '10px'; + this.multiModeUI.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'; + this.multiModeUI.style.padding = '10px'; + this.multiModeUI.style.borderRadius = '5px'; + this.multiModeUI.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; + this.multiModeUI.style.zIndex = '1000'; + + const title = document.createElement('h3'); + title.textContent = 'Select Images to Compare'; + title.style.margin = '0 0 10px 0'; + this.multiModeUI.appendChild(title); + + this.imageListContainer = document.createElement('div'); + this.imageListContainer.className = 'image-list-container'; + this.imageListContainer.style.maxHeight = '200px'; + this.imageListContainer.style.overflowY = 'auto'; + this.multiModeUI.appendChild(this.imageListContainer); + + this.compareButton = document.createElement('button'); + this.compareButton.textContent = 'Compare Selected'; + this.compareButton.disabled = true; + this.compareButton.style.marginTop = '10px'; + this.multiModeUI.appendChild(this.compareButton); + + const pluginUIContainer = document.getElementById('pluginUIContainer'); + if (pluginUIContainer) { + pluginUIContainer.appendChild(this.multiModeUI); + } else { + console.warn('pluginUIContainer element not found in the DOM.'); + } + + this.imageListContainer.addEventListener('change', this.onMultiModeSelection); + this.compareButton.addEventListener('click', () => { + const selectedIds = Array.from(this.imageListContainer.querySelectorAll('input[name="compareSelect"]:checked')) + .map(input => input.value); + if (selectedIds.length === 2) { + this.setupSliderWithImages(selectedIds[0], selectedIds[1]); + console.log(`Comparing images: ${selectedIds[0]} and ${selectedIds[1]}`); + } + }); + + this.updateMultiModeUI(); + } + + updateMultiModeUI() { + if (this.options.mode !== 'Multi') return; + + this.imageListContainer.innerHTML = ''; + + this.images.forEach(img => { + const label = document.createElement('label'); + label.style.display = 'block'; + label.style.marginBottom = '5px'; + label.innerHTML = ` + + Image ${img.id} + `; + this.imageListContainer.appendChild(label); + }); + } + + onMultiModeSelection() { + const checked = this.imageListContainer.querySelectorAll('input[name="compareSelect"]:checked').length; + this.compareButton.disabled = checked !== 2; + } + + resetMultiModeSelection() { + if (this.multiModeUI) { + this.multiModeUI.querySelectorAll('input[name="compareSelect"]').forEach(input => { + input.checked = false; + }); + this.compareButton.disabled = true; + } + } +} diff --git a/web/core/js/common/components/canvas/ImageLoaderPlugin.js b/web/core/js/common/components/canvas/ImageLoaderPlugin.js new file mode 100644 index 0000000..5d40df6 --- /dev/null +++ b/web/core/js/common/components/canvas/ImageLoaderPlugin.js @@ -0,0 +1,479 @@ + +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class ImageLoaderPlugin extends CanvasPlugin { + constructor(options = {}) { + super('ImageLoaderPlugin'); + + this.options = { + mode: 'Single', // 'Single' or 'Multi' + ...options + }; + + this.canvasManager = null; + this.canvas = null; + + this.uiContainer = null; + this.toggleButton = null; + this.loadButton = null; + this.fileInput = null; + + this.loadedImages = []; + this.originalImages = {}; + + this.onImageDrop = this.onImageDrop.bind(this); + this.onDoubleClick = this.onDoubleClick.bind(this); + this.onToggleMode = this.onToggleMode.bind(this); + this.onCanvasResized = this.onCanvasResized.bind(this); + this.onDragOver = this.onDragOver.bind(this); + this.onImageRemove = this.onImageRemove.bind(this); + this.onLoadButtonClick = this.onLoadButtonClick.bind(this); + this.onFileInputChange = this.onFileInputChange.bind(this); + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.createUI(); + + this.attachEventListeners(); + + this.canvasManager.on('canvas:resized', this.onCanvasResized); + + this.canvasManager.on('image:remove', this.onImageRemove); + } + + createUI() { + if (!document.querySelector('style[data-plugin="image-loader"]')) { + const styleSheet = document.createElement('style'); + styleSheet.setAttribute('data-plugin', 'image-loader'); + styleSheet.textContent = ` + .il-container * { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } + + .il-container { + display: inline-flex; + padding: 0.5rem; + gap: 0.5rem; + } + + .il-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem; + background: var(--color-button-primary, #007BFF); + border: none; + cursor: pointer; + transition: background 0.2s; + color: var(--color-primary-text, #FFFFFF); + height: 36px; + } + + .il-button:hover { + background: var(--color-button-primary-hover, #0056b3); + } + + .il-button svg { + width: 1.25rem; + height: 1.25rem; + } + + .il-toggle-button[data-mode="Multi"] { + border: 1px dashed var(--color-button-secondary-text-active, #FFC107); + } + + .il-hidden-file-input { + display: none; + } + /* Temporary fix for hidden future functionality */ + #toggleModeBtn { + display: none; + } + `; + document.head.appendChild(styleSheet); + } + + this.uiContainer = document.createElement('div'); + this.uiContainer.className = 'il-container'; + + this.renderToggleButton(); + + this.renderLoadButton(); + + this.createHiddenFileInput(); + + const pluginUIContainer = document.getElementById('pluginUIContainer'); + if (pluginUIContainer) { + pluginUIContainer.appendChild(this.uiContainer); + } + } + + renderToggleButton() { + this.toggleButton = document.createElement('button'); + this.toggleButton.className = 'il-button il-toggle-button'; + this.toggleButton.id = 'toggleModeBtn'; + this.toggleButton.setAttribute('data-mode', this.options.mode); + this.toggleButton.innerHTML = ` + + + + ${this.options.mode} + `; + this.toggleButton.addEventListener('click', this.onToggleMode); + this.uiContainer.appendChild(this.toggleButton); + } + + renderLoadButton() { + this.loadButton = document.createElement('button'); + this.loadButton.className = 'il-button il-load-button'; + this.loadButton.id = 'loadImageBtn'; + this.loadButton.innerHTML = ` + + + + + `; + this.loadButton.addEventListener('click', this.onLoadButtonClick); + this.uiContainer.appendChild(this.loadButton); + } + + createHiddenFileInput() { + this.fileInput = document.createElement('input'); + this.fileInput.type = 'file'; + this.fileInput.accept = 'image/*'; + this.fileInput.className = 'il-hidden-file-input'; + this.fileInput.addEventListener('change', this.onFileInputChange); + document.body.appendChild(this.fileInput); + } + + onToggleMode() { + // Toggle between 'Single' and 'Multi' modes + this.options.mode = this.options.mode === 'Single' ? 'Multi' : 'Single'; + this.toggleButton.setAttribute('data-mode', this.options.mode); + this.toggleButton.innerHTML = ` + + + + ${this.options.mode} + `; + + if (this.options.mode === 'Single' && this.loadedImages.length > 1) { + const imagesToRemove = this.loadedImages.slice(0, -1); + imagesToRemove.forEach(({ id }) => { + this.removeImageById(id); + }); + } + } + + onLoadButtonClick() { + if (this.fileInput) { + this.fileInput.click(); + } + } + + onFileInputChange(event) { + const file = event.target.files[0]; + if (file) { + this.handleImageFile(file); + } + event.target.value = ''; + } + + attachEventListeners() { + this.canvas.upperCanvasEl.addEventListener('dragover', this.onDragOver); + this.canvas.upperCanvasEl.addEventListener('drop', this.onImageDrop); + } + + detachEventListeners() { + this.canvas.upperCanvasEl.removeEventListener('dragover', this.onDragOver); + this.canvas.upperCanvasEl.removeEventListener('drop', this.onImageDrop); + + if (this.toggleButton) { + this.toggleButton.removeEventListener('click', this.onToggleMode); + } + + if (this.loadButton) { + this.loadButton.removeEventListener('click', this.onLoadButtonClick); + } + + if (this.fileInput) { + this.fileInput.removeEventListener('change', this.onFileInputChange); + } + + this.canvasManager.off('canvas:resized', this.onCanvasResized); + + this.canvasManager.off('image:remove', this.onImageRemove); + } + + onCanvasResized({ width, height }) { + // Recalculate and apply scale to all images based on original dimensions + this.loadedImages.forEach(({ imageObject, borderRect }) => { + const img = imageObject; + const border = borderRect; + + const strokeWidth = border.strokeWidth || 2; + const desiredWidth = width - strokeWidth * 2; + const desiredHeight = height - strokeWidth * 2; + + const imgAspect = img.width / img.height; + const canvasAspect = width / height; + + let scaleFactor; + if (imgAspect > canvasAspect) { + scaleFactor = desiredWidth / img.width; + } else { + scaleFactor = desiredHeight / img.height; + } + + // Apply uniform scale to maintain aspect ratio + img.set({ + scaleX: scaleFactor, + scaleY: scaleFactor, + left: width / 2, + top: height / 2, + originX: 'center', + originY: 'center', + selectable: false, + evented: false, + }); + + border.set({ + scaleX: scaleFactor, + scaleY: scaleFactor, + left: width / 2, + top: height / 2, + originX: 'center', + originY: 'center', + }); + }); + + this.canvas.requestRenderAll(); + } + + onDragOver(event) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + } + + onImageDrop(event) { + event.preventDefault(); + const files = event.dataTransfer.files; + if (files && files[0]) { + this.handleImageFile(files[0]); + } + } + + onDoubleClick(event) { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.style.display = 'none'; + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + this.handleImageFile(file); + } + }); + document.body.appendChild(fileInput); + fileInput.click(); + document.body.removeChild(fileInput); + } + + handleImageFile(file) { + if (!file.type.startsWith('image/')) { + alert('Please drop a valid image file.'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + fabric.Image.fromURL(e.target.result, (img) => { + const uniqueId = `Image_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + img.set({ + id: uniqueId, + scaleX: 1, + scaleY: 1, + left: this.canvas.getWidth() / 2, + top: this.canvas.getHeight() / 2, + originX: 'center', + originY: 'center', + selectable: false, + evented: false, + }); + + const originalWidth = img.width; + const originalHeight = img.height; + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = img.width; + tempCanvas.height = img.height; + const tempCtx = tempCanvas.getContext('2d'); + + img.clone((clonedImg) => { + tempCtx.drawImage(clonedImg._element, 0, 0); + const originalDataURL = tempCanvas.toDataURL('image/png'); + this.originalImages[uniqueId] = originalDataURL; + }); + + const canvasWidth = this.canvas.getWidth(); + const canvasHeight = this.canvas.getHeight(); + const imgAspect = originalWidth / originalHeight; + const canvasAspect = canvasWidth / canvasHeight; + + const strokeWidth = 2; + const desiredWidth = canvasWidth - strokeWidth * 2; + const desiredHeight = canvasHeight - strokeWidth * 2; + let scaleFactor; + if (imgAspect > canvasAspect) { + scaleFactor = desiredWidth / originalWidth; + } else { + scaleFactor = desiredHeight / originalHeight; + } + + img.set({ + scaleX: scaleFactor, + scaleY: scaleFactor, + left: canvasWidth / 2, + top: canvasHeight / 2, + originX: 'center', + originY: 'center', + selectable: false, + evented: false, + }); + + const borderRect = new fabric.Rect({ + width: img.getScaledWidth(), + height: img.getScaledHeight(), + originX: 'center', + originY: 'center', + left: canvasWidth / 2, + top: canvasHeight / 2, + fill: 'transparent', + // stroke: 'rgba(255, 0, 0, 0.5)', + strokeWidth: strokeWidth, + strokeDashArray: [10, 5], + strokeUniform: true, + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + lockMovementX: true, + lockMovementY: true, + }); + + this.canvas.add(img); + this.canvas.add(borderRect); + + borderRect.bringToFront(); + + this.loadedImages.push({ + id: uniqueId, + imageObject: img, + borderRect: borderRect, + originalWidth: originalWidth, + originalHeight: originalHeight, + scaleFactor: scaleFactor, + }); + + if (this.options.mode === 'Single' && this.loadedImages.length > 1) { + const imagesToRemove = this.loadedImages.slice(0, -1); + imagesToRemove.forEach(({ id }) => { + this.removeImageById(id); + }); + } + + this.canvasManager.emit('image:loaded', { + image: img, + id: uniqueId, + originalWidth: originalWidth, + originalHeight: originalHeight, + scaleFactor: scaleFactor, + }); + + this.canvasManager.emit('image:list:updated', { + images: this.loadedImages.map(img => ({ + id: img.id, + left: img.imageObject.left, + top: img.imageObject.top, + scaleX: img.imageObject.scaleX, + scaleY: img.imageObject.scaleY, + })), + }); + }, { crossOrigin: 'anonymous' }); + }; + reader.readAsDataURL(file); + } + + getOriginalImage(id) { + if (this.loadedImages.length === 0) { + console.warn('No images loaded.'); + return null; + } + + if (id) { + return this.originalImages[id] || null; + } + + const latestImage = this.loadedImages[this.loadedImages.length - 1]; + return this.originalImages[latestImage.id] || null; + } + + removeImageById(id) { + const imageIndex = this.loadedImages.findIndex(img => img.id === id); + if (imageIndex !== -1) { + const { imageObject, borderRect } = this.loadedImages[imageIndex]; + this.canvas.remove(imageObject); + this.canvas.remove(borderRect); + this.loadedImages.splice(imageIndex, 1); + + delete this.originalImages[id]; + + console.log(`ImageLoaderPlugin: Removed image with ID=${id}`); + this.canvasManager.emit('image:removed', { id }); + + this.canvasManager.emit('image:list:updated', { + images: this.loadedImages.map(img => ({ + id: img.id, + left: img.imageObject.left, + top: img.imageObject.top, + scaleX: img.imageObject.scaleX, + scaleY: img.imageObject.scaleY, + })), + }); + + if (this.options.mode === 'Single' && this.loadedImages.length === 0) { + this.canvas.clear(); + console.log('ImageLoaderPlugin: Canvas cleared in Single mode'); + } + + this.canvas.requestRenderAll(); + } else { + console.warn(`ImageLoaderPlugin: Image with ID=${id} not found`); + } + } + + onImageRemove({ id }) { + console.log(`ImageLoaderPlugin: Received request to remove image ID=${id}`); + this.removeImageById(id); + } + + destroy() { + if (this.uiContainer && this.uiContainer.parentNode) { + this.uiContainer.parentNode.removeChild(this.uiContainer); + } + this.detachEventListeners(); + this.loadedImages.forEach(({ imageObject, borderRect }) => { + this.canvas.remove(imageObject); + this.canvas.remove(borderRect); + }); + this.loadedImages = []; + this.originalImages = {}; + this.canvas.requestRenderAll(); + } +} diff --git a/web/core/js/common/components/canvas/MaskBrushPlugin.js b/web/core/js/common/components/canvas/MaskBrushPlugin.js new file mode 100644 index 0000000..14490e3 --- /dev/null +++ b/web/core/js/common/components/canvas/MaskBrushPlugin.js @@ -0,0 +1,1271 @@ +import { CustomBrushPlugin } from './CustomBrushPlugin.js'; + +export class MaskBrushPlugin extends CustomBrushPlugin { + constructor(options = {}) { + super({ ...options, name: 'MaskBrushPlugin' }); + + this.masks = []; + this.currentMask = null; + this.maskStrokeHistory = {}; // { maskName: { undoStack: [], redoStack: [] } } + this.brushIcon = '/core/media/ui/double-face-mask.png'; + + this.onAddMask = this.onAddMask.bind(this); + this.onChangeMask = this.onChangeMask.bind(this); + this.onChangeMaskColor = this.onChangeMaskColor.bind(this); + this.onMoveMaskUp = this.onMoveMaskUp.bind(this); + this.onMoveMaskDown = this.onMoveMaskDown.bind(this); + this.onApplyColorToExistingMaskChange = this.onApplyColorToExistingMaskChange.bind(this); + this.onHandleSave = this.onHandleSave.bind(this); + this.onRemoveMask = this.onRemoveMask.bind(this); + this.onImageLoaded = this.onImageLoaded.bind(this); + this.onCanvasStateChanged = this.onCanvasStateChanged.bind(this); + this.onHandleSaveFromCanvas = this.onHandleSaveFromCanvas.bind(this); + this.onWindowResize = this.onWindowResize.bind(this); + this.onUndoMaskStroke = this.onUndoMaskStroke.bind(this); + this.onRedoMaskStroke = this.onRedoMaskStroke.bind(this); + } + + init(canvasManager) { + super.init(canvasManager); + + if (this.brushOpacityInput && this.brushOpacityInput.parentElement) { + this.brushOpacityInput.parentElement.style.display = 'none'; + } + + this.extendUI(); + + this.attachAdditionalEventListeners(); + + this.canvasManager.on('image:loaded', this.onImageLoaded); + this.canvasManager.on('canvas:state:changed', this.onCanvasStateChanged); + this.canvasManager.on('undo:mask:stroke', this.onUndoMaskStroke); + this.canvasManager.on('redo:mask:stroke', this.onRedoMaskStroke); + + window.addEventListener('resize', this.onWindowResize); + + this.canvasManager.on('save:trigger', this.onHandleSaveFromCanvas); + } + + extendUI() { + const styleSheet = document.createElement('style'); + styleSheet.textContent = ` + + `; + document.head.appendChild(styleSheet); + + const temp = document.createElement('div'); + temp.innerHTML = ` +
+
+ + +
+
+ +
+ + +
+ + +
+ +
+ + +
+ + + + + +
+ `; + + while (temp.firstChild) { + this.uiContainer.appendChild(temp.firstChild); + } + + this.addMaskBtn = this.uiContainer.querySelector('#addMaskBtn'); + this.maskList = this.uiContainer.querySelector('#maskList'); + this.applyColorToMaskCheckbox = this.uiContainer.querySelector('#applyColorToMaskCheckbox'); + this.moveMaskUpBtn = this.uiContainer.querySelector('#moveMaskUpBtn'); + this.moveMaskDownBtn = this.uiContainer.querySelector('#moveMaskDownBtn'); + this.removeMaskBtn = this.uiContainer.querySelector('#removeMaskBtn'); + this.saveOptionsSelect = this.uiContainer.querySelector('#saveOptionsSelect'); + this.saveMaskBtn = this.uiContainer.querySelector('#saveMaskBtn'); + + this.saveOptions = [ + { + value: 'saveMaskAlphaOnImage', + text: 'Mask As Alpha on Image', + handler: () => this.saveMaskAlphaOnImage(), + exportFunction: () => this.exportMaskAlphaOnImage(), + show: true, + }, + { + value: 'saveAllMasksAlphaOnImage', + text: 'All Masks As Alpha on Image', + handler: () => this.saveAllMasksAlphaOnImage(), + exportFunction: () => this.exportAllMasksAlphaOnImage(), + show: true, + }, + { + value: 'saveAllMasksCombinedAlphaOnImage', + text: 'All Masks As Alpha Combined on Image', + handler: () => this.saveAllMasksCombinedAlphaOnImage(), + exportFunction: () => this.exportAllMasksCombinedAlphaOnImage(), + show: true, + }, + { + value: 'saveMask', + text: 'Mask', + handler: () => this.saveMask(), + exportFunction: () => this.exportMask(), + show: true, + }, + { + value: 'saveAllMasks', + text: 'All Masks', + handler: () => this.saveAllMasks(), + exportFunction: () => this.exportMasksImage(), + show: true, + }, + { + value: 'saveAllMasksCombined', + text: 'All Masks Combined', + handler: () => this.saveAllMasksCombined(), + exportFunction: () => this.exportAllMasksCombined(), + show: true, + }, + { + value: 'saveAllMasksCombinedBW', + text: 'All Masks Combined (B&W)', + handler: () => this.saveAllMasksCombinedBlackWhite(), + exportFunction: () => this.exportMasksCombinedBlackWhite(), + show: true, + }, + { + value: 'saveMaskOnImage', + text: 'Mask on Image', + handler: () => this.saveMaskOnImage(), + exportFunction: () => this.exportMaskOnImage(), + show: true, + }, + { + value: 'saveAllMasksOnImage', + text: 'All Masks on Image', + handler: () => this.saveAllMasksOnImage(), + exportFunction: () => this.exportAllMasksOnImage(), + show: true, + }, + { + value: 'saveAllMasksCombinedOnImage', + text: 'All Masks Combined on Image', + handler: () => this.saveAllMasksCombinedOnImage(), + exportFunction: () => this.exportAllMasksCombinedOnImage(), + show: true, + }, + { + value: 'saveAllMasksCombinedBWOnImage', + text: 'All Masks Combined (B&W) on Image', + handler: () => this.saveAllMasksCombinedBlackWhiteOnImage(), + exportFunction: () => this.exportAllMasksCombinedBlackWhiteOnImage(), + show: true, + }, + ]; + + this.saveOptions.forEach(optionData => { + if (optionData.show) { + const option = document.createElement('option'); + option.value = optionData.value; + option.text = optionData.text; + this.saveOptionsSelect.add(option); + } + }); + } + + + attachAdditionalEventListeners() { + this.addMaskBtn.addEventListener('click', this.onAddMask); + this.maskList.addEventListener('change', this.onChangeMask); + this.colorPicker.addEventListener('input', this.onChangeMaskColor); + this.applyColorToMaskCheckbox.addEventListener('change', this.onApplyColorToExistingMaskChange); + this.moveMaskUpBtn.addEventListener('click', this.onMoveMaskUp); + this.moveMaskDownBtn.addEventListener('click', this.onMoveMaskDown); + this.removeMaskBtn.addEventListener('click', this.onRemoveMask); + + this.saveMaskBtn.addEventListener('click', this.onHandleSave); + } + + detachAdditionalEventListeners() { + this.addMaskBtn.removeEventListener('click', this.onAddMask); + this.maskList.removeEventListener('change', this.onChangeMask); + this.colorPicker.removeEventListener('input', this.onChangeMaskColor); + this.applyColorToMaskCheckbox.removeEventListener('change', this.onApplyColorToExistingMaskChange); + this.moveMaskUpBtn.removeEventListener('click', this.onMoveMaskUp); + this.moveMaskDownBtn.removeEventListener('click', this.onMoveMaskDown); + this.removeMaskBtn.removeEventListener('click', this.onRemoveMask); + + this.saveMaskBtn.removeEventListener('click', this.onHandleSave); + + this.canvasManager.off('image:loaded', this.onImageLoaded); + this.canvasManager.off('canvas:state:changed', this.onCanvasStateChanged); + this.canvasManager.off('undo:mask:stroke', this.onUndoMaskStroke); + this.canvasManager.off('redo:mask:stroke', this.onRedoMaskStroke); + + window.removeEventListener('resize', this.onWindowResize); + + this.canvasManager.off('save:trigger', this.onHandleSaveFromCanvas); + } + + onWindowResize() { + if (typeof this.onImageModified === 'function') { + this.onImageModified(); + } else { + console.error('onImageModified method is not defined or not bound correctly.'); + } + } + + onImageLoaded(event) { + const { image, originalWidth, originalHeight, scaleFactor } = event; + + this.imageObject = image; + this.imageOriginalWidth = originalWidth; + this.imageOriginalHeight = originalHeight; + this.imageScaleFactor = scaleFactor; + + this.imageObject.on('modified', this.onImageModified); + + this.masks.forEach(mask => { + this.canvas.remove(mask.fabricImage); + }); + this.masks = []; + this.currentMask = null; + this.maskList.options.length = 0; + + this.maskStrokeHistory = {}; + + this.onAddMask(); + } + + onImageModified() { + if (!this.imageObject) { + console.error('Image object is not defined.'); + return; + } + + this.masks.forEach(mask => { + mask.fabricImage.set({ + scaleX: this.imageObject.scaleX, + scaleY: this.imageObject.scaleY, + left: this.imageObject.left, + top: this.imageObject.top, + originX: this.imageObject.originX, + originY: this.imageObject.originY, + }); + mask.fabricImage.setCoords(); + }); + this.canvas.renderAll(); + } + + onCanvasStateChanged() { + this.masks.forEach(mask => { + if (mask.fabricImage) { + mask.fabricImage.selectable = !this.drawingMode; + mask.fabricImage.evented = !this.drawingMode; + } + }); + this.canvas.renderAll(); + } + + onAddMask() { + if (!this.imageObject) { + alert('Please load an image before adding masks.'); + return; + } + + const maskName = `Mask ${this.maskList.options.length + 1}`; + const color = this.brushColor; + + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = this.imageOriginalWidth; + maskCanvas.height = this.imageOriginalHeight; + const maskCtx = maskCanvas.getContext('2d'); + + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + + const maskFabricImage = new fabric.Image(maskCanvas, { + left: this.imageObject.left, + top: this.imageObject.top, + originX: this.imageObject.originX, + originY: this.imageObject.originY, + selectable: false, + evented: false, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + hasControls: false, + hasBorders: false, + hoverCursor: 'default', + opacity: this.brushOpacity, + scaleX: this.imageObject.scaleX, + scaleY: this.imageObject.scaleY, + willReadFrequently: true, + }); + + this.canvas.add(maskFabricImage); + maskFabricImage.bringToFront(); + + const mask = { + name: maskName, + color: color, + fabricImage: maskFabricImage, + canvasEl: maskCanvas, + ctx: maskCtx, + }; + + this.masks.push(mask); + this.currentMask = mask; + + this.maskStrokeHistory[maskName] = { + undoStack: [], + redoStack: [] + }; + + const option = document.createElement('option'); + option.value = maskName; + option.text = maskName; + option.dataset.color = color; + this.maskList.add(option); + this.maskList.value = maskName; + + this.updateBrushColorAndCursor(); + } + + onChangeMask() { + const selectedOption = this.maskList.options[this.maskList.selectedIndex]; + const maskName = selectedOption.value; + const color = selectedOption.dataset.color; + + this.currentMask = this.masks.find(m => m.name === maskName); + this.brushColor = color; + + this.colorPicker.value = color; + + this.updateBrushColorAndCursor(); + } + + onChangeMaskColor() { + const color = this.brushColor = this.colorPicker.value; + const selectedOption = this.maskList.options[this.maskList.selectedIndex]; + if (selectedOption) { + selectedOption.dataset.color = color; + } + + const applyToExistingMask = this.applyColorToMaskCheckbox.checked; + + if (this.currentMask) { + this.currentMask.color = color; + + if (applyToExistingMask) { + this.applyColorToExistingMask(this.currentMask, color); + this.canvas.renderAll(); + } + + this.updateBrushColorAndCursor(); + } + } + + applyColorToExistingMask(mask, newColor) { + if (!mask || !mask.ctx) { + console.error('Cannot apply color: mask or its context is null.'); + return; + } + + const ctx = mask.ctx; + const canvasEl = mask.canvasEl; + const imageData = ctx.getImageData(0, 0, canvasEl.width, canvasEl.height); + const data = imageData.data; + + const tempElem = document.createElement('div'); + tempElem.style.color = newColor; + document.body.appendChild(tempElem); + const rgbColor = window.getComputedStyle(tempElem).color; + document.body.removeChild(tempElem); + + const rgbMatch = rgbColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); + if (!rgbMatch) { + console.error('Failed to parse new color.'); + return; + } + + const rNew = parseInt(rgbMatch[1]); + const gNew = parseInt(rgbMatch[2]); + const bNew = parseInt(rgbMatch[3]); + + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + if (alpha > 0) { + data[i] = rNew; // Red + data[i + 1] = gNew; // Green + data[i + 2] = bNew; // Blue + } + } + + ctx.putImageData(imageData, 0, 0); + mask.fabricImage.dirty = true; + } + + onApplyColorToExistingMaskChange() { + //can be used if additional actions are needed when the checkbox state changes + } + + onMoveMaskUp() { + const index = this.maskList.selectedIndex; + if (index > 0) { + const option = this.maskList.options[index]; + this.maskList.remove(index); + this.maskList.add(option, index - 1); + this.maskList.selectedIndex = index - 1; + + const mask = this.masks.splice(index, 1)[0]; + this.masks.splice(index - 1, 0, mask); + + const imageIndex = this.canvas.getObjects().indexOf(this.imageObject); + const newZIndex = imageIndex + 1 + (index - 1); + this.canvas.moveTo(mask.fabricImage, newZIndex); + this.canvas.renderAll(); + } + } + + onMoveMaskDown() { + const index = this.maskList.selectedIndex; + if (index < this.maskList.options.length - 1) { + const option = this.maskList.options[index]; + this.maskList.remove(index); + this.maskList.add(option, index + 1); + this.maskList.selectedIndex = index + 1; + + const mask = this.masks.splice(index, 1)[0]; + this.masks.splice(index + 1, 0, mask); + + const imageIndex = this.canvas.getObjects().indexOf(this.imageObject); + const newZIndex = imageIndex + 1 + (index + 1); + this.canvas.moveTo(mask.fabricImage, newZIndex); + this.canvas.renderAll(); + } + } + + onRemoveMask() { + if (this.masks.length === 0) { + alert('No masks available to remove.'); + return; + } + + const selectedIndex = this.maskList.selectedIndex; + if (selectedIndex === -1) { + alert('Please select a mask to remove.'); + return; + } + + const maskName = this.maskList.options[selectedIndex].value; + const mask = this.masks.find(m => m.name === maskName); + + if (!mask) { + alert('Selected mask not found.'); + return; + } + + const confirmRemoval = confirm(`Are you sure you want to remove "${maskName}"?`); + if (!confirmRemoval) { + return; + } + + this.canvas.remove(mask.fabricImage); + + this.masks = this.masks.filter(m => m.name !== maskName); + + delete this.maskStrokeHistory[maskName]; + + this.maskList.remove(selectedIndex); + + if (this.masks.length > 0) { + const newIndex = selectedIndex > 0 ? selectedIndex - 1 : 0; + this.maskList.selectedIndex = newIndex; + this.currentMask = this.masks[newIndex]; + } else { + this.currentMask = null; + } + + this.updateBrushColorAndCursor(); + + this.canvas.renderAll(); + } + + updateBrushColorAndCursor() { + if (this.currentMask) { + this.brushColor = this.currentMask.color; + this.updateCursorCircle(); + this.canvas.requestRenderAll(); + } else { + + this.brushColor = '#FF0000'; + this.updateCursorCircle(); + this.canvas.requestRenderAll(); + } + } + + onMouseDown(o) { + if (!this.currentMask || !this.currentMask.ctx) { + console.error('Cannot draw: currentMask or its context is null.'); + return; + } + + this.isMouseDown = true; + this.isStrokeInProgress = true; + + const pointer = this.canvas.getPointer(o.e, true); + const transformedPointer = this.mapPointerToImageSpace(pointer); + + if (!this.isPointerInsideImage(transformedPointer)) { + this.isMouseDown = false; + this.isStrokeInProgress = false; + return; + } + + this.lastPointer = transformedPointer; + + const imageDataBefore = this.currentMask.ctx.getImageData(0, 0, this.currentMask.canvasEl.width, this.currentMask.canvasEl.height); + this.maskStrokeHistory[this.currentMask.name].undoStack.push(imageDataBefore); + if (this.maskStrokeHistory[this.currentMask.name].undoStack.length > this.maxHistory) { + this.maskStrokeHistory[this.currentMask.name].undoStack.shift(); + } + + this.maskStrokeHistory[this.currentMask.name].redoStack = []; + + this.drawOnMask(transformedPointer); + } + + onMouseMove(o) { + if (!this.currentMask || !this.currentMask.ctx) { + console.error('Cannot draw: currentMask or its context is null.'); + return; + } + + const pointer = this.canvas.getPointer(o.e, true); + const transformedPointer = this.mapPointerToImageSpace(pointer); + + this.updateCursorPosition(o); + + if (this.isMouseDown && this.isStrokeInProgress) { + if (!this.isPointerInsideImage(transformedPointer)) { + return; + } + + this.drawLineOnMask(this.lastPointer, transformedPointer); + this.lastPointer = transformedPointer; + } + + this.canvas.requestRenderAll(); + } + + onMouseUp() { + if (this.isMouseDown) { + this.isMouseDown = false; + this.isStrokeInProgress = false; + } + } + + onUndoMaskStroke(strokeData) { + console.log('MaskBrushPlugin: Undoing mask stroke', strokeData); + if (strokeData.type === 'add') { + this.undoLastStroke(strokeData.maskName); + } + } + + onRedoMaskStroke(strokeData) { + console.log('MaskBrushPlugin: Redoing mask stroke', strokeData); + if (strokeData.type === 'add') { + this.redoLastStroke(strokeData.maskName, strokeData); + } + } + + undoLastStroke(maskName) { + const history = this.maskStrokeHistory[maskName]; + if (!history || history.undoStack.length === 0) { + console.warn(`No undo actions available for mask "${maskName}".`); + return; + } + + const lastState = history.undoStack.pop(); + + const currentState = this.currentMask.ctx.getImageData(0, 0, this.currentMask.canvasEl.width, this.currentMask.canvasEl.height); + history.redoStack.push(currentState); + + this.currentMask.ctx.putImageData(lastState, 0, 0); + this.currentMask.fabricImage.dirty = true; + this.canvas.renderAll(); + } + + redoLastStroke(maskName, strokeData) { + const history = this.maskStrokeHistory[maskName]; + if (!history || history.redoStack.length === 0) { + console.warn(`No redo actions available for mask "${maskName}".`); + return; + } + + const redoState = history.redoStack.pop(); + const currentState = this.currentMask.ctx.getImageData(0, 0, this.currentMask.canvasEl.width, this.currentMask.canvasEl.height); + history.undoStack.push(currentState); + + this.currentMask.ctx.putImageData(redoState, 0, 0); + this.currentMask.fabricImage.dirty = true; + this.canvas.renderAll(); + } + + mapPointerToImageSpace(pointer) { + if (!this.imageObject) { + console.error('Image object is not defined.'); + return { x: 0, y: 0 }; + } + + const img = this.imageObject; + const viewportTransform = this.canvas.viewportTransform; + const invViewportTransform = fabric.util.invertTransform(viewportTransform); + + const canvasPoint = new fabric.Point(pointer.x, pointer.y); + + const imagePoint = fabric.util.transformPoint(canvasPoint, invViewportTransform); + const relativeX = (imagePoint.x - img.left) / img.scaleX + (this.imageOriginalWidth / 2); + const relativeY = (imagePoint.y - img.top) / img.scaleY + (this.imageOriginalHeight / 2); + + return { x: relativeX, y: relativeY }; + } + + isPointerInsideImage(point) { + return ( + point.x >= 0 && + point.x <= this.imageOriginalWidth && + point.y >= 0 && + point.y <= this.imageOriginalHeight + ); + } + + drawOnMask(point) { + if (!this.imageObject) { + console.error('Image object is not defined.'); + return; + } + + this.currentMask.ctx.globalAlpha = 1; + + const adjustedBrushSize = this.brushSize / (this.currentZoom * this.imageObject.scaleX); + + this.currentMask.ctx.fillStyle = this.brushColor; + this.currentMask.ctx.beginPath(); + this.currentMask.ctx.arc(point.x, point.y, adjustedBrushSize / 2, 0, Math.PI * 2); + this.currentMask.ctx.fill(); + this.currentMask.fabricImage.dirty = true; + + const strokeData = { + type: 'add', + maskName: this.currentMask.name, + color: this.brushColor, + position: { x: point.x, y: point.y }, + brushSize: adjustedBrushSize + }; + this.canvasManager.emit('mask:stroke:added', strokeData); + } + + drawLineOnMask(fromPoint, toPoint) { + if (!this.imageObject) { + console.error('Image object is not defined.'); + return; + } + + this.currentMask.ctx.globalAlpha = 1; + + const adjustedBrushSize = this.brushSize / (this.currentZoom * this.imageObject.scaleX); + + this.currentMask.ctx.strokeStyle = this.brushColor; + this.currentMask.ctx.lineWidth = adjustedBrushSize; + this.currentMask.ctx.lineCap = 'round'; + this.currentMask.ctx.beginPath(); + this.currentMask.ctx.moveTo(fromPoint.x, fromPoint.y); + this.currentMask.ctx.lineTo(toPoint.x, toPoint.y); + this.currentMask.ctx.stroke(); + this.currentMask.fabricImage.dirty = true; + + const strokeData = { + type: 'add', + maskName: this.currentMask.name, + color: this.brushColor, + from: { x: fromPoint.x, y: fromPoint.y }, + to: { x: toPoint.x, y: toPoint.y }, + brushSize: adjustedBrushSize + }; + this.canvasManager.emit('mask:stroke:added', strokeData); + } + + onHandleSave() { + const selectedOption = this.saveOptionsSelect.value; + + const option = this.saveOptions.find(opt => opt.value === selectedOption); + if (option && option.handler) { + option.handler(); + } else { + console.error('Unknown save option selected.'); + } + } + + onHandleSaveFromCanvas(selectedOption) { + const option = this.saveOptions.find(opt => opt.value === selectedOption); + if (option && option.handler) { + option.handler(); + } else { + console.error('Unknown save option selected.'); + } + } + + getExportFunction(optionValue) { + return this.saveOptions.find(opt => opt.value === optionValue)?.exportFunction || null; + } + + createCombinedAlphaMask() { + const alphaCanvas = document.createElement('canvas'); + alphaCanvas.width = this.imageOriginalWidth; + alphaCanvas.height = this.imageOriginalHeight; + const alphaCtx = alphaCanvas.getContext('2d'); + + alphaCtx.clearRect(0, 0, alphaCanvas.width, alphaCanvas.height); + this.masks.forEach(mask => { + alphaCtx.drawImage(mask.canvasEl, 0, 0, alphaCanvas.width, alphaCanvas.height); + }); + + return alphaCanvas; + } + + exportMaskAlphaOnImage() { + if (!this.imageObject) { + alert('No image loaded to save the alpha on.'); + return null; + } + + if (!this.currentMask) { + alert('No mask selected to save as alpha.'); + return null; + } + + try { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + const alphaMask = document.createElement('canvas'); + alphaMask.width = this.imageOriginalWidth; + alphaMask.height = this.imageOriginalHeight; + const alphaCtx = alphaMask.getContext('2d'); + alphaCtx.drawImage(this.currentMask.canvasEl, 0, 0, alphaMask.width, alphaMask.height); + + combinedCtx.globalCompositeOperation = 'destination-out'; + combinedCtx.drawImage(alphaMask, 0, 0, combinedCanvas.width, combinedCanvas.height); + combinedCtx.globalCompositeOperation = 'source-over'; + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + + } catch (error) { + console.error('Error saving single mask alpha on image:', error); + alert('An error occurred while saving the single mask alpha on image.'); + return null; + } + } + + exportAllMasksAlphaOnImage() { + if (!this.imageObject) { + alert('No image loaded to save the alphas on.'); + return null; + } + + if (this.masks.length === 0) { + alert('No masks available to save as alphas.'); + return null; + } + + try { + const dataURLs = this.masks.map(mask => { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + const alphaMask = document.createElement('canvas'); + alphaMask.width = this.imageOriginalWidth; + alphaMask.height = this.imageOriginalHeight; + const alphaCtx = alphaMask.getContext('2d'); + alphaCtx.drawImage(mask.canvasEl, 0, 0, alphaMask.width, alphaMask.height); + + combinedCtx.globalCompositeOperation = 'destination-out'; + combinedCtx.drawImage(alphaMask, 0, 0, combinedCanvas.width, combinedCanvas.height); + combinedCtx.globalCompositeOperation = 'source-over'; + + return { + dataURL: combinedCanvas.toDataURL('image/png'), + filename: `${mask.name}_alpha_on_image.png` + }; + }); + + return dataURLs; + + } catch (error) { + console.error('Error saving all masks alphas on image:', error); + alert('An error occurred while saving all masks alphas on image.'); + return null; + } + } + + exportAllMasksCombinedAlphaOnImage() { + if (!this.imageObject) { + alert('No image loaded to save the combined alpha on.'); + return null; + } + + if (this.masks.length === 0) { + alert('No masks available to save as combined alpha.'); + return null; + } + + try { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + const alphaMask = this.createCombinedAlphaMask(); + + combinedCtx.globalCompositeOperation = 'destination-out'; + combinedCtx.drawImage(alphaMask, 0, 0, combinedCanvas.width, combinedCanvas.height); + combinedCtx.globalCompositeOperation = 'source-over'; + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + + } catch (error) { + console.error('Error saving combined masks alpha on image:', error); + alert('An error occurred while saving the combined masks alpha on image.'); + return null; + } + } + + //Mask + exportMask() { + if (!this.currentMask) { + alert('No mask selected to export.'); + return null; + } + + try { + const dataURL = this.currentMask.canvasEl.toDataURL('image/png'); + return dataURL; + } catch (error) { + console.error('Error exporting single mask image:', error); + alert('An error occurred while exporting the single mask image.'); + return null; + } + } + + exportAllMasks() { + if (!this.imageObject) { + alert('No image loaded to export masks on.'); + return null; + } + + if (this.masks.length === 0) { + alert('No masks available to export on image.'); + return null; + } + + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + this.masks.forEach(mask => { + combinedCtx.drawImage(mask.canvasEl, 0, 0, combinedCanvas.width, combinedCanvas.height); + }); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } + + exportAllMasksCombined() { + if (this.masks.length === 0) { + alert('No masks available to export.'); + return null; + } + + try { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + this.masks.forEach(mask => { + combinedCtx.drawImage(mask.canvasEl, 0, 0, combinedCanvas.width, combinedCanvas.height); + }); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } catch (error) { + console.error('Error exporting combined masks image:', error); + alert('An error occurred while exporting the combined masks image.'); + return null; + } + } + + exportMasksCombinedBlackWhite() { + if (this.masks.length === 0) { + alert('No masks available to export.'); + return null; + } + + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.fillStyle = 'black'; + combinedCtx.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height); + + this.masks.forEach(mask => { + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = this.imageOriginalWidth; + maskCanvas.height = this.imageOriginalHeight; + const maskCtx = maskCanvas.getContext('2d'); + + maskCtx.drawImage(mask.canvasEl, 0, 0, maskCanvas.width, maskCanvas.height); + + const imageData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + if (alpha > 0) { + data[i] = 255; + data[i + 1] = 255; + data[i + 2] = 255; + } + } + + maskCtx.putImageData(imageData, 0, 0); + + combinedCtx.drawImage(maskCanvas, 0, 0, combinedCanvas.width, combinedCanvas.height); + }); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } + + //Mask on image + exportMaskOnImage() { + if (!this.imageObject) { + alert('No image loaded to export mask on.'); + return null; + } + + if (!this.currentMask) { + alert('No mask selected to export on image.'); + return null; + } + + try { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + combinedCtx.drawImage(this.currentMask.canvasEl, 0, 0, combinedCanvas.width, combinedCanvas.height); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } catch (error) { + console.error('Error exporting single mask on image:', error); + alert('An error occurred while exporting the single mask on image.'); + return null; + } + } + + exportAllMasksOnImage() { + if (!this.imageObject) { + alert('No image loaded to export masks on.'); + return null; + } + + if (this.masks.length === 0) { + alert('No masks available to export on image.'); + return null; + } + + try { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + this.masks.forEach(mask => { + combinedCtx.drawImage(mask.canvasEl, 0, 0, combinedCanvas.width, combinedCanvas.height); + }); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } catch (error) { + console.error('Error exporting all masks on image:', error); + alert('An error occurred while exporting all masks on image.'); + return null; + } + } + + exportAllMasksCombinedOnImage() { + if (!this.imageObject) { + alert('No image loaded to export combined masks on.'); + return null; + } + + if (this.masks.length === 0) { + alert('No masks available to export combined on image.'); + return null; + } + + try { + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + const combinedMaskCanvas = this.createCombinedAlphaMask(); + combinedCtx.drawImage(combinedMaskCanvas, 0, 0, combinedCanvas.width, combinedCanvas.height); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } catch (error) { + console.error('Error exporting combined masks on image:', error); + alert('An error occurred while exporting combined masks on image.'); + return null; + } + } + + exportAllMasksCombinedBlackWhiteOnImage() { + if (!this.imageObject) { + alert('No image loaded to export masks on.'); + return null; + } + + if (this.masks.length === 0) { + alert('No masks available to export on image.'); + return null; + } + + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = this.imageOriginalWidth; + combinedCanvas.height = this.imageOriginalHeight; + const combinedCtx = combinedCanvas.getContext('2d'); + + combinedCtx.drawImage(this.imageObject.getElement(), 0, 0, combinedCanvas.width, combinedCanvas.height); + + const tempMasksCanvas = document.createElement('canvas'); + tempMasksCanvas.width = this.imageOriginalWidth; + tempMasksCanvas.height = this.imageOriginalHeight; + const tempMasksCtx = tempMasksCanvas.getContext('2d'); + + tempMasksCtx.fillStyle = 'black'; + tempMasksCtx.fillRect(0, 0, tempMasksCanvas.width, tempMasksCanvas.height); + + this.masks.forEach(mask => { + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = this.imageOriginalWidth; + maskCanvas.height = this.imageOriginalHeight; + const maskCtx = maskCanvas.getContext('2d'); + + maskCtx.drawImage(mask.canvasEl, 0, 0, maskCanvas.width, maskCanvas.height); + + const imageData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + if (alpha > 0) { + data[i] = 255; + data[i + 1] = 255; + data[i + 2] = 255; + } + } + + maskCtx.putImageData(imageData, 0, 0); + + tempMasksCtx.drawImage(maskCanvas, 0, 0, tempMasksCanvas.width, tempMasksCanvas.height); + }); + + combinedCtx.drawImage(tempMasksCanvas, 0, 0, combinedCanvas.width, combinedCanvas.height); + + const combinedDataURL = combinedCanvas.toDataURL('image/png'); + return combinedDataURL; + } + + saveMaskAlphaOnImage() { + const dataURL = this.exportMaskAlphaOnImage(); + if (dataURL) { + this.downloadImage(dataURL, `${this.currentMask.name}_alpha_on_image.png`); + } + } + + saveAllMasksAlphaOnImage() { + const dataURLs = this.exportAllMasksAlphaOnImage(); + if (dataURLs) { + dataURLs.forEach(({ dataURL, filename }) => { + this.downloadImage(dataURL, filename); + }); + } + } + + saveAllMasksCombinedAlphaOnImage() { + const dataURL = this.exportAllMasksCombinedAlphaOnImage(); + if (dataURL) { + this.downloadImage(dataURL, 'combined_masks_alpha_on_image.png'); + } + } + + saveMask() { + if (this.currentMask && this.currentMask.canvasEl) { + const dataURL = this.exportMask(); + if (dataURL) { + this.downloadImage(dataURL, `${this.currentMask.name}.png`); + } + } else { + alert('No current mask data available to save.'); + } + } + + saveAllMasks() { + if (this.masks.length > 0) { + this.masks.forEach((mask) => { + const dataURL = mask.canvasEl.toDataURL('image/png'); + this.downloadImage(dataURL, `${mask.name}.png`); + }); + } else { + alert('No mask data available to save.'); + } + } + + saveAllMasksCombined() { + const combinedDataURL = this.exportAllMasksCombined(); + if (combinedDataURL) { + this.downloadImage(combinedDataURL, 'combined_masks.png'); + } + } + + saveAllMasksCombinedBlackWhite() { + const combinedDataURL = this.exportMasksCombinedBlackWhite(); + if (combinedDataURL) { + this.downloadImage(combinedDataURL, 'combined_masks_black_white.png'); + } + } + + saveMaskOnImage() { + const dataURL = this.exportMaskOnImage(); + if (dataURL) { + this.downloadImage(dataURL, `${this.currentMask.name}_on_image.png`); + } + } + + saveAllMasksOnImage() { + const dataURL = this.exportAllMasksOnImage(); + if (dataURL) { + this.downloadImage(dataURL, 'all_masks_on_image.png'); + } + } + + saveAllMasksCombinedOnImage() { + const dataURL = this.exportAllMasksCombinedOnImage(); + if (dataURL) { + this.downloadImage(dataURL, 'combined_masks_on_image.png'); + } + } + + saveAllMasksCombinedBlackWhiteOnImage() { + const combinedDataURL = this.exportAllMasksCombinedBlackWhiteOnImage(); + if (combinedDataURL) { + this.downloadImage(combinedDataURL, 'combined_masks_black_white_on_image.png'); + } + } + + downloadImage(dataUrl, filename) { + const link = document.createElement('a'); + link.href = dataUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + destroy() { + this.detachAdditionalEventListeners(); + + this.masks.forEach(mask => { + this.canvas.remove(mask.fabricImage); + }); + this.masks = []; + this.currentMask = null; + + if (this.imageObject) { + this.imageObject.off('modified', this.onImageModified); + } + + this.canvasManager.off('undo:mask:stroke', this.onUndoMaskStroke); + this.canvasManager.off('redo:mask:stroke', this.onRedoMaskStroke); + + super.destroy(); + } +} diff --git a/web/core/js/common/components/canvas/UndoRedoPlugin.js b/web/core/js/common/components/canvas/UndoRedoPlugin.js new file mode 100644 index 0000000..5f4f8e6 --- /dev/null +++ b/web/core/js/common/components/canvas/UndoRedoPlugin.js @@ -0,0 +1,362 @@ +import { CanvasPlugin } from './CanvasPlugin.js'; + +export class UndoRedoPlugin extends CanvasPlugin { + constructor(options = {}) { + super(options.name || 'UndoRedoPlugin'); + + this.maxHistory = options.maxHistory || 50; + + this.undoStack = []; + this.redoStack = []; + + this.canvasManager = null; + this.canvas = null; + + this.undoBtn = null; + this.redoBtn = null; + + this.isPerformingUndoRedo = false; + + this.handleObjectAdded = this.handleObjectAdded.bind(this); + this.handleObjectRemoved = this.handleObjectRemoved.bind(this); + this.handleMaskStrokeAdded = this.handleMaskStrokeAdded.bind(this); + this.handleMaskStrokeRemoved = this.handleMaskStrokeRemoved.bind(this); + this.onUndo = this.onUndo.bind(this); + this.onRedo = this.onRedo.bind(this); + this.updateButtons = this.updateButtons.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onMouseEnter = this.onMouseEnter.bind(this); + } + + init(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + + this.makeFocusable(); + + this.createUI(); + + this.canvas.on('object:added', this.handleObjectAdded); + this.canvas.on('object:removed', this.handleObjectRemoved); + this.canvasManager.on('mask:stroke:added', this.handleMaskStrokeAdded); + this.canvasManager.on('mask:stroke:removed', this.handleMaskStrokeRemoved); + + this.focusableElement.addEventListener('keydown', this.onKeyDown); + this.focusableElement.addEventListener('focus', this.onFocus); + this.focusableElement.addEventListener('blur', this.onBlur); + + this.focusableElement.addEventListener('mouseenter', this.onMouseEnter); + + this.updateButtons(); + } + + makeFocusable() { + const container = this.canvas.lowerCanvasEl.parentElement; + if (!container) { + console.error('UndoRedoPlugin: Canvas container not found.'); + return; + } + + container.setAttribute('tabindex', '0'); + container.style.outline = 'none'; + + this.focusableElement = container; + } + + handleObjectAdded(e) { + if (this.isPerformingUndoRedo) return; + + const obj = e.target; + if (this.isBrushStroke(obj)) { + // console.log('UndoRedoPlugin: Brush stroke added', obj); + + if (!obj.brushStrokeId) { + obj.brushStrokeId = `stroke_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + obj.set('brushStrokeId', obj.brushStrokeId); + } + this.undoStack.push(obj.toObject(['type', 'path', 'stroke', 'strokeWidth', 'opacity', 'brushStrokeId'])); + if (this.undoStack.length > this.maxHistory) { + this.undoStack.shift(); + } + this.redoStack = []; + this.updateButtons(); + } + } + + handleObjectRemoved(e) { + if (this.isPerformingUndoRedo) return; + + const obj = e.target; + if (this.isBrushStroke(obj)) { + // console.log('UndoRedoPlugin: Brush stroke removed', obj); + this.redoStack.push(obj.toObject(['type', 'path', 'stroke', 'strokeWidth', 'opacity', 'brushStrokeId'])); + if (this.redoStack.length > this.maxHistory) { + this.redoStack.shift(); + } + this.updateButtons(); + } + } + + isBrushStroke(obj) { + const isPath = obj && obj.type === 'path'; + // console.log(`UndoRedoPlugin: isBrushStroke check for object type '${obj.type}': ${isPath}`); + return isPath; + } + + handleMaskStrokeAdded(strokeData) { + // console.log('UndoRedoPlugin: Mask stroke added', strokeData); + this.undoStack.push({ + type: 'mask_add', + maskName: strokeData.maskName, + strokeData: strokeData + }); + if (this.undoStack.length > this.maxHistory) { + this.undoStack.shift(); + } + this.redoStack = []; + this.updateButtons(); + } + + handleMaskStrokeRemoved(strokeData) { + // console.log('UndoRedoPlugin: Mask stroke removed', strokeData); + this.redoStack.push({ + type: 'mask_remove', + maskName: strokeData.maskName, + strokeData: strokeData + }); + if (this.redoStack.length > this.maxHistory) { + this.redoStack.shift(); + } + this.updateButtons(); + } + + createUI() { + if (!document.querySelector('style[data-plugin="undo-redo"]')) { + const styleSheet = document.createElement('style'); + styleSheet.setAttribute('data-plugin', 'undo-redo'); + styleSheet.textContent = ` + .ur-container * { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } + + .ur-container { + display: inline-flex; + gap: 0.5rem; + padding: 0.5rem; + /* background: var(--color-background); */ + /* border: 1px dashed var(--color-border); */ + } + + .ur-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: var(--color-button-primary); + border: none; + cursor: pointer; + transition: all 0.2s; + color: var(--color-primary-text); + height: 36px; + min-width: 36px; + } + + .ur-button:hover { + background: var(--color-button-primary-hover); + } + + .ur-button svg { + width: 1.25rem; + height: 1.25rem; + } + + .ur-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .ur-button:disabled:hover { + background: var(--color-button-primary); + } + `; + document.head.appendChild(styleSheet); + } + + const container = document.createElement('div'); + container.className = 'ur-container'; + + this.undoBtn = document.createElement('button'); + this.undoBtn.className = 'ur-button'; + this.undoBtn.innerHTML = ` + + + + `; + this.undoBtn.title = 'Undo (Ctrl+Z)'; + this.undoBtn.disabled = true; + this.undoBtn.addEventListener('click', this.onUndo); + + this.redoBtn = document.createElement('button'); + this.redoBtn.className = 'ur-button'; + this.redoBtn.innerHTML = ` + + + + `; + this.redoBtn.title = 'Redo (Ctrl+Y)'; + this.redoBtn.disabled = true; + this.redoBtn.addEventListener('click', this.onRedo); + + container.appendChild(this.undoBtn); + container.appendChild(this.redoBtn); + + const pluginUIContainer = document.getElementById('pluginUIContainer'); + if (pluginUIContainer) { + pluginUIContainer.appendChild(container); + } else { + console.error('UndoRedoPlugin: #pluginUIContainer not found in the DOM.'); + } + } + + onUndo() { + // console.log('UndoRedoPlugin: Undo.'); + + if (this.undoStack.length === 0) { + console.log('UndoRedoPlugin: Undo stack is empty.'); + return; + } + + const lastAction = this.undoStack.pop(); + // console.log('UndoRedoPlugin: Performing Undo', lastAction); + + this.isPerformingUndoRedo = true; + + if (lastAction.type === 'mask_add') { + this.canvasManager.emit('undo:mask:stroke', lastAction.strokeData); + this.redoStack.push(lastAction); + } else if (lastAction.type === 'mask_remove') { + this.canvasManager.emit('redo:mask:stroke', lastAction.strokeData); + this.redoStack.push(lastAction); + } else { + const brushStrokeId = lastAction.brushStrokeId; + const targetObj = this.canvas.getObjects('path').find(obj => obj.brushStrokeId === brushStrokeId); + if (targetObj) { + this.canvas.remove(targetObj); + this.redoStack.push(lastAction); + // console.log('UndoRedoPlugin: Brush stroke removed for Undo'); + } else { + console.warn('UndoRedoPlugin: Could not find the brush stroke object to undo.'); + } + } + + this.isPerformingUndoRedo = false; + + this.updateButtons(); + } + + onRedo() { + // console.log('UndoRedoPlugin: Redo.'); + + if (this.redoStack.length === 0) { + // console.log('UndoRedoPlugin: Redo stack is empty.'); + return; + } + + const lastUndone = this.redoStack.pop(); + // console.log('UndoRedoPlugin: Performing Redo', lastUndone); + + this.isPerformingUndoRedo = true; + + if (lastUndone.type === 'mask_add') { + this.canvasManager.emit('redo:mask:stroke', lastUndone.strokeData); + this.undoStack.push(lastUndone); + } else if (lastUndone.type === 'mask_remove') { + this.canvasManager.emit('undo:mask:stroke', lastUndone.strokeData); + this.undoStack.push(lastUndone); + } else { + const path = new fabric.Path(lastUndone.path, { + stroke: lastUndone.stroke, + strokeWidth: lastUndone.strokeWidth, + fill: null, + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + lockMovementX: true, + lockMovementY: true, + opacity: lastUndone.opacity, + brushStrokeId: lastUndone.brushStrokeId + }); + this.canvas.add(path); + this.undoStack.push(lastUndone); + // console.log('UndoRedoPlugin: Brush stroke re-added for Redo'); + } + + this.isPerformingUndoRedo = false; + + this.updateButtons(); + } + + updateButtons() { + this.undoBtn.disabled = this.undoStack.length === 0; + this.redoBtn.disabled = this.redoStack.length === 0; + // console.log(`UndoRedoPlugin: Update Buttons - Undo: ${!this.undoBtn.disabled}, Redo: ${!this.redoBtn.disabled}`); + } + + onKeyDown(e) { + if (this.isFocused) { + if (e.ctrlKey && (e.key === 'z' || e.key === 'Z')) { + e.preventDefault(); + this.onUndo(); + } + if (e.ctrlKey && (e.key === 'y' || e.key === 'Y')) { + e.preventDefault(); + this.onRedo(); + } + } + } + + onFocus() { + this.isFocused = true; + // console.log('UndoRedoPlugin: Canvas focused.'); + } + + onBlur() { + this.isFocused = false; + // console.log('UndoRedoPlugin: Canvas lost focus.'); + } + + onMouseEnter() { + this.focusableElement.focus(); + // console.log('UndoRedoPlugin: Canvas focused via mouse enter.'); + } + + destroy() { + this.canvas.off('object:added', this.handleObjectAdded); + this.canvas.off('object:removed', this.handleObjectRemoved); + this.canvasManager.off('mask:stroke:added', this.handleMaskStrokeAdded); + this.canvasManager.off('mask:stroke:removed', this.handleMaskStrokeRemoved); + if (this.focusableElement) { + this.focusableElement.removeEventListener('keydown', this.onKeyDown); + this.focusableElement.removeEventListener('focus', this.onFocus); + this.focusableElement.removeEventListener('blur', this.onBlur); + this.focusableElement.removeEventListener('mouseenter', this.onMouseEnter); + } + + const container = document.querySelector('.ur-container'); + if (container && this.undoBtn && this.redoBtn) { + container.removeChild(this.undoBtn); + container.removeChild(this.redoBtn); + } + + this.undoStack = []; + this.redoStack = []; + + super.destroy(); + } +} diff --git a/web/core/js/common/components/canvas/fabric.5.2.4.min.js b/web/core/js/common/components/canvas/fabric.5.2.4.min.js new file mode 100644 index 0000000..d28da51 --- /dev/null +++ b/web/core/js/common/components/canvas/fabric.5.2.4.min.js @@ -0,0 +1 @@ +var jsdom,virtualWindow,fabric=fabric||{version:"5.2.4"};function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,t=t.destinationHeight;i===n&&r===t||(e.width=n,e.height=t)}function copyGLTo2DDrawImage(t,e){var t=t.canvas,e=e.targetCanvas,i=e.getContext("2d"),r=(i.translate(0,e.height),i.scale(1,-1),t.height-e.height);i.drawImage(t,0,r,e.width,e.height,0,0,e.width,e.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,e=e.destinationHeight,n=r*e*4,s=new Uint8Array(this.imageBuffer,0,n),n=new Uint8ClampedArray(this.imageBuffer,0,n),t=(t.readPixels(0,0,r,e,t.RGBA,t.UNSIGNED_BYTE,s),new ImageData(n,r,e));i.putImageData(t,0,0)}"undefined"!=typeof exports?exports.fabric=fabric:"function"==typeof define&&define.amd&&define([],function(){return fabric}),"undefined"!=typeof document&&"undefined"!=typeof window?(document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?fabric.document=document:fabric.document=document.implementation.createHTMLDocument(""),fabric.window=window):(jsdom=require("jsdom"),virtualWindow=new jsdom.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window,fabric.document=virtualWindow.document,fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=virtualWindow,DOMParser=fabric.window.DOMParser),fabric.isTouchSupported="ontouchstart"in fabric.window||"ontouchstart"in fabric.document||fabric.window&&fabric.window.navigator&&0_)for(var C=1,S=v.length;Ct[i-2].x?1:s.x===t[i-2].x?0:-1,h=s.y>t[i-2].y?1:s.y===t[i-2].y?0:-1),n.push(["L",s.x+c*e,s.y+h*e]),n},fabric.util.getPathSegmentsInfo=l,fabric.util.getBoundsOfCurve=function(t,e,i,r,n,s,o,a){var c;if(fabric.cachesBoundsOfCurve&&(c=w.call(arguments),fabric.boundsOfCurveCache[c]))return fabric.boundsOfCurveCache[c];for(var h,l,u,f=Math.sqrt,d=Math.min,g=Math.max,p=Math.abs,m=[],v=[[],[]],b=6*t-12*i+6*n,y=-3*t+9*i-9*n+3*o,_=3*i-3*t,x=0;x<2;++x)0/g,">")},graphemeSplit:function(t){for(var e,i=0,r=[],i=0;it.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,e){return void 0===e&&(e=.5),e=Math.max(Math.min(1,e),0),new i(this.x+(t.x-this.x)*e,this.y+(t.y-this.y)*e)},distanceFrom:function(t){var e=this.x-t.x,t=this.y-t.y;return Math.sqrt(e*e+t*t)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new i(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new i(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new i(this.x,this.y)}}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var a=t.fabric||(t.fabric={});function c(t){this.status=t,this.points=[]}a.Intersection?a.warn("fabric.Intersection is already defined"):(a.Intersection=c,a.Intersection.prototype={constructor:c,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},a.Intersection.intersectLineLine=function(t,e,i,r){var n,s=(r.x-i.x)*(t.y-i.y)-(r.y-i.y)*(t.x-i.x),o=(e.x-t.x)*(t.y-i.y)-(e.y-t.y)*(t.x-i.x),r=(r.y-i.y)*(e.x-t.x)-(r.x-i.x)*(e.y-t.y);return 0!=r?(i=o/r,0<=(r=s/r)&&r<=1&&0<=i&&i<=1?(n=new c("Intersection")).appendPoint(new a.Point(t.x+r*(e.x-t.x),t.y+r*(e.y-t.y))):n=new c):n=new c(0==s||0==o?"Coincident":"Parallel"),n},a.Intersection.intersectLinePolygon=function(t,e,i){for(var r,n,s=new c,o=i.length,a=0;a=o&&(s.x-=o),s.x<=-o&&(s.x+=o),s.y>=o&&(s.y-=o),s.y<=o&&(s.y+=o),s.x-=t.offsetX,s.y-=t.offsetY,s}function w(t){return t.flipX!==t.flipY}function O(t,e,i,r,n){0!==t[e]&&(e=n/t._getTransformedDimensions()[r]*t[i],t.set(i,e))}function P(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(0,s.skewY),i=T(e,e.originX,e.originY,i,r),r=Math.abs(2*i.x)-o.x,i=s.skewX,r=(r<2?n=0:(n=g(Math.atan2(r/s.scaleX,o.y/s.scaleY)),e.originX===c&&e.originY===u&&(n=-n),e.originX===l&&e.originY===h&&(n=-n),w(s)&&(n=-n)),i!==n);return r&&(o=s._getTransformedDimensions().y,s.set("skewX",n),O(s,"skewY","scaleY","y",o)),r}function k(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(s.skewX,0),i=T(e,e.originX,e.originY,i,r),r=Math.abs(2*i.y)-o.y,i=s.skewY,r=(r<2?n=0:(n=g(Math.atan2(r/s.scaleY,o.x/s.scaleX)),e.originX===c&&e.originY===u&&(n=-n),e.originX===l&&e.originY===h&&(n=-n),w(s)&&(n=-n)),i!==n);return r&&(o=s._getTransformedDimensions().x,s.set("skewY",n),O(s,"skewX","scaleX","x",o)),r}function E(t,e,i,r,n){var s=e.target,o=s.lockScalingX,a=s.lockScalingY,n=(n=n||{}).by,t=b(t,s),c=_(s,n,t),h=e.gestureScale;if(c)return!1;if(h)l=e.scaleX*h,u=e.scaleY*h;else{if(c=T(e,e.originX,e.originY,i,r),h="y"!==n?p(c.x):1,i="x"!==n?p(c.y):1,e.signX||(e.signX=h),e.signY||(e.signY=i),s.lockScalingFlip&&(e.signX!==h||e.signY!==i))return!1;var l,u,r=s._getTransformedDimensions();u=t&&!n?(t=Math.abs(c.x)+Math.abs(c.y),f=e.original,t=t/(Math.abs(r.x*f.scaleX/s.scaleX)+Math.abs(r.y*f.scaleY/s.scaleY)),l=f.scaleX*t,f.scaleY*t):(l=Math.abs(c.x*s.scaleX/r.x),Math.abs(c.y*s.scaleY/r.y)),y(e)&&(l*=2,u*=2),e.signX!==h&&"y"!==n&&(e.originX=d[e.originX],l*=-1,e.signX=h),e.signY!==i&&"x"!==n&&(e.originY=d[e.originY],u*=-1,e.signY=i)}var f=s.scaleX,t=s.scaleY;return n?("x"===n&&s.set("scaleX",l),"y"===n&&s.set("scaleY",u)):(o||s.set("scaleX",l),a||s.set("scaleY",u)),f!==s.scaleX||t!==s.scaleY}o.scaleCursorStyleHandler=function(t,e,i){var t=b(t,i),r="";return 0!==e.x&&0===e.y?r="x":0===e.x&&0!==e.y&&(r="y"),_(i,r,t)?"not-allowed":(r=m(i,e),n[r]+"-resize")},o.skewCursorStyleHandler=function(t,e,i){var r="not-allowed";return 0!==e.x&&i.lockSkewingY||0!==e.y&&i.lockSkewingX?r:(r=m(i,e)%4,s[r]+"-resize")},o.scaleSkewCursorStyleHandler=function(t,e,i){return t[i.canvas.altActionKey]?o.skewCursorStyleHandler(t,e,i):o.scaleCursorStyleHandler(t,e,i)},o.rotationWithSnapping=S("rotating",C(function(t,e,i,r){var n=e.target,s=n.translateToOriginPoint(n.getCenterPoint(),e.originX,e.originY);if(n.lockRotation)return!1;var o=Math.atan2(e.ey-s.y,e.ex-s.x),r=Math.atan2(r-s.y,i-s.x),i=g(r-o+e.theta);return 0r.r2,o=(this.gradientTransform||fabric.iMatrix).concat(),a=-this.offsetX,c=-this.offsetY,h=!!e.additionalTransform,l="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(n.sort(function(t,e){return t.offset-e.offset}),"objectBoundingBox"==l?(a/=t.width,c/=t.height):(a+=t.width/2,c+=t.height/2),"path"===t.type&&"percentage"!==this.gradientUnits&&(a-=t.pathOffset.x,c-=t.pathOffset.y),o[4]-=a,o[5]-=c,t='id="SVGID_'+this.id+'" gradientUnits="'+l+'"',t+=' gradientTransform="'+(h?e.additionalTransform+" ":"")+fabric.util.matrixToSVG(o)+'" ',"linear"===this.type?i=["\n']:"radial"===this.type&&(i=["\n']),"radial"===this.type){if(s)for((n=n.concat()).reverse(),f=0,d=n.length;f\n')}return i.push("linear"===this.type?"\n":"\n"),i.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var o=t.fabric||(t.fabric={}),a=o.util.toFixed;o.Shadow?o.warn("fabric.Shadow is already defined."):(o.Shadow=o.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){for(var e in t="string"==typeof t?this._parseShadow(t):t)this[e]=t[e];this.id=o.Object.__uid++},_parseShadow:function(t){var t=t.trim(),e=o.Shadow.reOffsetsAndBlur.exec(t)||[];return{color:(t.replace(o.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseFloat(e[1],10)||0,offsetY:parseFloat(e[2],10)||0,blur:parseFloat(e[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var e=40,i=40,r=o.Object.NUM_FRACTION_DIGITS,n=o.util.rotateVector({x:this.offsetX,y:this.offsetY},o.util.degreesToRadians(-t.angle)),s=new o.Color(this.color);return t.width&&t.height&&(e=100*a((Math.abs(n.x)+this.blur)/t.width,r)+20,i=100*a((Math.abs(n.y)+this.blur)/t.height,r)+20),t.flipX&&(n.x*=-1),t.flipY&&(n.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke,nonScaling:this.nonScaling};var e={},i=o.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach(function(t){this[t]!==i[t]&&(e[t]=this[t])},this),e}}),o.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/)}("undefined"!=typeof exports?exports:this),function(){"use strict";var n,t,h,a,s,o,i,r,e;fabric.StaticCanvas?fabric.warn("fabric.StaticCanvas is already defined."):(n=fabric.util.object.extend,t=fabric.util.getElementOffset,h=fabric.util.removeFromArray,a=fabric.util.toFixed,s=fabric.util.transformPoint,o=fabric.util.invertTransform,i=fabric.util.getNodeCanvas,r=fabric.util.createCanvasElement,e=new Error("Could not initialize `canvas` element"),fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e=e||{},this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("\n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,r=e.width||this.width,n=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',o=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+a(-i[4]/i[0],o)+" "+a(-i[5]/i[3],o)+" "+a(this.width/i[0],o)+" "+a(this.height/i[3],o)+'" '),t.push("\n',"Created with Fabric.js ",fabric.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),this.createSVGClipPathMarkup(e),"\n")},createSVGClipPathMarkup:function(t){var e=this.clipPath;return e?(e.clipPathId="CLIPPATH_"+fabric.Object.__uid++,'\n'+this.clipPath.toClipPathSVG(t.reviver)+"\n"):""},createSVGRefElementsMarkup:function(){var n=this;return["background","overlay"].map(function(t){var e,i,r=n[t+"Color"];if(r&&r.toLive)return t=n[t+"Vpt"],e=n.viewportTransform,i={width:n.width/(t?e[0]:1),height:n.height/(t?e[3]:1)},r.toSVG(i,{additionalTransform:t?fabric.util.matrixToSVG(e):""})}).join("")},createSVGFontFacesMarkup:function(){var t,e,i,r,n,s,o,a,c,h="",l={},u=fabric.fontPaths,f=[];for(this._objects.forEach(function t(e){f.push(e),e._objects&&e._objects.forEach(t)}),o=0,a=f.length;o',"\n",h,"","\n"].join("")},_setSVGObjects:function(t,e){for(var i,r=this._objects,n=0,s=r.length;n\n")):t.push('\n"))},sendToBack:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(e=(r=n._objects).length;e--;)i=r[e],h(this._objects,i),this._objects.unshift(i);else h(this._objects,t),this._objects.unshift(t);return this.renderOnAddRemove&&this.requestRenderAll(),this},bringToFront:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(r=n._objects,e=0;e"}}),n(fabric.StaticCanvas.prototype,fabric.Observable),n(fabric.StaticCanvas.prototype,fabric.Collection),n(fabric.StaticCanvas.prototype,fabric.DataURLExporter),n(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=r();if(!e||!e.getContext)return null;e=e.getContext("2d");return!e||"setLineDash"!==t?null:void 0!==e.setLineDash}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject,fabric.isLikelyNode&&(fabric.StaticCanvas.prototype.createPNGStream=function(){var t=i(this.lowerCanvasEl);return t&&t.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(t){var e=i(this.lowerCanvasEl);return e&&e.createJPEGStream(t)}))}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeMiterLimit:10,strokeDashArray:null,limitedToCanvasSize:!1,_setBrushStyles:function(t){t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.miterLimit=this.strokeMiterLimit,t.lineJoin=this.strokeLineJoin,t.setLineDash(this.strokeDashArray||[])},_saveAndTransform:function(t){var e=this.canvas.viewportTransform;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5])},_setShadow:function(){var t,e,i,r;this.shadow&&(t=this.canvas,e=this.shadow,i=t.contextTop,r=t.getZoom(),t&&t._isRetinaScaling()&&(r*=fabric.devicePixelRatio),i.shadowColor=e.color,i.shadowBlur=e.blur*r,i.shadowOffsetX=e.offsetX*r,i.shadowOffsetY=e.offsetY*r)},needsFullRender:function(){return new fabric.Color(this.color).getAlpha()<1||!!this.shadow},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0},_isOutSideCanvas:function(t){return t.x<0||t.x>this.canvas.getWidth()||t.y<0||t.y>this.canvas.getHeight()}}),fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{decimate:.4,drawStraightLine:!1,straightLineKey:"shiftKey",initialize:function(t){this.canvas=t,this._points=[]},needsFullRender:function(){return this.callSuper("needsFullRender")||this._hasStraightLine},_drawSegment:function(t,e,i){i=e.midPointFrom(i);return t.quadraticCurveTo(e.x,e.y,i.x,i.y),i},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],this._prepareForDrawing(t),this._captureDrawingPath(t),this._render())},onMouseMove:function(t,e){var i;this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],!0===this.limitedToCanvasSize&&this._isOutSideCanvas(t)||this._captureDrawingPath(t)&&1"},getObjectScaling:function(){if(!this.group)return{scaleX:this.scaleX,scaleY:this.scaleY};var t=g.util.qrDecompose(this.calcTransformMatrix());return{scaleX:Math.abs(t.scaleX),scaleY:Math.abs(t.scaleY)}},getTotalObjectScaling:function(){var t,e,i=this.getObjectScaling(),r=i.scaleX,i=i.scaleY;return this.canvas&&(r*=(t=this.canvas.getZoom())*(e=this.canvas.getRetinaScaling()),i*=t*e),{scaleX:r,scaleY:i}},getObjectOpacity:function(){var t=this.opacity;return this.group&&(t*=this.group.getObjectOpacity()),t},_set:function(t,e){var i=this[t]!==e;return("scaleX"===t||"scaleY"===t)&&(e=this._constrainScale(e)),"scaleX"===t&&e<0?(this.flipX=!this.flipX,e*=-1):"scaleY"===t&&e<0?(this.flipY=!this.flipY,e*=-1):"shadow"!==t||!e||e instanceof g.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",e):e=new g.Shadow(e),this[t]=e,i&&(e=this.group&&this.group.isOnACache(),-1=t.x&&i.left+i.width<=e.x&&i.top>=t.y&&i.top+i.height<=e.y},containsPoint:function(t,e,i,r){i=this._getCoords(i,r),e=e||this._getImageLines(i),r=this._findCrossPoints(t,e);return 0!==r&&r%2==1},isOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.getCoords(!0,t).some(function(t){return t.x<=i.x&&t.x>=e.x&&t.y<=i.y&&t.y>=e.y})||(!!this.intersectsWithRect(e,i,!0,t)||this._containsCenterOfCanvas(e,i,t))},_containsCenterOfCanvas:function(t,e,i){t={x:(t.x+e.x)/2,y:(t.y+e.y)/2};return!!this.containsPoint(t,null,!0,i)},isPartiallyOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.intersectsWithRect(e,i,!0,t)||this.getCoords(!0,t).every(function(t){return(t.x>=i.x||t.x<=e.x)&&(t.y>=i.y||t.y<=e.y)})&&this._containsCenterOfCanvas(e,i,t)},_getImageLines:function(t){return{topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}}},_findCrossPoints:function(t,e){var i,r,n,s=0;for(n in e)if(!((r=e[n]).o.y=t.y&&r.d.y>=t.y||((r.o.x===r.d.x&&r.o.x>=t.x?r.o.x:(i=(r.d.y-r.o.y)/(r.d.x-r.o.x),-(t.y-0*t.x-(r.o.y-i*r.o.x))/(0-i)))>=t.x&&(s+=1),2!==s)))break;return s},getBoundingRect:function(t,e){t=this.getCoords(t,e);return s.makeBoundingBoxFromPoints(t)},getScaledWidth:function(){return this._getTransformedDimensions().x},getScaledHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)\n'))},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(t),{reviver:t})},toClipPathSVG:function(t){return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(t),{reviver:t})},_createBaseClipPathSVGMarkup:function(t,e){var i=(e=e||{}).reviver,e=e.additionalTransform||"",e=[this.getSvgTransform(!0,e),this.getSvgCommons()].join(""),r=t.indexOf("COMMON_PARTS");return t[r]=e,i?i(t.join("")):t.join("")},_createBaseSVGMarkup:function(t,e){var i,r=(e=e||{}).noStyle,n=e.reviver,s=r?"":'style="'+this.getSvgStyles()+'" ',o=e.withShadow?'style="'+this.getSvgFilter()+'" ':"",a=this.clipPath,c=this.strokeUniform?'vector-effect="non-scaling-stroke" ':"",h=a&&a.absolutePositioned,l=this.stroke,u=this.fill,f=this.shadow,d=[],g=t.indexOf("COMMON_PARTS"),e=e.additionalTransform;return a&&(a.clipPathId="CLIPPATH_"+fabric.Object.__uid++,i='\n'+a.toClipPathSVG(n)+"\n"),h&&d.push("\n"),d.push("\n"),o=[s,c,r?"":this.addPaintOrder()," ",e?'transform="'+e+'" ':""].join(""),t[g]=o,u&&u.toLive&&d.push(u.toSVG(this)),l&&l.toLive&&d.push(l.toSVG(this)),f&&d.push(f.toSVG(this)),a&&d.push(i),d.push(t.join("")),d.push("\n"),h&&d.push("\n"),n?n(d.join("")):d.join("")},addPaintOrder:function(){return"fill"!==this.paintFirst?' paint-order="'+this.paintFirst+'" ':""}})}(),function(){var n=fabric.util.object.extend,r="stateProperties";function s(e,t,i){var r={};i.forEach(function(t){r[t]=e[t]}),n(e[t],r,!0)}fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(t){var e="_"+(t=t||r);return Object.keys(this[e]).length\n']}}),n.Line.ATTRIBUTE_NAMES=n.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),n.Line.fromElement=function(t,e,i){i=i||{};var t=n.parseAttributes(t,n.Line.ATTRIBUTE_NAMES),r=[t.x1||0,t.y1||0,t.x2||0,t.y2||0];e(new n.Line(r,s(t,i)))},n.Line.fromObject=function(t,e){var i=r(t,!0);i.points=[t.x1,t.y1,t.x2,t.y2],n.Object._fromObject("Line",i,function(t){delete t.points,e&&e(t)},"points")})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var n=t.fabric||(t.fabric={}),s=n.util.degreesToRadians;n.Circle?n.warn("fabric.Circle is already defined."):(n.Circle=n.util.createClass(n.Object,{type:"circle",radius:0,startAngle:0,endAngle:360,cacheProperties:n.Object.prototype.cacheProperties.concat("radius","startAngle","endAngle"),_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},_toSVG:function(){var t,e,i,r=(this.endAngle-this.startAngle)%360;return 0==r?["\n']:(t=s(this.startAngle),e=s(this.endAngle),i=this.radius,['\n"])},_render:function(t){t.beginPath(),t.arc(0,0,this.radius,s(this.startAngle),s(this.endAngle),!1),this._renderPaintInOrder(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),n.Circle.ATTRIBUTE_NAMES=n.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),n.Circle.fromElement=function(t,e){var i,t=n.parseAttributes(t,n.Circle.ATTRIBUTE_NAMES);if(!("radius"in(i=t)&&0<=i.radius))throw new Error("value of `r` attribute is required and can not be negative");t.left=(t.left||0)-t.radius,t.top=(t.top||0)-t.radius,e(new n.Circle(t))},n.Circle.fromObject=function(t,e){n.Object._fromObject("Circle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var i=t.fabric||(t.fabric={});i.Triangle?i.warn("fabric.Triangle is already defined"):(i.Triangle=i.util.createClass(i.Object,{type:"triangle",width:100,height:100,_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderPaintInOrder(t)},_toSVG:function(){var t=this.width/2,e=this.height/2;return["']}}),i.Triangle.fromObject=function(t,e){return i.Object._fromObject("Triangle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var i=t.fabric||(t.fabric={}),e=2*Math.PI;i.Ellipse?i.warn("fabric.Ellipse is already defined."):(i.Ellipse=i.util.createClass(i.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:i.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']},_render:function(t){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(0,0,this.rx,0,e,!1),t.restore(),this._renderPaintInOrder(t)}}),i.Ellipse.ATTRIBUTE_NAMES=i.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),i.Ellipse.fromElement=function(t,e){t=i.parseAttributes(t,i.Ellipse.ATTRIBUTE_NAMES);t.left=(t.left||0)-t.rx,t.top=(t.top||0)-t.ry,e(new i.Ellipse(t))},i.Ellipse.fromObject=function(t,e){i.Object._fromObject("Ellipse",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var r=t.fabric||(t.fabric={}),n=r.util.object.extend;r.Rect?r.warn("fabric.Rect is already defined"):(r.Rect=r.util.createClass(r.Object,{stateProperties:r.Object.prototype.stateProperties.concat("rx","ry"),type:"rect",rx:0,ry:0,cacheProperties:r.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t){var e=this.rx?Math.min(this.rx,this.width/2):0,i=this.ry?Math.min(this.ry,this.height/2):0,r=this.width,n=this.height,s=-this.width/2,o=-this.height/2,a=0!==e||0!==i,c=.4477152502;t.beginPath(),t.moveTo(s+e,o),t.lineTo(s+r-e,o),a&&t.bezierCurveTo(s+r-c*e,o,s+r,o+c*i,s+r,o+i),t.lineTo(s+r,o+n-i),a&&t.bezierCurveTo(s+r,o+n-c*i,s+r-c*e,o+n,s+r-e,o+n),t.lineTo(s+e,o+n),a&&t.bezierCurveTo(s+c*e,o+n,s,o+n-c*i,s,o+n-i),t.lineTo(s,o+i),a&&t.bezierCurveTo(s,o+c*i,s+c*e,o,s+e,o),t.closePath(),this._renderPaintInOrder(t)},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']}}),r.Rect.ATTRIBUTE_NAMES=r.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),r.Rect.fromElement=function(t,e,i){if(!t)return e(null);i=i||{};t=r.parseAttributes(t,r.Rect.ATTRIBUTE_NAMES),t.left=t.left||0,t.top=t.top||0,t.height=t.height||0,t.width=t.width||0,i=new r.Rect(n(i?r.util.object.clone(i):{},t));i.visible=i.visible&&0\n']},commonRender:function(t){var e,i=this.points.length,r=this.pathOffset.x,n=this.pathOffset.y;if(!i||isNaN(this.points[i-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-r,this.points[0].y-n);for(var s=0;s"},toObject:function(t){return r(this.callSuper("toObject",t),{path:this.path.map(function(t){return t.slice()})})},toDatalessObject:function(t){t=this.toObject(["sourcePath"].concat(t));return t.sourcePath&&delete t.path,t},_toSVG:function(){return["\n"]},_getOffsetTransform:function(){var t=f.Object.NUM_FRACTION_DIGITS;return" translate("+e(-this.pathOffset.x,t)+", "+e(-this.pathOffset.y,t)+")"},toClipPathSVG:function(t){var e=this._getOffsetTransform();return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},toSVG:function(t){var e=this._getOffsetTransform();return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},complexity:function(){return this.path.length},_calcDimensions:function(){for(var t,e,i=[],r=[],n=0,s=0,o=0,a=0,c=0,h=this.path.length;c"},addWithUpdate:function(t){var e=!!this.group;return this._restoreObjectsState(),o.util.resetObjectTransform(this),t&&(e&&o.util.removeTransformFromObject(t,this.group.calcTransformMatrix()),this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,e?this.group.addWithUpdate():this.setCoords(),this},removeWithUpdate:function(t){return this._restoreObjectsState(),o.util.resetObjectTransform(this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.setCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group},_set:function(t,e){var i=this._objects.length;if(this.useSetOnGroup)for(;i--;)this._objects[i].setOnGroup(t,e);if("canvas"===t)for(;i--;)this._objects[i]._set(t,e);o.Object.prototype._set.call(this,t,e)},toObject:function(r){var n=this.includeDefaultValues,t=this._objects.filter(function(t){return!t.excludeFromExport}).map(function(t){var e=t.includeDefaultValues,i=(t.includeDefaultValues=n,t.toObject(r));return t.includeDefaultValues=e,i}),e=o.Object.prototype.toObject.call(this,r);return e.objects=t,e},toDatalessObject:function(r){var n,t=this.sourcePath,e=(t=t||(n=this.includeDefaultValues,this._objects.map(function(t){var e=t.includeDefaultValues,i=(t.includeDefaultValues=n,t.toDatalessObject(r));return t.includeDefaultValues=e,i})),o.Object.prototype.toDatalessObject.call(this,r));return e.objects=t,e},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=o.Object.prototype.shouldCache.call(this);if(t)for(var e=0,i=this._objects.length;e\n"],i=0,r=this._objects.length;i\n"),e},getSvgStyles:function(){var t=void 0!==this.opacity&&1!==this.opacity?"opacity: "+this.opacity+";":"",e=this.visible?"":" visibility: hidden;";return[t,this.getSvgFilter(),e].join("")},toClipPathSVG:function(t){for(var e=[],i=0,r=this._objects.length;i"},shouldCache:function(){return!1},isOnACache:function(){return!1},_renderControls:function(t,e,i){t.save(),t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,this.callSuper("_renderControls",t,e),void 0===(i=i||{}).hasControls&&(i.hasControls=!1),i.forActiveSelection=!0;for(var r=0,n=this._objects.length;r\n','\t\n',"\n"),a=' clip-path="url(#imageCrop_'+e+')" '),this.imageSmoothing||(c='" image-rendering="optimizeSpeed'),r.push("\t\n"),(this.stroke||this.strokeDashArray)&&(e=this.fill,this.fill=null,t=["\t\n'],this.fill=e),"fill"!==this.paintFirst?i.concat(t,r):i.concat(r,t)):[]},getSrc:function(t){t=t?this._element:this._originalElement;return t?t.toDataURL?t.toDataURL():this.srcFromAttribute?t.getAttribute("src"):t.src:this.src||""},setSrc:function(t,i,r){return fabric.util.loadImage(t,function(t,e){this.setElement(t,r),this._setWidthHeight(),i&&i(this,e)},this,r&&r.crossOrigin),this},toString:function(){return'#'},applyResizeFilters:function(){var t=this.resizeFilter,e=this.minimumScaleTrigger,i=this.getTotalObjectScaling(),r=i.scaleX,i=i.scaleY,n=this._filteredEl||this._originalElement;if(this.group&&this.set("dirty",!0),!t||e=t,o=["highp","mediump","lowp"],a=0;a<3;a++)if(r=void 0,i="precision "+(i=o[a])+" float;\nvoid main(){}",r=(e=s).createShader(e.FRAGMENT_SHADER),e.shaderSource(r,i),e.compileShader(r),!!e.getShaderParameter(r,e.COMPILE_STATUS)){fabric.webGlPrecision=o[a];break}}return this.isSupported=n},(fabric.WebglFilterBackend=t).prototype={tileSize:2048,resources:{},setupGLContext:function(t,e){this.dispose(),this.createWebGLCanvas(t,e),this.aPosition=new Float32Array([0,0,0,1,1,0,1,1]),this.chooseFastestCopyGLTo2DMethod(t,e)},chooseFastestCopyGLTo2DMethod:function(t,e){var i=void 0!==window.performance;try{new ImageData(1,1),s=!0}catch(t){s=!1}var r="undefined"!=typeof ArrayBuffer,n="undefined"!=typeof Uint8ClampedArray;if(i&&s&&r&&n){var i=fabric.util.createCanvasElement(),s=new ArrayBuffer(t*e*4);if(fabric.forceGLPutImageData)return this.imageBuffer=s,void(this.copyGLTo2D=copyGLTo2DPutImageData);r={imageBuffer:s,destinationWidth:t,destinationHeight:e,targetCanvas:i};i.width=t,i.height=e,n=window.performance.now(),copyGLTo2DDrawImage.call(r,this.gl,r),t=window.performance.now()-n,n=window.performance.now(),copyGLTo2DPutImageData.call(r,this.gl,r),window.performance.now()-n 0.0) {\n"+this.fragmentSource[t]+"}\n}"},retrieveShader:function(t){var e,i=this.type+"_"+this.mode;return t.programCache.hasOwnProperty(i)||(e=this.buildSource(this.mode),t.programCache[i]=this.createProgram(t.context,e)),t.programCache[i]},applyTo2d:function(t){for(var e,i,r,n=t.imageData.data,s=n.length,o=1-this.alpha,t=new u.Color(this.color).getSource(),a=t[0]*this.alpha,c=t[1]*this.alpha,h=t[2]*this.alpha,l=0;l'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){var e=this.path;e&&!e.isNotVisible()&&e._render(t),this._setTextStyles(t),this._renderTextLinesBackground(t),this._renderTextDecoration(t,"underline"),this._renderText(t),this._renderTextDecoration(t,"overline"),this._renderTextDecoration(t,"linethrough")},_renderText:function(t){"stroke"===this.paintFirst?(this._renderTextStroke(t),this._renderTextFill(t)):(this._renderTextFill(t),this._renderTextStroke(t))},_setTextStyles:function(t,e,i){if(t.textBaseline="alphabetical",this.path)switch(this.pathAlign){case"center":t.textBaseline="middle";break;case"ascender":t.textBaseline="top";break;case"descender":t.textBaseline="bottom"}t.font=this._getFontDeclaration(e,i)},calcTextWidth:function(){for(var t=this.getLineWidth(0),e=1,i=this._textLines.length;ethis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=t):(this.selectionStart=t,this.selectionEnd=this.__selectionStartOnMouseDown),this.selectionStart===e&&this.selectionEnd===i||(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection())))},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},fromStringToGraphemeSelection:function(t,e,i){var r=i.slice(0,t),r=fabric.util.string.graphemeSplit(r).length;if(t===e)return{selectionStart:r,selectionEnd:r};i=i.slice(t,e);return{selectionStart:r,selectionEnd:r+fabric.util.string.graphemeSplit(i).length}},fromGraphemeToStringSelection:function(t,e,i){var r=i.slice(0,t).join("").length;return t===e?{selectionStart:r,selectionEnd:r}:{selectionStart:r,selectionEnd:r+i.slice(t,e).join("").length}},_updateTextarea:function(){var t;this.cursorOffsetCache={},this.hiddenTextarea&&(this.inCompositionMode||(t=this.fromGraphemeToStringSelection(this.selectionStart,this.selectionEnd,this._text),this.hiddenTextarea.selectionStart=t.selectionStart,this.hiddenTextarea.selectionEnd=t.selectionEnd),this.updateTextareaPosition())},updateFromTextArea:function(){var t;this.hiddenTextarea&&(this.cursorOffsetCache={},this.text=this.hiddenTextarea.value,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),t=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),this.selectionEnd=this.selectionStart=t.selectionEnd,this.inCompositionMode||(this.selectionStart=t.selectionStart),this.updateTextareaPosition())},updateTextareaPosition:function(){var t;this.selectionStart===this.selectionEnd&&(t=this._calcTextareaPosition(),this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top)},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.inCompositionMode?this.compositionStart:this.selectionStart,e=this._getCursorBoundaries(t),t=this.get2DCursorLocation(t),i=t.lineIndex,t=t.charIndex,i=this.getValueOfPropertyAt(i,t,"fontSize")*this.lineHeight,t=e.leftOffset,r=this.calcTransformMatrix(),t={x:e.left+t,y:e.top+e.topOffset+i},e=this.canvas.getRetinaScaling(),n=this.canvas.upperCanvasEl,s=n.width/e,e=n.height/e,o=s-i,a=e-i,s=n.clientWidth/s,n=n.clientHeight/e,t=fabric.util.transformPoint(t,r);return(t=fabric.util.transformPoint(t,this.canvas.viewportTransform)).x*=s,t.y*=n,t.x<0&&(t.x=0),t.x>o&&(t.x=o),t.y<0&&(t.y=0),t.y>a&&(t.y=a),t.x+=this.canvas._offset.left,t.y+=this.canvas._offset.top,{left:t.x+"px",top:t.y+"px",fontSize:i+"px",charHeight:i}},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,selectable:this.selectable,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){this._savedProps&&(this.hoverCursor=this._savedProps.hoverCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.selectable=this._savedProps.selectable,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text,e=this.hiddenTextarea;return this.selected=!1,this.isEditing=!1,this.selectionEnd=this.selectionStart,e&&(e.blur&&e.blur(),e.parentNode&&e.parentNode.removeChild(e)),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},removeStyleFromTo:function(t,e){var t=this.get2DCursorLocation(t,!0),e=this.get2DCursorLocation(e,!0),i=t.lineIndex,r=t.charIndex,n=e.lineIndex,s=e.charIndex;if(i!==n){if(this.styles[i])for(l=r;lt?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown)},onMouseDown:function(t){var e;this.canvas&&(this.__newClickTime=+new Date,e=t.pointer,this.isTripleClick(e)&&(this.fire("tripleclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected)},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},doubleClickHandler:function(t){this.isEditing&&this.selectWord(this.getSelectionStartFromPointer(t.e))},tripleClickHandler:function(t){this.isEditing&&this.selectLine(this.getSelectionStartFromPointer(t.e))},initClicks:function(){this.on("mousedblclick",this.doubleClickHandler),this.on("tripleclick",this.tripleClickHandler)},_mouseDownHandler:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.__isMousedown=!0,this.selected&&(this.inCompositionMode=!1,this.setCursorByClick(t.e)),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection()))},_mouseDownHandlerBefore:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.selected=this===this.canvas._activeObject)},initMousedownHandler:function(){this.on("mousedown",this._mouseDownHandler),this.on("mousedown:before",this._mouseDownHandlerBefore)},initMouseupHandler:function(){this.on("mouseup",this.mouseUpHandler)},mouseUpHandler:function(t){if(this.__isMousedown=!1,!(!this.editable||this.group||t.transform&&t.transform.actionPerformed||t.e.button&&1!==t.e.button)){if(this.canvas){var e=this.canvas._activeObject;if(e&&e!==this)return}this.__lastSelected&&!this.__corner?(this.selected=!1,this.__lastSelected=!1,this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()):this.selected=!0}},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,r=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,r,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e=this.getLocalPointer(t),i=0,r=0,n=0,s=0,o=0,a=0,c=this._textLines.length;athis._text.length?this._text.length:t}}),fabric.util.object.extend(fabric.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.setAttribute("autocorrect","off"),this.hiddenTextarea.setAttribute("autocomplete","off"),this.hiddenTextarea.setAttribute("spellcheck","false"),this.hiddenTextarea.setAttribute("data-fabric-hiddentextarea",""),this.hiddenTextarea.setAttribute("wrap","off");var t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="position: absolute; top: "+t.top+"; left: "+t.left+"; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; paddingーtop: "+t.fontSize+";",(this.hiddenTextareaContainer||fabric.document.body).appendChild(this.hiddenTextarea),fabric.util.addListener(this.hiddenTextarea,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(this.hiddenTextarea,"keyup",this.onKeyUp.bind(this)),fabric.util.addListener(this.hiddenTextarea,"input",this.onInput.bind(this)),fabric.util.addListener(this.hiddenTextarea,"copy",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"cut",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"paste",this.paste.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionstart",this.onCompositionStart.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionupdate",this.onCompositionUpdate.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionend",this.onCompositionEnd.bind(this)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},keysMap:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown"},keysMapRtl:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorLeft",36:"moveCursorRight",37:"moveCursorRight",38:"moveCursorUp",39:"moveCursorLeft",40:"moveCursorDown"},ctrlKeysMapUp:{67:"copy",88:"cut"},ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){var e="rtl"===this.direction?this.keysMapRtl:this.keysMap;if(t.keyCode in e)this[e[t.keyCode]](t);else{if(!(t.keyCode in this.ctrlKeysMapDown&&(t.ctrlKey||t.metaKey)))return;this[this.ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),33<=t.keyCode&&t.keyCode<=40?(this.inCompositionMode=!1,this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.requestRenderAll()}},onKeyUp:function(t){!this.isEditing||this._copyDone||this.inCompositionMode?this._copyDone=!1:t.keyCode in this.ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this.ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.requestRenderAll())},onInput:function(t){var e=this.fromPaste;if(this.fromPaste=!1,t&&t.stopPropagation(),this.isEditing){var i,r,n,t=this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,s=this._text.length,o=t.length,a=o-s,c=this.selectionStart,h=this.selectionEnd,l=c!==h;if(""===this.hiddenTextarea.value)return this.styles={},this.updateFromTextArea(),this.fire("changed"),void(this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll()));var u=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),f=c>u.selectionStart;l?(i=this._text.slice(c,h),a+=h-c):o=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){t=this["get"+t+"CursorOffset"](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(t):this.moveCursorWithoutShift(t),0!==t&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return t<0?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var r;if(t.altKey)r=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;r=this["findLineBoundary"+i](this[e])}if(void 0!==r&&this[e]!==r)return this[e]=r,!0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){t="moveCursor"+t+"With";this._currentCursorOpacity=1,e.shiftKey?t+="Shift":t+="outShift",this[t](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this._text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t,e){this.removeStyleFromTo(t,e=void 0===e?t+1:e),this._text.splice(t,e-t),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()},insertChars:function(t,e,i,r){i<(r=void 0===r?i:r)&&this.removeStyleFromTo(i,r);t=fabric.util.string.graphemeSplit(t);this.insertNewStyleBlock(t,i,e),this._text=[].concat(this._text.slice(0,i),t,this._text.slice(r)),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()}}),function(){var a=fabric.util.toFixed,c=/ +/g;fabric.util.object.extend(fabric.Text.prototype,{_toSVG:function(){var t=this._getSVGLeftTopOffsets(),t=this._getSVGTextAndBg(t.textTop,t.textLeft);return this._wrapSVGTextAndBg(t)},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,noStyle:!0,withShadow:!0})},_getSVGLeftTopOffsets:function(){return{textLeft:-this.width/2,textTop:-this.height/2,lineTop:this.getHeightOfLine(0)}},_wrapSVGTextAndBg:function(t){var e=this.getSvgTextDecoration(this);return[t.textBgRects.join(""),'\t\t",t.textSpans.join(""),"\n"]},_getSVGTextAndBg:function(t,e){var i,r=[],n=[],s=t;this._setSVGBg(n);for(var o=0,a=this._textLines.length;o",fabric.util.string.escapeXml(t),""].join("")},_setSVGTextLineText:function(t,e,i,r){var n,s,o,a,c=this.getHeightOfLine(e),h=-1!==this.textAlign.indexOf("justify"),l="",u=0,f=this._textLines[e];r+=c*(1-this._fontSizeFraction)/this.lineHeight;for(var d=0,g=f.length-1;d<=g;d++)a=d===g||this.charSpacing,l+=f[d],o=this.__charBounds[e][d],0===u?(i+=o.kernedWidth-o.width,u+=o.width):u+=o.kernedWidth,(a=h&&!a&&this._reSpaceAndTab.test(f[d])?!0:a)||(n=n||this.getCompleteStyleDeclaration(e,d),s=this.getCompleteStyleDeclaration(e,d+1),a=fabric.util.hasStyleChanged(n,s,!0)),a&&(o=this._getStyleDeclaration(e,d)||{},t.push(this._createTextCharSpan(l,o,i,r)),l="",n=s,i+=u,u=0)},_pushTextBgRect:function(t,e,i,r,n,s){var o=fabric.Object.NUM_FRACTION_DIGITS;t.push("\t\t\n')},_setSVGTextLineBg:function(t,e,i,r){for(var n,s,o=this._textLines[e],a=this.getHeightOfLine(e)/this.lineHeight,c=0,h=0,l=this.getValueOfPropertyAt(e,0,"textBackgroundColor"),u=0,f=o.length;uthis.width&&this._set("width",this.dynamicMinWidth),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.height=this.calcTextHeight(),this.saveState({propertySet:"_dimensionAffectingProps"}))},_generateStyleMap:function(t){for(var e=0,i=0,r=0,n={},s=0;sthis.dynamicMinWidth&&(this.dynamicMinWidth=g-m+r),c},isEndOfWrapping:function(t){return!this._styleMap[t+1]||this._styleMap[t+1].line!==this._styleMap[t].line},missingNewlineOffset:function(t){return!this.splitByGrapheme||this.isEndOfWrapping(t)?1:0},_splitTextIntoLines:function(t){for(var t=b.Text.prototype._splitTextIntoLines.call(this,t),e=this._wrapText(t.lines,this.width),i=new Array(e.length),r=0;r { @@ -46,15 +47,15 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin console.log('Flow name from URL path:', paths[1]); return paths[1]; } - // Default flow name console.log('Default flow name: linker'); return 'linker'; } - // let response = await api.fetchApi("/manager/reboot"); + const flowName = getFlowName(); const client_id = uuidv4(); const flowConfig = await fetchflowConfig(flowName); let workflow = await fetchWorkflow(flowName); + let canvasLoader; const seeders = []; initializeWebSocket(client_id); @@ -232,28 +233,31 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin } imageLoaderComp(flowConfig, workflow); - - function updateQueueDisplay(jobQueue) { - const queueDisplay = document.getElementById('queue-display'); - if (!queueDisplay) { - console.warn('queue-display element not found in the DOM.'); - return; + + const initCanvas = async () => { + canvasLoader = new CanvasLoader('imageCanvas', flowConfig); + await canvasLoader.initPromise; // Wait for initialization to complete + + if (canvasLoader.isInitialized) { + console.log("Canvas initialized successfully with selected plugins."); + } else { + console.log("Canvas was not initialized due to missing flowConfig fields."); } - if (jobQueue.length > 0) { - queueDisplay.textContent = `${jobQueue.length}`; + }; + + initCanvas(); - } else { - queueDisplay.textContent = ''; + + async function queue() { + + if (canvasLoader && canvasLoader.isInitialized) { + await CanvasComponent(flowConfig, workflow, canvasLoader); + } else { + console.warn("Canvas is not initialized. Skipping CanvasComponent."); } - // if (jobQueue.length > 0) { - // const jobIds = jobQueue.map(job => job.id).join(', '); - // queueDisplay.textContent += ` (Job IDs: ${jobIds})`; - // } - } + console.log("Queueing workflow:", workflow); - async function queue() { - console.log("Queueing workflow:" , workflow); if (flowConfig.prompts) { flowConfig.prompts.forEach(pathConfig => { const { id } = pathConfig; @@ -266,11 +270,13 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin } }); } + const jobId = StateManager.incrementJobId(); const job = { id: jobId, workflow: { ...workflow } }; StateManager.addJob(job); console.log(`Added job to queue. Job ID: ${jobId}`); console.log("Current queue:", StateManager.getJobQueue()); + console.log("queued workflow:", workflow); updateQueueDisplay(StateManager.getJobQueue()); @@ -324,7 +330,27 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin } catch (error) { console.error('Error processing prompt:', error); throw error; + } finally { + // hideSpinner(); + } + } + function updateQueueDisplay(jobQueue) { + const queueDisplay = document.getElementById('queue-display'); + if (!queueDisplay) { + console.warn('queue-display element not found in the DOM.'); + return; + } + if (jobQueue.length > 0) { + queueDisplay.textContent = `${jobQueue.length}`; + + } else { + queueDisplay.textContent = ''; } + + // if (jobQueue.length > 0) { + // const jobIds = jobQueue.map(job => job.id).join(', '); + // queueDisplay.textContent += ` (Job IDs: ${jobIds})`; + // } } async function interrupt() { @@ -356,6 +382,7 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin if (!response.ok) { throw new Error('Failed to interrupt the process.'); } + const result = await response.json(); console.log('Interrupted:', result); } catch (error) { @@ -398,3 +425,4 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin }); }); })(window, document, undefined); + diff --git a/web/core/media/git/flow_5.jpg b/web/core/media/git/flow_5.jpg new file mode 100644 index 0000000..b6f7fc1 Binary files /dev/null and b/web/core/media/git/flow_5.jpg differ diff --git a/web/core/media/ui/paintree.png b/web/core/media/ui/paintree.png new file mode 100644 index 0000000..37b562d Binary files /dev/null and b/web/core/media/ui/paintree.png differ