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 = `
+
+
+ `;
+
+ 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 = `
+
Brush Size:
+
+
+
Brush Opacity:
+
+
+
Brush Color:
+
+
+
+
+
+ `;
+ } 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 = `
+
+ Image Color:
+
+
+ Width (px):
+
+
+ Height (px):
+
+
+
+ Add Image
+
+
+ `;
+
+ 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 = `
+
+
+ Mask Color Fill
+
+
+
+
+
+
+
+
+
+
Mask Order
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ 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"].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