Drawaria Layer System for Canvas

Adds a client-side layer system to Drawaria.online

// ==UserScript==
// @name         Drawaria Layer System for Canvas
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a client-side layer system to Drawaria.online
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @license MIT
// @grant        none

// ==/UserScript==

(function() {
    'use strict';

    // Global variables
    let layers = [];
    let activeLayerId = null;
    let mainCanvas = null;
    let mainCtx = null;
    let layerCounter = 1;
    let isDrawing = false;
    let lastX, lastY;

    // Temporary canvas to store background layers for efficient drawing of the active layer
    let tempBackgroundCanvas = document.createElement('canvas');
    let tempBackgroundCtx = tempBackgroundCanvas.getContext('2d');

    // --- Layer Class ---
    function Layer(id, name, order) {
        this.id = id;
        this.name = name;
        this.canvas = document.createElement('canvas');
        if (mainCanvas) { // Ensure mainCanvas is available
            this.canvas.width = mainCanvas.width;
            this.canvas.height = mainCanvas.height;
        }
        this.ctx = this.canvas.getContext('2d');
        this.isVisible = true;
        this.order = order; // Used for Z-ordering layers (front to back)
    }

    // --- UI Functions ---
    function createLayerPanelUI() {
        const panelHTML = `
            <div id="layerPanel" style="position: fixed; top: 100px; right: 10px; width: 220px; background: #f8f9fa; border: 1px solid #ced4da; padding: 10px; z-index: 10000; font-family: Arial, sans-serif; font-size: 13px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.15);">
              <h4 style="margin-top: 0; margin-bottom:10px; text-align: center; color: #343a40; border-bottom: 1px solid #dee2e6; padding-bottom: 5px;">Layers</h4>
              <button id="newLayerBtn" style="width: 100%; margin-bottom: 10px; padding: 6px 10px; background-color: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size:12px;">New Layer</button>
              <ul id="layerList" style="list-style: none; padding: 0; margin-top: 10px; max-height: 300px; overflow-y: auto;"></ul>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', panelHTML);
        document.getElementById('newLayerBtn').addEventListener('click', addNewLayer);
    }

    function renderLayerList() {
        const layerListUI = document.getElementById('layerList');
        if (!layerListUI) return;
        layerListUI.innerHTML = '';

        // Sort layers to display top layer first in the UI list
        layers.slice().sort((a, b) => b.order - a.order).forEach(layer => {
            const listItem = document.createElement('li');
            listItem.style.padding = "6px 8px";
            listItem.style.marginBottom = "5px";
            listItem.style.background = layer.id === activeLayerId ? "#cce5ff" : "#fff";
            listItem.style.border = "1px solid #dee2e6";
            listItem.style.borderRadius = "3px";
            listItem.style.display = "flex";
            listItem.style.justifyContent = "space-between";
            listItem.style.alignItems = "center";
            listItem.setAttribute('data-layer-id', layer.id);

            const layerNameSpan = document.createElement('span');
            layerNameSpan.textContent = layer.name;
            layerNameSpan.style.cursor = "pointer";
            layerNameSpan.style.flexGrow = "1";
            layerNameSpan.style.overflow = "hidden";
            layerNameSpan.style.textOverflow = "ellipsis";
            layerNameSpan.style.whiteSpace = "nowrap";
            layerNameSpan.title = `Activate ${layer.name}`;
            layerNameSpan.addEventListener('click', () => setActiveLayer(layer.id));

            const visibilityButton = document.createElement('button');
            visibilityButton.innerHTML = layer.isVisible ? '👁️' : '🙈';
            visibilityButton.title = layer.isVisible ? "Hide layer" : "Show layer";
            visibilityButton.style.cssText = "background:none; border:none; cursor:pointer; margin-right:5px; font-size:16px;";
            visibilityButton.addEventListener('click', (e) => { e.stopPropagation(); toggleLayerVisibility(layer.id); });

            const deleteButton = document.createElement('button');
            deleteButton.innerHTML = '🗑️';
            deleteButton.title = "Delete layer";
            deleteButton.style.cssText = "background:none; border:none; cursor:pointer; font-size:16px;";
            deleteButton.addEventListener('click', (e) => { e.stopPropagation(); deleteLayer(layer.id); });
            // Disable delete if only one layer left
            if (layers.length <= 1) {
                deleteButton.disabled = true;
                deleteButton.style.opacity = "0.5";
            }

            const controlsDiv = document.createElement('div');
            controlsDiv.style.flexShrink = "0";
            controlsDiv.appendChild(visibilityButton);
            controlsDiv.appendChild(deleteButton);

            listItem.appendChild(layerNameSpan);
            listItem.appendChild(controlsDiv);
            layerListUI.appendChild(listItem);
        });
    }

    // --- Layer Management Functions ---
    function addInitialLayer() {
        // Add one default layer when the script starts
        const initialLayer = new Layer(`layer_${layerCounter}`, `Layer ${layerCounter}`, 0);
        layerCounter++;
        layers.push(initialLayer);
        activeLayerId = initialLayer.id;
        renderLayerList();
    }

    function addNewLayer() {
        if (layers.length >= 10) {
            alert("Maximum of 10 layers reached.");
            return;
        }
        // Assign a new order to the new layer, placing it on top
        const newOrder = layers.length > 0 ? Math.max(...layers.map(l => l.order)) + 1 : 0;
        const newLayer = new Layer(`layer_${layerCounter}`, `Layer ${layerCounter}`, newOrder);
        layerCounter++;
        layers.push(newLayer);
        setActiveLayer(newLayer.id);
    }

    function setActiveLayer(layerId) {
        activeLayerId = layerId;
        console.log("Active layer set to:", layerId);
        renderLayerList(); // Re-render to highlight active layer
        flattenAndDraw(); // Redraw canvas to show active layer on top visually (though it's only active for drawing)
    }

    function toggleLayerVisibility(layerId) {
        const layer = layers.find(l => l.id === layerId);
        if (layer) {
            layer.isVisible = !layer.isVisible;
            renderLayerList();
            flattenAndDraw(); // Redraw canvas to reflect visibility change
        }
    }

    function deleteLayer(layerId) {
        if (layers.length <= 1) {
            alert("Cannot delete the only layer.");
            return;
        }
        layers = layers.filter(l => l.id !== layerId);
        // If the active layer was deleted, set a new active layer (e.g., the top-most remaining one)
        if (activeLayerId === layerId) {
            activeLayerId = layers.length > 0 ? layers.sort((a,b) => b.order - a.order)[0].id : null;
        }
        renderLayerList();
        flattenAndDraw(); // Redraw canvas after deletion
    }

    function getActiveLayer() {
        return layers.find(l => l.id === activeLayerId);
    }

    // --- Drawing and Flattening Logic ---
    // Prepares a temporary canvas with all non-active, visible layers merged
    function prepareTempBackground() {
        if (!mainCanvas) return;
        tempBackgroundCanvas.width = mainCanvas.width;
        tempBackgroundCanvas.height = mainCanvas.height;

        tempBackgroundCtx.fillStyle = 'white';
        tempBackgroundCtx.fillRect(0, 0, tempBackgroundCanvas.width, tempBackgroundCanvas.height);

        // Draw all layers EXCEPT the active one onto the temporary background canvas
        layers.slice().sort((a,b)=> a.order - b.order).forEach(layer => {
            if (layer.isVisible && layer.id !== activeLayerId) {
                tempBackgroundCtx.drawImage(layer.canvas, 0, 0);
            }
        });
    }

    // Merges all visible layers onto the main Drawaria canvas
    function flattenAndDraw() {
        if (!mainCanvas || !mainCtx) return;

        // Clear the main Drawaria canvas
        mainCtx.fillStyle = 'white';
        mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);

        // Draw all layers onto the main canvas, sorted by their order
        layers.slice().sort((a,b)=> a.order - b.order).forEach(layer => {
            if (layer.isVisible) {
                mainCtx.drawImage(layer.canvas, 0, 0);
            }
        });
    }

    // --- Intercepting Drawing ---
    // Copies current Drawaria tool settings to the active layer's context
    function copyDrawariaStylesToLayer(layerCtx) {
        if (!mainCtx || !layerCtx) return;
        layerCtx.strokeStyle = mainCtx.strokeStyle;
        layerCtx.lineWidth = mainCtx.lineWidth;
        layerCtx.lineCap = mainCtx.lineCap;
        layerCtx.lineJoin = mainCtx.lineJoin;
        layerCtx.globalCompositeOperation = mainCtx.globalCompositeOperation; // Important for eraser
    }

    function handlePointerDown(e) {
        const activeLayer = getActiveLayer();
        if (!activeLayer || !activeLayer.isVisible) return; // Only draw on visible active layer

        // Check if the click is on the layer panel or other UI elements we control
        if (e.target.closest && e.target.closest("#layerPanel")) return;

        isDrawing = true;
        copyDrawariaStylesToLayer(activeLayer.ctx);

        const rect = mainCanvas.getBoundingClientRect();
        lastX = e.clientX - rect.left;
        lastY = e.clientY - rect.top;

        prepareTempBackground(); // Update background for live drawing preview

        activeLayer.ctx.beginPath();
        // For a single dot on click (especially important for small brush sizes)
        if (activeLayer.ctx.globalCompositeOperation === 'source-over') {
            activeLayer.ctx.fillStyle = activeLayer.ctx.strokeStyle; // Use strokeStyle for fill color of the dot
            activeLayer.ctx.arc(lastX, lastY, activeLayer.ctx.lineWidth / 2, 0, Math.PI * 2);
            activeLayer.ctx.fill();
        } else if (activeLayer.ctx.globalCompositeOperation === 'destination-out') { // Eraser dot
            activeLayer.ctx.arc(lastX, lastY, activeLayer.ctx.lineWidth / 2, 0, Math.PI * 2);
            activeLayer.ctx.fill(); // For eraser, fill clears
        }
        activeLayer.ctx.beginPath(); // Start the actual line path
        activeLayer.ctx.moveTo(lastX, lastY);

        e.stopPropagation(); // Crucial to prevent Drawaria's default drawing
        e.preventDefault();  // May prevent some default browser actions
    }

    function handlePointerMove(e) {
        if (!isDrawing) return;
        const activeLayer = getActiveLayer();
        if (!activeLayer || !activeLayer.isVisible) return;

        const rect = mainCanvas.getBoundingClientRect();
        const currentX = e.clientX - rect.left;
        const currentY = e.clientY - rect.top;

        activeLayer.ctx.lineTo(currentX, currentY);
        activeLayer.ctx.stroke();

        lastX = currentX;
        lastY = currentY;

        // Live preview: Clear main canvas, draw merged background, then draw active layer content
        mainCtx.fillStyle = 'white';
        mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
        mainCtx.drawImage(tempBackgroundCanvas, 0, 0); // Draw combined background
        mainCtx.drawImage(activeLayer.canvas, 0, 0);    // Draw active layer on top

        e.stopPropagation();
    }

    function handlePointerUpOrOut(e) {
        if (!isDrawing) return;
        const activeLayer = getActiveLayer();
        if (activeLayer && activeLayer.isVisible) {
             activeLayer.ctx.closePath(); // Close the current drawing path
        }
        isDrawing = false;
        flattenAndDraw(); // Final merge of all layers onto the main canvas after drawing is done
        e.stopPropagation();
    }

    // Attaches custom drawing listeners to the main Drawaria canvas
    function setupDrawingListeners() {
        // Use capture phase to intercept events before Drawaria's handlers
        mainCanvas.addEventListener('pointerdown', handlePointerDown, true);
        mainCanvas.addEventListener('pointermove', handlePointerMove, true);
        mainCanvas.addEventListener('pointerup', handlePointerUpOrOut, true);
        mainCanvas.addEventListener('pointerout', handlePointerUpOrOut, true); // Handles mouse leaving canvas
    }

    // --- Initialization ---
    function initializeLayerScript() {
        mainCanvas = document.getElementById('canvas');
        if (!mainCanvas) {
            console.error("Drawaria Layer System: Main canvas not found!");
            return;
        }
        mainCtx = mainCanvas.getContext('2d');

        // Resize layer canvases and temp background canvas when main canvas resizes
        const resizeObserver = new ResizeObserver(entries => {
            for (let entry of entries) {
                if (entry.target.id === 'canvas') {
                    const newWidth = mainCanvas.width;
                    const newHeight = mainCanvas.height;

                    layers.forEach(layer => {
                        // Create a temporary copy to preserve content during resize
                        const tempCopy = document.createElement('canvas');
                        tempCopy.width = layer.canvas.width;
                        tempCopy.height = layer.canvas.height;
                        tempCopy.getContext('2d').drawImage(layer.canvas, 0, 0);

                        layer.canvas.width = newWidth;
                        layer.canvas.height = newHeight;
                        // Redraw scaled content from temp copy to new canvas size
                        layer.ctx.drawImage(tempCopy, 0, 0, tempCopy.width, tempCopy.height, 0, 0, newWidth, newHeight);
                    });
                    tempBackgroundCanvas.width = newWidth;
                    tempBackgroundCanvas.height = newHeight;
                    flattenAndDraw(); // Redraw everything after resize
                }
            }
        });
        resizeObserver.observe(mainCanvas);


        createLayerPanelUI();
        addInitialLayer(); // Add one default layer
        setupDrawingListeners();
        flattenAndDraw(); // Initial draw to ensure canvas is clear and layers are rendered
    }

    // Wait for the Drawaria canvas to be available before initializing the script
    const CMAX_WAIT_FOR_CANVAS = 20000; // Max wait time in ms
    const CINTERVAL_WAIT_FOR_CANVAS = 200; // Check interval in ms
    let waitTimeElapsed = 0;
    const g_interv = setInterval(function() {
        if (document.getElementById('canvas') && document.getElementById('canvas').getContext('2d')) {
            clearInterval(g_interv);
            initializeLayerScript();
        } else if (waitTimeElapsed >= CMAX_WAIT_FOR_CANVAS) {
            clearInterval(g_interv);
            console.error("Drawaria Layer System: Canvas not ready in time, script initialization aborted.");
        }
        waitTimeElapsed += CINTERVAL_WAIT_FOR_CANVAS;
    }, CINTERVAL_WAIT_FOR_CANVAS);

})();