stable-diffusion-webui icon indicating copy to clipboard operation
stable-diffusion-webui copied to clipboard

[Feature Request]: canvas zoom from the forge repository deobsufcated

Open richrobber2 opened this issue 1 year ago • 2 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues and checked the recent builds/commits

What would your feature do ?

you would implement this yourself but i did the work to deobsufcate the code from the canvas zoom, I'm not entirely sure if they licensed the code but I'm pretty sure its not

Edit: forgot to mention that this is the js code for the canvas from the forge project there is more info about the licensing in the comments, there is nothing for the js file itself but i would still recommend asking about it on the forge repo, even though i tried a few times to get info about it, i get no replies granted it was all on the one issue i created there

Proposed workflow

Edit: added js formating for ease of reading from issue

/**
 * Class to bind a textarea element with Gradio's backend.
 */
class GradioTextAreaBind {
    /**
     * Initializes a binding between a textarea and Gradio.
     * @param {string} elementId - The ID of the target element.
     * @param {string} className - The class name of the target element.
     */
    constructor(elementId, className) {
        this.target = document.querySelector(`#${elementId}.${className} textarea`);
        this.syncLock = false;
        this.previousValue = '';
    }

    /**
     * Sets the value of the textarea and dispatches an input event.
     * @param {string} newValue - The new value to set.
     */
    setValue(newValue) {
        if (this.syncLock) return;

        this.syncLock = true;
        this.target.value = newValue;
        this.previousValue = newValue;

        const inputEvent = new Event('input', { bubbles: true });
        Object.defineProperty(inputEvent, 'target', { value: this.target });
        this.target.dispatchEvent(inputEvent);

        this.syncLock = false;
    }

    /**
     * Listens for changes in the textarea and triggers a callback.
     * @param {Function} callback - The function to call on value change.
     */
    listen(callback) {
        setInterval(() => {
            if (this.target.value !== this.previousValue) {
                this.previousValue = this.target.value;

                if (this.syncLock) return;

                this.syncLock = true;
                callback(this.target.value);
                this.syncLock = false;
            }
        }, 100); // Checks every 100ms
    }
}

/**
 * Class to manage a drawing canvas with image manipulation features.
 */
class ForgeCanvas {
    /**
     * Initializes the ForgeCanvas with various configurations.
     * @param {string} uuid - Unique identifier for the canvas.
     * @param {boolean} noUpload - Disable upload functionality.
     * @param {boolean} noScribbles - Disable scribble functionality.
     * @param {boolean} contrastScribbles - Enable contrast in scribbles.
     * @param {number} initialHeight - Initial height of the canvas.
     * @param {string} scribbleColor - Default scribble color.
     * @param {boolean} scribbleColorFixed - Fix the scribble color.
     * @param {number} scribbleWidth - Width of the scribble.
     * @param {boolean} scribbleWidthFixed - Fix the scribble width.
     * @param {number} scribbleAlpha - Transparency of the scribble.
     * @param {boolean} scribbleAlphaFixed - Fix the scribble transparency.
     * @param {number} scribbleSoftness - Softness of the scribble.
     * @param {boolean} scribbleSoftnessFixed - Fix the scribble softness.
     */
    constructor(
        uuid,
        noUpload = false,
        noScribbles = false,
        contrastScribbles = false,
        initialHeight = 512,
        scribbleColor = '#000000',
        scribbleColorFixed = false,
        scribbleWidth = 4,
        scribbleWidthFixed = false,
        scribbleAlpha = 100,
        scribbleAlphaFixed = false,
        scribbleSoftness = 0,
        scribbleSoftnessFixed = false
    ) {
        this.gradioConfig = typeof gradio_config !== 'undefined' ? gradio_config : null; // Ensure gradio_config is defined
        this.uuid = uuid;
        this.noUpload = noUpload;
        this.noScribbles = noScribbles;
        this.contrastScribbles = contrastScribbles;
        this.initialHeight = initialHeight;
        this.img = null;
        this.imgX = 0;
        this.imgY = 0;
        this.originalWidth = 0;
        this.originalHeight = 0;
        this.imgScale = 1;
        this.dragging = false;
        this.draggedJustNow = false;
        this.resizing = false;
        this.drawing = false;
        this.scribbleColor = scribbleColor;
        this.scribbleWidth = scribbleWidth;
        this.scribbleAlpha = scribbleAlpha;
        this.scribbleSoftness = scribbleSoftness;
        this.scribbleColorFixed = scribbleColorFixed;
        this.scribbleWidthFixed = scribbleWidthFixed;
        this.scribbleAlphaFixed = scribbleAlphaFixed;
        this.scribbleSoftnessFixed = scribbleSoftnessFixed;
        this.history = [];
        this.historyIndex = -1;
        this.maximized = false;
        this.originalState = {};
        this.contrastPattern = null;
        this.pointerInsideContainer = false;
        this.tempCanvas = document.createElement('canvas');
        this.tempDrawPoints = [];
        this.tempDrawBackground = null;
        this.backgroundGradioBind = new GradioTextAreaBind(this.uuid, 'logical_image_background');
        this.foregroundGradioBind = new GradioTextAreaBind(this.uuid, 'logical_image_foreground');
        this.start();
    }

    /**
     * Initializes event listeners and sets up the canvas.
     */
    start() {
        const self = this;

        // Get DOM elements
        const imageContainer = document.getElementById(`imageContainer_${self.uuid}`);
        const imageElement = document.getElementById(`image_${self.uuid}`);
        const resizeLine = document.getElementById(`resizeLine_${self.uuid}`);
        const container = document.getElementById(`container_${self.uuid}`);
        const toolbar = document.getElementById(`toolbar_${self.uuid}`);
        const uploadButton = document.getElementById(`uploadButton_${self.uuid}`);
        const resetButton = document.getElementById(`resetButton_${self.uuid}`);
        const centerButton = document.getElementById(`centerButton_${self.uuid}`);
        const removeButton = document.getElementById(`removeButton_${self.uuid}`);
        const undoButton = document.getElementById(`undoButton_${self.uuid}`);
        const redoButton = document.getElementById(`redoButton_${self.uuid}`);
        const drawingCanvas = document.getElementById(`drawingCanvas_${self.uuid}`);
        const maxButton = document.getElementById(`maxButton_${self.uuid}`);
        const minButton = document.getElementById(`minButton_${self.uuid}`);
        const scribbleIndicator = document.getElementById(`scribbleIndicator_${self.uuid}`);
        const uploadHint = document.getElementById(`uploadHint_${self.uuid}`);
        const scribbleColorInput = document.getElementById(`scribbleColor_${self.uuid}`);
        const scribbleColorBlock = document.getElementById(`scribbleColorBlock_${self.uuid}`);
        const scribbleWidthInput = document.getElementById(`scribbleWidth_${self.uuid}`);
        const widthLabel = document.getElementById(`widthLabel_${self.uuid}`);
        const scribbleWidthBlock = document.getElementById(`scribbleWidthBlock_${self.uuid}`);
        const scribbleAlphaInput = document.getElementById(`scribbleAlpha_${self.uuid}`);
        const alphaLabel = document.getElementById(`alphaLabel_${self.uuid}`);
        const scribbleAlphaBlock = document.getElementById(`scribbleAlphaBlock_${self.uuid}`);
        const scribbleSoftnessInput = document.getElementById(`scribbleSoftness_${self.uuid}`);
        const softnessLabel = document.getElementById(`softnessLabel_${self.uuid}`);
        const scribbleSoftnessBlock = document.getElementById(`scribbleSoftnessBlock_${self.uuid}`);

        // Initialize input values
        scribbleColorInput.value = self.scribbleColor;
        scribbleWidthInput.value = self.scribbleWidth;
        scribbleAlphaInput.value = self.scribbleAlpha;
        scribbleSoftnessInput.value = self.scribbleSoftness;

        // Set initial styles
        const scribbleIndicatorSize = self.scribbleWidth * 20;
        scribbleIndicator.style.width = `${scribbleIndicatorSize}px`;
        scribbleIndicator.style.height = `${scribbleIndicatorSize}px`;
        container.style.height = `${self.initialHeight}px`;
        drawingCanvas.width = imageContainer.clientWidth;
        drawingCanvas.height = imageContainer.clientHeight;

        const drawingContext = drawingCanvas.getContext('2d');
        self.drawingCanvas = drawingCanvas;

        // Handle feature toggles
        if (self.noScribbles) {
            resetButton.style.display = 'none';
            undoButton.style.display = 'none';
            redoButton.style.display = 'none';
            scribbleColorInput.style.display = 'none';
            scribbleColorBlock.style.display = 'none';
            scribbleWidthBlock.style.display = 'none';
            scribbleAlphaBlock.style.display = 'none';
            scribbleSoftnessBlock.style.display = 'none';
            scribbleIndicator.style.display = 'none';
            drawingCanvas.style.display = 'none';
        }

        if (self.noUpload) {
            uploadButton.style.display = 'none';
            uploadHint.style.display = 'none';
        }

        if (self.contrastScribbles) {
            scribbleColorBlock.style.display = 'none';
            scribbleAlphaBlock.style.display = 'none';
            scribbleSoftnessBlock.style.display = 'none';

            // Create a contrast pattern
            const patternCanvas = self.tempCanvas;
            patternCanvas.width = 20;
            patternCanvas.height = 20;
            const patternContext = patternCanvas.getContext('2d');
            patternContext.fillStyle = '#ffffff';
            patternContext.fillRect(0, 0, 10, 10);
            patternContext.fillRect(10, 10, 10, 10);
            patternContext.fillStyle = '#000000';
            patternContext.fillRect(10, 0, 10, 10);
            patternContext.fillRect(0, 10, 10, 10);
            self.contrastPattern = drawingContext.createPattern(patternCanvas, 'repeat');
            drawingCanvas.style.opacity = '0.5';
        }

        // Adjust UI based on fixed scribble properties
        if (self.contrastScribbles || (self.scribbleColorFixed && self.scribbleAlphaFixed && self.scribbleSoftnessFixed)) {
            scribbleSoftnessBlock.style.width = '100%';
            scribbleWidthInput.style.width = '100%';
            widthLabel.style.display = 'none';
        }

        if (self.scribbleColorFixed) {
            scribbleColorBlock.style.display = 'none';
        }

        if (self.scribbleWidthFixed) {
            scribbleWidthBlock.style.display = 'none';
        }

        if (self.scribbleAlphaFixed) {
            scribbleAlphaBlock.style.display = 'none';
        }

        if (self.scribbleSoftnessFixed) {
            scribbleSoftnessBlock.style.display = 'none';
        }

        // Observe container resizing
        const resizeObserver = new ResizeObserver(() => {
            self.adjustInitialPositionAndScale();
            self.drawImage();
        });
        resizeObserver.observe(container);

        // Handle file upload via input
        const imageInput = document.getElementById(`imageInput_${self.uuid}`);
        imageInput.addEventListener('change', function (event) {
            self.handleFileUpload(event.target.files[0]);
        });

        // Handle upload button click
        uploadButton.addEventListener('click', function () {
            if (self.noUpload) return;
            imageInput.click();
        });

        // Handle reset button click
        resetButton.addEventListener('click', function () {
            self.resetImage();
        });

        // Handle center button click
        centerButton.addEventListener('click', function () {
            self.adjustInitialPositionAndScale();
            self.drawImage();
        });

        // Handle remove button click
        removeButton.addEventListener('click', function () {
            self.removeImage();
        });

        // Handle undo and redo button clicks
        undoButton.addEventListener('click', function () {
            self.undo();
        });

        redoButton.addEventListener('click', function () {
            self.redo();
        });

        // Handle scribble color change
        scribbleColorInput.addEventListener('input', function () {
            self.scribbleColor = this.value;
            scribbleIndicator.style.borderColor = self.scribbleColor;
        });

        // Handle scribble width change
        scribbleWidthInput.addEventListener('input', function () {
            self.scribbleWidth = this.value;
            const newSize = self.scribbleWidth * 20;
            scribbleIndicator.style.width = `${newSize}px`;
            scribbleIndicator.style.height = `${newSize}px`;
        });

        // Handle scribble alpha change
        scribbleAlphaInput.addEventListener('input', function () {
            self.scribbleAlpha = this.value;
        });

        // Handle scribble softness change
        scribbleSoftnessInput.addEventListener('input', function () {
            self.scribbleSoftness = this.value;
        });

        // Drawing canvas events
        drawingCanvas.addEventListener('pointerdown', function (event) {
            if (!self.img || event.button !== 0 || self.noScribbles) return;

            const rect = drawingCanvas.getBoundingClientRect();
            self.drawing = true;
            drawingCanvas.style.cursor = 'crosshair';
            scribbleIndicator.style.display = 'none';
            self.tempDrawPoints = [
                [
                    (event.clientX - rect.left) / self.imgScale,
                    (event.clientY - rect.top) / self.imgScale,
                ],
            ];
            self.tempDrawBackground = drawingContext.getImageData(0, 0, drawingCanvas.width, drawingCanvas.height);
            self.handleDraw(event);
        });

        drawingCanvas.addEventListener('pointermove', function (event) {
            if (self.drawing) {
                self.handleDraw(event);
            }

            if (self.img && !self.dragging) {
                drawingCanvas.style.cursor = 'crosshair';
            }

            // Optional: Show scribble indicator
            if (self.img && !self.dragging && !self.noScribbles) {
                const containerRect = imageContainer.getBoundingClientRect();
                const indicatorOffset = self.scribbleWidth * 10;
                scribbleIndicator.style.left = `${event.clientX - containerRect.left - indicatorOffset}px`;
                scribbleIndicator.style.top = `${event.clientY - containerRect.top - indicatorOffset}px`;
                scribbleIndicator.style.display = 'block';
            }
        });

        drawingCanvas.addEventListener('pointerup', function () {
            if (self.drawing) {
                self.drawing = false;
                drawingCanvas.style.cursor = '';
                self.saveState();
            }
        });

        drawingCanvas.addEventListener('pointerleave', function () {
            if (self.drawing) {
                self.drawing = false;
                drawingCanvas.style.cursor = '';
                scribbleIndicator.style.display = 'none';
                self.saveState();
            }
        });

        // Prevent toolbar from triggering drag events
        toolbar.addEventListener('pointerdown', function (event) {
            event.stopPropagation();
        });

        // Image container events for dragging and uploading
        imageContainer.addEventListener('pointerdown', function (event) {
            const rect = imageContainer.getBoundingClientRect();
            const offsetX = event.clientX - rect.left;
            const offsetY = event.clientY - rect.top;

            if (event.button === 2 && self.isInsideImage(offsetX, offsetY)) {
                self.dragging = true;
                self.offsetX = offsetX - self.imgX;
                self.offsetY = offsetY - self.imgY;
                imageElement.style.cursor = 'grabbing';
                drawingCanvas.style.cursor = 'grabbing';
                scribbleIndicator.style.display = 'none';
            } else if (event.button === 0 && !self.img && !self.noUpload) {
                imageInput.click();
            }
        });

        imageContainer.addEventListener('pointermove', function (event) {
            if (self.dragging) {
                const rect = imageContainer.getBoundingClientRect();
                const mouseX = event.clientX - rect.left;
                const mouseY = event.clientY - rect.top;

                self.imgX = mouseX - self.offsetX;
                self.imgY = mouseY - self.offsetY;
                self.drawImage();
                self.draggedJustNow = true;
            }
        });

        imageContainer.addEventListener('pointerup', function (event) {
            if (self.dragging) {
                self.handleDragEnd(event, false);
            }
        });

        imageContainer.addEventListener('pointerleave', function (event) {
            if (self.dragging) {
                self.handleDragEnd(event, true);
            }
        });

        // Handle zooming with mouse wheel
        imageContainer.addEventListener('wheel', function (event) {
            if (!self.img) return;

            event.preventDefault();

            const rect = imageContainer.getBoundingClientRect();
            const mouseX = event.clientX - rect.left;
            const mouseY = event.clientY - rect.top;
            const previousScale = self.imgScale;
            const zoomFactor = event.deltaY * -0.001;

            self.imgScale += zoomFactor;
            self.imgScale = Math.max(0.1, self.imgScale);

            const scaleRatio = self.imgScale / previousScale;
            self.imgX = mouseX - (mouseX - self.imgX) * scaleRatio;
            self.imgY = mouseY - (mouseY - self.imgY) * scaleRatio;

            self.drawImage();
        });

        // Prevent context menu on image container
        imageContainer.addEventListener('contextmenu', function (event) {
            event.preventDefault();
            self.draggedJustNow = false;
        });

        // Show toolbar on pointer over
        imageContainer.addEventListener('pointerover', function () {
            toolbar.style.opacity = '1';
            if (!self.img && !self.noUpload) {
                imageContainer.style.cursor = 'pointer';
            }
        });

        // Hide toolbar on pointer out
        imageContainer.addEventListener('pointerout', function () {
            toolbar.style.opacity = '0';
            imageElement.style.cursor = '';
            drawingCanvas.style.cursor = '';
            imageContainer.style.cursor = '';
            scribbleIndicator.style.display = 'none';
        });

        // Handle resizing via resize line
        resizeLine.addEventListener('pointerdown', function (event) {
            self.resizing = true;
            event.preventDefault();
            event.stopPropagation();
        });

        document.addEventListener('pointermove', function (event) {
            if (self.resizing) {
                const containerRect = container.getBoundingClientRect();
                const newHeight = event.clientY - containerRect.top;
                container.style.height = `${newHeight}px`;
                event.preventDefault();
                event.stopPropagation();
            }
        });

        document.addEventListener('pointerup', function () {
            self.resizing = false;
        });

        document.addEventListener('pointerleave', function () {
            self.resizing = false;
        });

        // Prevent default behaviors for drag events
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventType => {
            imageContainer.addEventListener(eventType, function (event) {
                event.preventDefault();
                event.stopPropagation();
            }, false);
        });

        // Change cursor on drag events
        imageContainer.addEventListener('dragenter', () => {
            imageElement.style.cursor = 'copy';
            drawingCanvas.style.cursor = 'copy';
        });

        imageContainer.addEventListener('dragleave', () => {
            imageElement.style.cursor = '';
            drawingCanvas.style.cursor = '';
        });

        imageContainer.addEventListener('drop', function (event) {
            imageElement.style.cursor = '';
            drawingCanvas.style.cursor = '';

            const dataTransfer = event.dataTransfer;
            const files = dataTransfer.files;
            if (files.length > 0) {
                self.handleFileUpload(files[0]);
            }
        });

        // Pointer inside container tracking
        imageContainer.addEventListener('pointerenter', () => {
            self.pointerInsideContainer = true;
        });

        imageContainer.addEventListener('pointerleave', () => {
            self.pointerInsideContainer = false;
        });

        // Prevent paste events
        document.addEventListener('paste', function (event) {
            event.preventDefault();
            event.stopPropagation();
            self.handlePaste(event);
        });

        // Keyboard shortcuts for undo and redo 
        document.addEventListener('keydown', function (event) {
            if (event.ctrlKey && event.key === 'z') {
              event.preventDefault();
              self.undo();
            } else if (event.ctrlKey && event.key === 'y') {
              event.preventDefault();
              self.redo();
            }
        });

        // Maximize and minimize buttons
        maxButton.addEventListener('click', function () {
            self.maximize();
        });

        minButton.addEventListener('click', function () {
            self.minimize();
        });

        // Initialize undo/redo button states
        self.updateUndoRedoButtons();

        // Listen for background and foreground changes
        self.backgroundGradioBind.listen(function (base64Data) {
            self.uploadBase64(base64Data);
        });

        self.foregroundGradioBind.listen(function (base64Data) {
            self.uploadBase64DrawingCanvas(base64Data);
        });
    }

    /**
     * Handles drawing on the canvas.
     * @param {PointerEvent} event - The pointer event.
     */
    handleDraw(event) {
        const context = this.drawingCanvas.getContext('2d');
        const rect = this.drawingCanvas.getBoundingClientRect();
        const x = (event.clientX - rect.left) / this.imgScale;
        const y = (event.clientY - rect.top) / this.imgScale;

        this.tempDrawPoints.push([x, y]);
        context.putImageData(this.tempDrawBackground, 0, 0);
        context.beginPath();
        context.moveTo(this.tempDrawPoints[0][0], this.tempDrawPoints[0][1]);

        for (let i = 1; i < this.tempDrawPoints.length; i++) {
            context.lineTo(this.tempDrawPoints[i][0], this.tempDrawPoints[i][1]);
        }

        context.lineCap = 'round';
        context.lineJoin = 'round';
        context.lineWidth = (this.scribbleWidth / this.imgScale) * 20;

        if (this.contrastScribbles) {
            context.strokeStyle = this.contrastPattern;
            context.stroke();
            return;
        }

        context.strokeStyle = this.scribbleColor;

        if (this.scribbleAlpha <= 0) {
            context.globalCompositeOperation = 'destination-out';
            context.globalAlpha = 1;
            context.stroke();
            return;
        }

        context.globalCompositeOperation = 'source-over';

        if (this.scribbleSoftness <= 0) {
            context.globalAlpha = this.scribbleAlpha / 100;
            context.stroke();
            return;
        }

        const minLineWidth = context.lineWidth * (1 - this.scribbleSoftness / 150);
        const maxLineWidth = context.lineWidth * (1 + this.scribbleSoftness / 150);
        const steps = Math.round(5 + this.scribbleSoftness / 5);
        const lineWidthIncrement = (maxLineWidth - minLineWidth) / (steps - 1);

        context.globalAlpha = 1 - Math.pow(1 - Math.min(this.scribbleAlpha / 100, 0.95), 1 / steps);

        for (let i = 0; i < steps; i++) {
            context.lineWidth = minLineWidth + lineWidthIncrement * i;
            context.stroke();
        }
    }

    /**
     * Handles file uploads.
     * @param {File} file - The uploaded file.
     */
    handleFileUpload(file) {
        if (file && !this.noUpload) {
            const reader = new FileReader();
            reader.onload = (event) => {
                this.uploadBase64(event.target.result);
            };
            reader.readAsDataURL(file);
        }
    }

    /**
     * Handles paste events
     * @param {ClipboardEvent} event - The paste event.
     */
    handlePaste(event) {
        const items = event.clipboardData.items;
        for (let i = 0; i < items.length; i++) {
            const item = items[i];
            if (item.type.indexOf('image') !== -1) {
                const file = item.getAsFile();
                this.handleFileUpload(file);
                break;
            }
        }
    }

    /**
     * Uploads a base64 image to the background binding.
     * @param {string} base64Data - The base64 image data.
     */
    uploadBase64(base64Data) {
        if (this.gradioConfig && !this.gradioConfig.version.startsWith('4')) return;
        if (!this.gradioConfig) return;

        const img = new Image();
        img.onload = () => {
            this.img = base64Data;
            this.originalWidth = img.width;
            this.originalHeight = img.height;

            const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
            if (drawingCanvas.width !== img.width || drawingCanvas.height !== img.height) {
                drawingCanvas.width = img.width;
                drawingCanvas.height = img.height;
            }

            this.adjustInitialPositionAndScale();
            this.drawImage();
            this.onImageUpload();
            this.saveState();

            document.getElementById(`imageInput_${this.uuid}`).value = null;
            document.getElementById(`uploadHint_${this.uuid}`).style.display = 'none';
        };

        if (base64Data) {
            img.src = base64Data;
        } else {
            this.img = null;
            const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
            drawingCanvas.width = 1;
            drawingCanvas.height = 1;
            this.adjustInitialPositionAndScale();
            this.drawImage();
            this.onImageUpload();
            this.saveState();
        }
    }

    /**
     * Uploads the drawing canvas as a base64 image to the foreground binding.
     * @param {string} base64Data - The base64 image data.
     */
    uploadBase64DrawingCanvas(base64Data) {
        const img = new Image();
        img.onload = () => {
            const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
            const context = drawingCanvas.getContext('2d');
            context.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height);
            context.drawImage(img, 0, 0);
            this.saveState();
        };

        if (base64Data) {
            img.src = base64Data;
        } else {
            const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
            const context = drawingCanvas.getContext('2d');
            context.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height);
            this.saveState();
        }
    }

    /**
     * Checks if the given coordinates are inside the image.
     * @param {number} x - The x-coordinate.
     * @param {number} y - The y-coordinate.
     * @returns {boolean} - True if inside the image, else false.
     */
    isInsideImage(x, y) {
        const scaledWidth = this.originalWidth * this.imgScale;
        const scaledHeight = this.originalHeight * this.imgScale;
        return (
            x > this.imgX &&
            x < this.imgX + scaledWidth &&
            y > this.imgY &&
            y < this.imgY + scaledHeight
        );
    }

    /**
     * Draws the image and updates the canvas.
     */
    drawImage() {
        const imageElement = document.getElementById(`image_${this.uuid}`);
        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);

        if (this.img) {
            const scaledWidth = this.originalWidth * this.imgScale;
            const scaledHeight = this.originalHeight * this.imgScale;

            imageElement.src = this.img;
            imageElement.style.width = `${scaledWidth}px`;
            imageElement.style.height = `${scaledHeight}px`;
            imageElement.style.left = `${this.imgX}px`;
            imageElement.style.top = `${this.imgY}px`;
            imageElement.style.display = 'block';

            drawingCanvas.style.width = `${scaledWidth}px`;
            drawingCanvas.style.height = `${scaledHeight}px`;
            drawingCanvas.style.left = `${this.imgX}px`;
            drawingCanvas.style.top = `${this.imgY}px`;
        } else {
            imageElement.src = '';
            imageElement.style.display = 'none';
        }
    }

    /**
     * Adjusts the initial position and scale of the image to fit the container.
     */
    adjustInitialPositionAndScale() {
        const imageContainer = document.getElementById(`imageContainer_${this.uuid}`);
        const containerWidth = imageContainer.clientWidth - 20;
        const containerHeight = imageContainer.clientHeight - 20;

        const scaleX = containerWidth / this.originalWidth;
        const scaleY = containerHeight / this.originalHeight;
        this.imgScale = Math.min(scaleX, scaleY);

        const scaledWidth = this.originalWidth * this.imgScale;
        const scaledHeight = this.originalHeight * this.imgScale;

        this.imgX = (imageContainer.clientWidth - scaledWidth) / 2;
        this.imgY = (imageContainer.clientHeight - scaledHeight) / 2;
    }

    /**
     * Resets the image to its initial state.
     */
    resetImage() {
        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
        const context = drawingCanvas.getContext('2d');
        context.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height);

        this.adjustInitialPositionAndScale();
        this.drawImage();
        this.saveState();
    }

    /**
     * Removes the current image from the canvas.
     */
    removeImage() {
        this.img = null;
        const imageElement = document.getElementById(`image_${this.uuid}`);
        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
        const context = drawingCanvas.getContext('2d');

        context.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height);
        imageElement.src = '';
        imageElement.style.width = '0';
        imageElement.style.height = '0';
        this.saveState();

        if (!this.noUpload) {
            document.getElementById(`uploadHint_${this.uuid}`).style.display = 'block';
        }

        this.onImageUpload();
    }

    /**
     * Saves the current state for undo/redo functionality.
     */
    saveState() {
        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
        const context = drawingCanvas.getContext('2d');
        const imageData = context.getImageData(0, 0, drawingCanvas.width, drawingCanvas.height);

        this.history = this.history.slice(0, this.historyIndex + 1);
        this.history.push(imageData);
        this.historyIndex++;
        this.updateUndoRedoButtons();
        this.onDrawingCanvasUpload();
    }

    /**
     * Undoes the last action.
     */
    undo() {
        if (this.historyIndex > 0) {
            this.historyIndex--;
            this.restoreState();
            this.updateUndoRedoButtons();
        }
    }

    /**
     * Redoes the last undone action.
     */
    redo() {
        if (this.historyIndex < this.history.length - 1) {
            this.historyIndex++;
            this.restoreState();
            this.updateUndoRedoButtons();
        }
    }

    /**
     * Restores the canvas to a previous state.
     */
    restoreState() {
        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
        const context = drawingCanvas.getContext('2d');
        const imageData = this.history[this.historyIndex];

        context.putImageData(imageData, 0, 0);
        this.onDrawingCanvasUpload();
    }

    /**
     * Updates the state of undo and redo buttons.
     */
    updateUndoRedoButtons() {
        const undoButton = document.getElementById(`undoButton_${this.uuid}`);
        const redoButton = document.getElementById(`redoButton_${this.uuid}`);

        undoButton.disabled = this.historyIndex <= 0;
        redoButton.disabled = this.historyIndex >= this.history.length - 1;

        undoButton.style.opacity = undoButton.disabled ? '0.5' : '1';
        redoButton.style.opacity = redoButton.disabled ? '0.5' : '1';
    }

    /**
     * Called when an image is uploaded.
     */
    onImageUpload() {
        if (!this.img) {
            this.backgroundGradioBind.setValue('');
            return;
        }

        const imageElement = document.getElementById(`image_${this.uuid}`);
        const tempCanvas = this.tempCanvas;
        const context = tempCanvas.getContext('2d');

        tempCanvas.width = this.originalWidth;
        tempCanvas.height = this.originalHeight;
        context.drawImage(imageElement, 0, 0, this.originalWidth, this.originalHeight);

        const base64Data = tempCanvas.toDataURL('image/png');
        this.backgroundGradioBind.setValue(base64Data);
    }

    /**
     * Called when the drawing canvas is updated.
     */
    onDrawingCanvasUpload() {
        if (!this.img) {
            this.foregroundGradioBind.setValue('');
            return;
        }

        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);
        const base64Data = drawingCanvas.toDataURL('image/png');
        this.foregroundGradioBind.setValue(base64Data);
    }

    /**
     * Maximizes the canvas container.
     */
    maximize() {
        if (this.maximized) return;

        const container = document.getElementById(`container_${this.uuid}`);
        const toolbar = document.getElementById(`toolbar_${this.uuid}`);
        const maxButton = document.getElementById(`maxButton_${this.uuid}`);
        const minButton = document.getElementById(`minButton_${this.uuid}`);

        // Save original state
        this.originalState = {
            width: container.style.width,
            height: container.style.height,
            top: container.style.top,
            left: container.style.left,
            position: container.style.position,
            zIndex: container.style.zIndex,
        };

        // Apply maximized styles
        container.style.width = '100vw';
        container.style.height = '100vh';
        container.style.top = '0';
        container.style.left = '0';
        container.style.position = 'fixed';
        container.style.zIndex = '1000';

        maxButton.style.display = 'none';
        minButton.style.display = 'inline-block';
        this.maximized = true;
    }

    /**
     * Restores the canvas container to its original size.
     */
    minimize() {
        if (!this.maximized) return;

        const container = document.getElementById(`container_${this.uuid}`);
        const maxButton = document.getElementById(`maxButton_${this.uuid}`);
        const minButton = document.getElementById(`minButton_${this.uuid}`);

        // Restore original state
        container.style.width = this.originalState.width;
        container.style.height = this.originalState.height;
        container.style.top = this.originalState.top;
        container.style.left = this.originalState.left;
        container.style.position = this.originalState.position;
        container.style.zIndex = this.originalState.zIndex;

        maxButton.style.display = 'inline-block';
        minButton.style.display = 'none';
        this.maximized = false;
    }

    /**
     * Handles the end of a drag event.
     * @param {PointerEvent} event - The pointer event.
     * @param {boolean} isLeaving - Whether the pointer is leaving the container.
     */
    handleDragEnd(event, isLeaving) {
        this.dragging = false;
        const imageElement = document.getElementById(`image_${this.uuid}`);
        const drawingCanvas = document.getElementById(`drawingCanvas_${this.uuid}`);

        imageElement.style.cursor = 'grab';
        drawingCanvas.style.cursor = 'grab';
    }
}

// Constants
const True = true;
const False = false;

/**
 * Usage Example:
 * 
 * // Assuming you have the following HTML structure:
 * 
 * <div id="container_UUID" class="container">
 *   <div id="toolbar_UUID" class="toolbar">
 *     <button id="uploadButton_UUID">Upload</button>
 *     <button id="resetButton_UUID">Reset</button>
 *     <button id="centerButton_UUID">Center</button>
 *     <button id="removeButton_UUID">Remove</button>
 *     <button id="undoButton_UUID" disabled>Undo</button>
 *     <button id="redoButton_UUID" disabled>Redo</button>
 *     <button id="maxButton_UUID">Maximize</button>
 *     <button id="minButton_UUID" style="display:none;">Minimize</button>
 *     <!-- Add other toolbar elements as needed -->
 *   </div>
 *   <div id="imageContainer_UUID" class="image-container">
 *     <img id="image_UUID" class="image" src="" alt="Canvas Image" />
 *     <canvas id="drawingCanvas_UUID" class="drawing-canvas"></canvas>
 *     <div id="resizeLine_UUID" class="resize-line"></div>
 *     <div id="scribbleIndicator_UUID" class="scribble-indicator"></div>
 *     <input type="file" id="imageInput_UUID" accept="image/*" style="display:none;" />
 *     <div id="uploadHint_UUID" class="upload-hint">Drag & Drop an image here or click to upload.</div>
 *     <!-- Add other elements as needed -->
 *   </div>
 * </div>
 * 
 * // Initialize the ForgeCanvas
 * const canvas = new ForgeCanvas(
 *   'UUID',            // Unique identifier
 *   false,             // noUpload
 *   false,             // noScribbles
 *   false,             // contrastScribbles
 *   512,               // initialHeight
 *   '#000000',         // scribbleColor
 *   false,             // scribbleColorFixed
 *   4,                 // scribbleWidth
 *   false,             // scribbleWidthFixed
 *   100,               // scribbleAlpha
 *   false,             // scribbleAlphaFixed
 *   0,                 // scribbleSoftness
 *   false              // scribbleSoftnessFixed
 * );
 */

Additional information

No response

richrobber2 avatar Oct 04 '24 23:10 richrobber2

i just checked and the python code has an AGPL V3 license but there seems to be nothing for the js file

richrobber2 avatar Oct 04 '24 23:10 richrobber2

@AUTOMATIC1111 i think this can help but im not sure who i should talk to about this, or your progress on updating to gradio 4

mentioning you because i am wanting to make sure this is seen

richrobber2 avatar Oct 06 '24 00:10 richrobber2