Drawaria Avatar Size+Hider+Multiplier Customizer

Extend or diminish the size of selfavatarimage and avatar builder canvas, affecting uploaded canvas resolution, and add dynamic avatar tiling.

// ==UserScript==
// @name         Drawaria Avatar Size+Hider+Multiplier Customizer
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Extend or diminish the size of selfavatarimage and avatar builder canvas, affecting uploaded canvas resolution, and add dynamic avatar tiling.
// @author       YouTubeDrawaria
// @match        https://drawaria.online/avatar/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Store original sizes from the very first detection on page load (for "Original Size" button)
    let initialSelfAvatarImageWidth, initialSelfAvatarImageHeight;
    let initialAvatarBuilderCanvasWidth, initialAvatarBuilderCanvasHeight;

    // Global/scoped variables for the Avatar Builder Canvas tiling state
    let tiledBaseAvatarImage = null; // Stores the cropped Image object of the single avatar to be tiled
    let isTiledMode = false;         // Flag to indicate if tiling is currently active

    // Helper to get element's current effective dimensions
    function getElementCurrentDimensions(element, isCanvasAttributeBased) {
        if (isCanvasAttributeBased) {
            // For canvases, read the actual width/height attributes for intrinsic size
            return {
                width: element.width,
                height: element.height
            };
        } else {
            // For images/other elements, read computed CSS style dimensions
            const computedStyle = getComputedStyle(element);
            return {
                width: parseFloat(computedStyle.width),
                height: parseFloat(computedStyle.height)
            };
        }
    }

    // Function to apply dimensions to an element (CSS for images, Attributes + CSS for canvas)
    function applyElementDimensions(element, width, height, isCanvasAttributeBased) {
        if (isCanvasAttributeBased) {
            // For canvases, update the actual width/height attributes (clears content)
            element.width = width;
            element.height = height;
            // Also update CSS style for visual consistency and scaling
            element.style.width = `${width}px`;
            element.style.height = `${height}px`;
        } else {
            // For images and other elements, only update CSS style
            element.style.width = `${width}px`;
            element.style.height = `${height}px`;
        }
    }

    // Function to create and append the draggable menu
    function createDraggableMenu() {
        const menu = document.createElement('div');
        menu.id = 'tampermonkey-menu';
        menu.style.cssText = `
            position: fixed;
            top: 50px;
            right: 50px;
            width: 280px;
            background: #282c34; /* Dark background */
            border: 1px solid #61dafb; /* Light blue border */
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
            z-index: 99999;
            font-family: Arial, sans-serif;
            color: #fff;
            padding: 15px;
            resize: both; /* Allow resizing the menu itself */
            overflow: auto; /* Show scrollbars if content overflows */
            min-width: 200px;
            min-height: 100px;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            cursor: grab;
            font-weight: bold;
            padding-bottom: 10px;
            border-bottom: 1px solid #61dafb;
            margin-bottom: 15px;
            color: #61dafb;
            user-select: none;
        `;
        header.textContent = 'Drawaria Size Customizer';
        menu.appendChild(header);

        // Make the menu draggable
        let isDragging = false;
        let offsetX, offsetY;

        header.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - menu.getBoundingClientRect().left;
            offsetY = e.clientY - menu.getBoundingClientRect().top;
            menu.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            let newX = e.clientX - offsetX;
            let newY = e.clientY - offsetY;

            newX = Math.max(0, Math.min(newX, window.innerWidth - menu.offsetWidth));
            newY = Math.max(0, Math.min(newY, window.innerHeight - menu.offsetHeight));

            menu.style.left = newX + 'px';
            menu.style.top = newY + 'px';
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            menu.style.cursor = 'grab';
        });

        document.body.appendChild(menu);
        return menu;
    }

    // Function to create a slider control
    const createSlider = (label, value, min, max, step, onChange) => {
        const container = document.createElement('div');
        container.style.marginBottom = '10px';

        const labelElem = document.createElement('label');
        labelElem.textContent = label + ': ';
        labelElem.style.display = 'block';
        labelElem.style.marginBottom = '5px';
        labelElem.style.color = '#ccc';
        container.appendChild(labelElem);

        const slider = document.createElement('input');
        slider.type = 'range';
        slider.min = min;
        slider.max = max;
        slider.step = step;
        slider.value = value;
        slider.style.width = 'calc(100% - 60px)';
        slider.style.verticalAlign = 'middle';
        slider.style.background = '#444';
        slider.style.appearance = 'none';
        slider.style.height = '8px';
        slider.style.borderRadius = '5px';
        slider.style.outline = 'none';
        slider.style.webkitAppearance = 'none';
        slider.style.cursor = 'pointer';

        if (!document.getElementById('tampermonkey-slider-style')) {
            const styleSheet = document.createElement('style');
            styleSheet.id = 'tampermonkey-slider-style';
            styleSheet.textContent = `
                input[type=range]::-webkit-slider-thumb {
                    -webkit-appearance: none;
                    appearance: none;
                    width: 18px;
                    height: 18px;
                    border-radius: 50%;
                    background: #61dafb;
                    cursor: pointer;
                    box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
                }
                input[type=range]::-moz-range-thumb {
                    width: 18px;
                    height: 18px;
                    border-radius: 50%;
                    background: #61dafb;
                    cursor: pointer;
                    box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
                }
            `;
            document.head.appendChild(styleSheet);
        }

        container.appendChild(slider);

        const valueSpan = document.createElement('span');
        valueSpan.textContent = ` ${value}px`;
        valueSpan.style.marginLeft = '5px';
        valueSpan.style.verticalAlign = 'middle';
        valueSpan.style.color = '#fff';
        container.appendChild(valueSpan);

        slider.oninput = (e) => {
            valueSpan.textContent = ` ${e.target.value}px`;
            onChange(parseInt(e.target.value));
        };

        slider.update = (newValue) => {
            slider.value = newValue;
            valueSpan.textContent = ` ${newValue}px`;
        };
        return container;
    };

    // Helper function to find the bounding box of non-transparent pixels
    function getBoundingBox(imageData) {
        const data = imageData.data;
        const width = imageData.width;
        const height = imageData.height;

        let minX = width;
        let minY = height;
        let maxX = -1;
        let maxY = -1;

        let foundPixel = false;

        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const i = (y * width + x) * 4;
                const alpha = data[i + 3];
                if (alpha > 0) { // If not fully transparent
                    minX = Math.min(minX, x);
                    minY = Math.min(minY, y);
                    maxX = Math.max(maxX, x);
                    maxY = Math.max(maxY, y);
                    foundPixel = true;
                }
            }
        }

        if (!foundPixel) {
            return null; // Canvas is empty or fully transparent
        }

        return {
            x: minX,
            y: minY,
            width: maxX - minX + 1,
            height: maxY - minY + 1
        };
    }

    // Function to render the tiled avatar
    function renderTiledAvatar(canvas, ctx, avatarImage) {
        if (!avatarImage) {
            console.warn("No avatar image to tile.");
            return;
        }

        ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear the main canvas before tiling

        const tileSourceWidth = avatarImage.width;
        const tileSourceHeight = avatarImage.height;

        if (tileSourceWidth === 0 || tileSourceHeight === 0) {
            console.warn("Captured avatar has zero dimensions. Cannot tile.");
            return;
        }

        // Calculate the number of columns and rows needed to perfectly fill the canvas
        // and the size of each tile to stretch to fit
        const numCols = Math.ceil(canvas.width / tileSourceWidth);
        const numRows = Math.ceil(canvas.height / tileSourceHeight);

        // Adjust the drawing dimensions of each tile to perfectly fill the current canvas size
        const drawWidth = canvas.width / numCols;
        const drawHeight = canvas.height / numRows;

        for (let r = 0; r < numRows; r++) {
            for (let c = 0; c < numCols; c++) {
                ctx.drawImage(avatarImage, c * drawWidth, r * drawHeight, drawWidth, drawHeight);
            }
        }
    }


    // Function to add a section for an element's controls
    function addControlSection(menu, title, targetSelector, isCanvasAttributeBased = false, localStorageKey) {
        const section = document.createElement('div');
        section.style.marginBottom = '20px';
        section.style.borderTop = '1px dashed #444';
        section.style.paddingTop = '15px';

        const sectionTitle = document.createElement('h3');
        sectionTitle.style.cssText = `
            font-size: 1.1em;
            margin-bottom: 10px;
            color: #61dafb;
        `;
        sectionTitle.textContent = title;
        section.appendChild(sectionTitle);

        const targetElement = document.querySelector(targetSelector);
        if (!targetElement) {
            section.textContent = `Element "${targetSelector}" not found.`;
            section.style.color = 'red';
            menu.appendChild(section);
            return;
        }

        // Capture initial dimensions on page load for "Original Size" button
        const initialDimensions = getElementCurrentDimensions(targetElement, isCanvasAttributeBased);
        if (targetSelector === '#selfavatarimage') {
            initialSelfAvatarImageWidth = initialDimensions.width;
            initialSelfAvatarImageHeight = initialDimensions.height;
        } else if (targetSelector === 'canvas.main') {
            initialAvatarBuilderCanvasWidth = initialDimensions.width;
            initialAvatarBuilderCanvasHeight = initialDimensions.height;
        }

        let currentWidth = initialDimensions.width;
        let currentHeight = initialDimensions.height;
        let originalAspectRatio = (initialDimensions.width / initialDimensions.height);
        if (isNaN(originalAspectRatio) || !isFinite(originalAspectRatio) || originalAspectRatio === 0) {
            originalAspectRatio = 1; // Fallback to 1:1 if original dimensions are invalid or zero
        }

        // --- Sliders ---
        const widthSliderContainer = createSlider(
            'Width',
            currentWidth,
            0,
            1000,
            1,
            (val) => {
                const newWidth = val;
                const newHeight = Math.round(newWidth / originalAspectRatio);
                applyElementDimensions(targetElement, newWidth, newHeight, isCanvasAttributeBased);
                widthSliderContainer.querySelector('input').update(newWidth);
                heightSliderContainer.querySelector('input').update(newHeight);

                // Re-tile if in tiled mode
                if (isTiledMode && targetSelector === 'canvas.main') {
                    renderTiledAvatar(targetElement, targetElement.getContext('2d'), tiledBaseAvatarImage);
                }
            }
        );
        section.appendChild(widthSliderContainer);

        const heightSliderContainer = createSlider(
            'Height',
            currentHeight,
            0,
            1000,
            1,
            (val) => {
                const newHeight = val;
                const newWidth = Math.round(newHeight * originalAspectRatio);
                applyElementDimensions(targetElement, newWidth, newHeight, isCanvasAttributeBased);
                heightSliderContainer.querySelector('input').update(newHeight);
                widthSliderContainer.querySelector('input').update(newWidth);

                // Re-tile if in tiled mode
                if (isTiledMode && targetSelector === 'canvas.main') {
                    renderTiledAvatar(targetElement, targetElement.getContext('2d'), tiledBaseAvatarImage);
                }
            }
        );
        section.appendChild(heightSliderContainer);

        // --- Custom Size Buttons ---
        const buttonsContainer = document.createElement('div');
        buttonsContainer.style.marginTop = '10px';
        buttonsContainer.style.display = 'flex';
        buttonsContainer.style.flexWrap = 'wrap';
        buttonsContainer.style.gap = '8px';
        section.appendChild(buttonsContainer);

        const createButton = (text, onClick) => {
            const button = document.createElement('button');
            button.textContent = text;
            button.style.cssText = `
                background: #61dafb;
                color: #282c34;
                border: none;
                padding: 8px 12px;
                border-radius: 4px;
                cursor: pointer;
                font-size: 0.9em;
                flex: 1;
                min-width: 60px;
                white-space: nowrap;
                transition: background 0.2s ease;
            `;
            button.onmouseover = () => button.style.background = '#21a1f1';
            button.onmouseout = () => button.style.background = '#61dafb';
            button.onclick = onClick;
            buttonsContainer.appendChild(button);
        };

        createButton('Hide (0px)', () => {
            // Exit tiled mode
            isTiledMode = false;
            tiledBaseAvatarImage = null;

            applyElementDimensions(targetElement, 0, 0, isCanvasAttributeBased);
            widthSliderContainer.querySelector('input').update(0);
            heightSliderContainer.querySelector('input').update(0);
        });

        createButton('Set W=200px', () => {
            // Exit tiled mode
            isTiledMode = false;
            tiledBaseAvatarImage = null;

            const newWidth = 200;
            const newHeight = Math.round(newWidth / originalAspectRatio);
            applyElementDimensions(targetElement, newWidth, newHeight, isCanvasAttributeBased);
            widthSliderContainer.querySelector('input').update(newWidth);
            heightSliderContainer.querySelector('input').update(newHeight);
        });

        createButton('Original Size', () => {
            // Exit tiled mode
            isTiledMode = false;
            tiledBaseAvatarImage = null;

            const initialW = (targetSelector === '#selfavatarimage') ? initialSelfAvatarImageWidth : initialAvatarBuilderCanvasWidth;
            const initialH = (targetSelector === '#selfavatarimage') ? initialSelfAvatarImageHeight : initialAvatarBuilderCanvasHeight;

            if (isNaN(initialW) || isNaN(initialH) || initialW === 0 || initialH === 0) {
                console.warn("Original size not captured or is zero for", targetSelector, ". Cannot restore accurately.");
                alert("Original size not captured or is zero. Cannot restore accurately. Please refresh the page.");
                return;
            }
            applyElementDimensions(targetElement, initialW, initialH, isCanvasAttributeBased);
            widthSliderContainer.querySelector('input').update(initialW);
            heightSliderContainer.querySelector('input').update(initialH);
        });

        // --- Tile Avatar Button (only for Avatar Builder Canvas) ---
        if (isCanvasAttributeBased && targetSelector === 'canvas.main') {
            const tileButton = document.createElement('button');
            tileButton.textContent = 'Tile Avatar';
            tileButton.style.cssText = `
                background: #ffc107; /* Amber/Yellow */
                color: #282c34;
                border: none;
                padding: 8px 12px;
                border-radius: 4px;
                cursor: pointer;
                font-size: 0.9em;
                margin-top: 10px;
                width: 100%;
                transition: background 0.2s ease;
            `;
            tileButton.onmouseover = () => tileButton.style.background = '#e0a800';
            tileButton.onmouseout = () => tileButton.style.background = '#ffc107';
            tileButton.onclick = () => {
                const canvas = targetElement;
                const ctx = canvas.getContext('2d');

                // 1. Capture the actual drawn avatar by finding its bounding box
                const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                const boundingBox = getBoundingBox(currentImageData);

                if (!boundingBox) {
                    alert("Canvas is empty or only transparent pixels. Draw your avatar first!");
                    return;
                }

                // Create a temporary canvas to hold the cropped avatar
                const croppedCanvas = document.createElement('canvas');
                croppedCanvas.width = boundingBox.width;
                croppedCanvas.height = boundingBox.height;
                const croppedCtx = croppedCanvas.getContext('2d');
                // Put only the content within the bounding box onto the cropped canvas
                croppedCtx.putImageData(ctx.getImageData(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height), 0, 0);

                const avatarImage = new Image();
                avatarImage.src = croppedCanvas.toDataURL(); // This is our clean single tile source

                avatarImage.onload = () => {
                    tiledBaseAvatarImage = avatarImage; // Store the image for later re-tiling
                    isTiledMode = true; // Activate tiled mode

                    renderTiledAvatar(canvas, ctx, tiledBaseAvatarImage);
                    alert("Avatar tiled across the canvas!");
                };
                 avatarImage.onerror = () => {
                    alert("Failed to load avatar image for tiling. Is the canvas content corrupted or empty?");
                };
            };
            section.appendChild(tileButton);
        }


        // --- Default Load Values Section ---
        const defaultLoadSection = document.createElement('div');
        defaultLoadSection.style.marginTop = '20px';
        defaultLoadSection.style.borderTop = '1px solid #444';
        defaultLoadSection.style.paddingTop = '15px';
        defaultLoadSection.style.color = '#ccc';

        const defaultTitle = document.createElement('h4');
        defaultTitle.style.cssText = `
            font-size: 1em;
            margin-bottom: 10px;
            color: #ddd;
        `;
        defaultTitle.textContent = 'Default Load Size:';
        defaultLoadSection.appendChild(defaultTitle);

        const defaultWidthInput = document.createElement('input');
        defaultWidthInput.type = 'number';
        defaultWidthInput.placeholder = 'Enter default width (px)';
        defaultWidthInput.style.cssText = `
            width: calc(100% - 10px);
            padding: 8px;
            margin-bottom: 10px;
            background: #333;
            border: 1px solid #555;
            border-radius: 4px;
            color: #fff;
        `;
        defaultLoadSection.appendChild(defaultWidthInput);

        const defaultButtonsContainer = document.createElement('div');
        defaultButtonsContainer.style.display = 'flex';
        defaultButtonsContainer.style.gap = '8px';
        defaultLoadSection.appendChild(defaultButtonsContainer);

        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save as Default';
        saveButton.style.cssText = `
            background: #28a745; /* Green */
            color: #fff;
            border: none;
            padding: 8px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.9em;
            flex: 1;
            transition: background 0.2s ease;
        `;
        saveButton.onmouseover = () => saveButton.style.background = '#218838';
        saveButton.onmouseout = () => saveButton.style.background = '#28a745';
        saveButton.onclick = () => {
            const val = parseInt(defaultWidthInput.value);
            if (!isNaN(val) && val >= 0) {
                const defaultHeight = Math.round(val / originalAspectRatio);
                localStorage.setItem(localStorageKey, JSON.stringify({ width: val, height: defaultHeight }));
                alert(`Default size saved for ${title}: ${val}x${defaultHeight}px`);
            } else {
                alert('Please enter a valid positive number for default width.');
            }
        };
        defaultButtonsContainer.appendChild(saveButton);

        const resetButton = document.createElement('button');
        resetButton.textContent = 'Reset Default';
        resetButton.style.cssText = `
            background: #dc3545; /* Red */
            color: #fff;
            border: none;
            padding: 8px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.9em;
            flex: 1;
            transition: background 0.2s ease;
        `;
        resetButton.onmouseover = () => resetButton.style.background = '#c82333';
        resetButton.onmouseout = () => resetButton.style.background = '#dc3545';
        resetButton.onclick = () => {
            localStorage.removeItem(localStorageKey);
            alert(`Default size reset for ${title}. Refresh page to apply original application size.`);

            // Exit tiled mode when default is reset for canvas
            if (targetSelector === 'canvas.main') {
                isTiledMode = false;
                tiledBaseAvatarImage = null;
            }

            const initialW = (targetSelector === '#selfavatarimage') ? initialSelfAvatarImageWidth : initialAvatarBuilderCanvasWidth;
            const initialH = (targetSelector === '#selfavatarimage') ? initialSelfAvatarImageHeight : initialAvatarBuilderCanvasHeight;
            applyElementDimensions(targetElement, initialW, initialH, isCanvasAttributeBased);
            widthSliderContainer.querySelector('input').update(initialW);
            heightSliderContainer.querySelector('input').update(initialH);
            defaultWidthInput.value = '';
        };
        defaultButtonsContainer.appendChild(resetButton);

        section.appendChild(defaultLoadSection);
        menu.appendChild(section);

        const savedDefaults = localStorage.getItem(localStorageKey);
        if (savedDefaults) {
            try {
                const { width, height } = JSON.parse(savedDefaults);
                if (!isNaN(width) && !isNaN(height) && width >= 0 && height >= 0) {
                    applyElementDimensions(targetElement, width, height, isCanvasAttributeBased);
                    widthSliderContainer.querySelector('input').update(width);
                    heightSliderContainer.querySelector('input').update(height);
                    defaultWidthInput.value = width;
                    console.log(`Applied saved default for ${title}: ${width}x${height}px`);
                }
            } catch (e) {
                console.error("Error parsing saved defaults for", title, e);
                localStorage.removeItem(localStorageKey);
            }
        }
    }

    // Initialize the script
    const menu = createDraggableMenu();

    const currentUrl = window.location.href;

    if (currentUrl.includes('drawaria.online') && !currentUrl.includes('drawaria.online/avatar/builder/')) {
        const checkSelfAvatarImage = setInterval(() => {
            const selfAvatarImage = document.getElementById('selfavatarimage');
            if (selfAvatarImage) {
                clearInterval(checkSelfAvatarImage);
                addControlSection(menu, 'Self Avatar Image', '#selfavatarimage', false, 'drawariaCustomizer.selfAvatarImageDefaults');
            }
        }, 500);
    }

    if (currentUrl.includes('drawaria.online/avatar/builder/')) {
        const checkAvatarBuilderCanvas = setInterval(() => {
            const avatarBuilderCanvas = document.querySelector('canvas.main');
            if (avatarBuilderCanvas) {
                clearInterval(checkAvatarBuilderCanvas);
                addControlSection(menu, 'Avatar Builder Canvas', 'canvas.main', true, 'drawariaCustomizer.avatarBuilderCanvasDefaults');
            }
        }, 500);
    }

})();