Drawaria Animator

Animates characters on Drawaria.online canvas with optimized drawing commands, fetched from JSON, and precisely centered on the canvas. Supports multiple animations and a delay between loops.

// ==UserScript==
// @name Drawaria Animator
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Animates characters on Drawaria.online canvas with optimized drawing commands, fetched from JSON, and precisely centered on the canvas. Supports multiple animations and a delay between loops.
// @author YouTubeDrawaria
// @match https://drawaria.online/*
// @grant none
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const SPACE_INVADER_ANIMATION_JSON_URL = "https://raw.githubusercontent.com/NuevoMundoOficial/DrawariaASCIIPacks/main/spaceinvaders-videogames_drawaria_animation.json";
    const MARIO_ANIMATION_JSON_URL = "https://raw.githubusercontent.com/NuevoMundoOficial/DrawariaASCIIPacks/main/mario_drawaria_animation.json";
    const SONIC_ANIMATION_JSON_URL = "https://raw.githubusercontent.com/NuevoMundoOficial/DrawariaASCIIPacks/main/sonic_drawaria_animation.json";
    const DEFAULT_ANIMATION_DISPLAY_SIZE_PX = 30;
    const LOOPS_BEFORE_PAUSE = 1;
    const PAUSE_DURATION_MS = 10000;
    const COMMANDS_PER_CHUNK = 50;
    const CHUNK_DELAY_MS = 10;

    // --- GLOBAL SHARED WEBSOCKET HOOKING SYSTEM ---
    const _activeSockets = window._drawariaActiveSockets || [];
    if (!window._drawariaActiveSockets) {
        window._drawariaActiveSockets = _activeSockets;
        const _originalWebSocketSend = WebSocket.prototype.send;
        WebSocket.prototype.send = function (...args) {
            if (_activeSockets.indexOf(this) === -1) {
                _activeSockets.push(this);
                this.addEventListener('close', () => {
                    const index = _activeSockets.indexOf(this);
                    if (index > -1) {
                        _activeSockets.splice(index, 1);
                    }
                });
            }
            return _originalWebSocketSend.apply(this, args);
        };
    }

    function _getGameSocket() {
        return _activeSockets.find(s => s.url.includes("drawaria.online/socket.io") && s.readyState === WebSocket.OPEN);
    }

    function _delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function _clamp(value, min, max) {
        return Math.max(min, Math.min(max, value));
    }

    function _sendAndRenderDrawCmd(canvas, ctx, start_norm, end_norm, color, thickness_game_units, isEraser = false) {
        const gameSocket = _getGameSocket();
        if (!gameSocket || gameSocket.readyState !== WebSocket.OPEN) {
            console.warn("WebSocket no conectado. No se puede enviar ni renderizar el comando de dibujo.");
            return false;
        }
        const p1x_norm = _clamp(start_norm[0], 0, 1);
        const p1y_norm = _clamp(start_norm[1], 0, 1);
        const p2x_norm = _clamp(end_norm[0], 0, 1);
        const p2y_norm = _clamp(end_norm[1], 0, 1);
        let numThickness = parseFloat(thickness_game_units);
        if (isNaN(numThickness)) { numThickness = 5; }

        ctx.strokeStyle = color;
        ctx.lineWidth = numThickness * (canvas.width / 100);
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        if (isEraser) {
            ctx.globalCompositeOperation = 'destination-out';
        }
        ctx.beginPath();
        ctx.moveTo(p1x_norm * canvas.width, p1y_norm * canvas.height);
        ctx.lineTo(p2x_norm * canvas.width, p2y_norm * canvas.height);
        ctx.stroke();
        if (isEraser) {
            ctx.globalCompositeOperation = 'source-over';
        }

        const gT = isEraser ? numThickness : 0 - numThickness;
        gameSocket.send(`42["drawcmd",0,[${p1x_norm.toFixed(4)},${p1y_norm.toFixed(4)},${p2x_norm.toFixed(4)},${p2y_norm.toFixed(4)},${isEraser},${gT},"${color}",0,0,{}]]`);
        return true;
    }

    async function _clearCanvas(canvas, ctx, messageCallback) {
        const gameSocket = _getGameSocket();
        if (!gameSocket || gameSocket.readyState !== WebSocket.OPEN) {
            messageCallback("warning", "No hay conexión al juego para limpiar el lienzo.");
            return;
        }

        if (ctx && canvas) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }

        gameSocket.send(`42["drawcmd",0,[0.5,0.5,0.5,0.5,true,-2000,"#FFFFFF",0,0,{}]]`);
        messageCallback("success", "Lienzo limpiado para todos.");
        await _delay(100);
    }

    function makeDraggable(element, header) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const dragHandle = header || element;
        dragHandle.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
            pos3 = e.clientX; pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    class DrawariaAnimator {
        constructor() {
            this._currentAnimationType = 'spaceInvader';
            this._animationJsonUrl = this._getAnimationUrlByType(this._currentAnimationType);
            this._defaultAnimationDisplaySizePx = DEFAULT_ANIMATION_DISPLAY_SIZE_PX;
            this._mainCanvas = null;
            this._mainCtx = null;
            this._animationFrames = [];
            this._animationMetadata = {};
            this._currentFrameIndex = 0;
            this._animationTimeoutId = null;
            this._loopCount = 0;
            this._isActive = false;
            this._isAnimating = false;
            this._ui = {};
            this._notificationTimeout = null;
            this._init();
        }

        _getAnimationUrlByType(type) {
            switch (type) {
                case 'spaceInvader':
                    return SPACE_INVADER_ANIMATION_JSON_URL;
                case 'mario':
                    return MARIO_ANIMATION_JSON_URL;
                case 'sonic':
                    return SONIC_ANIMATION_JSON_URL;
                default:
                    return SPACE_INVADER_ANIMATION_JSON_URL;
            }
        }

        _init() {
            const checkCanvas = () => {
                this._mainCanvas = document.getElementById('canvas');
                if (this._mainCanvas) {
                    this._mainCtx = this._mainCanvas.getContext('2d');
                    this._setupUI();
                    this._loadAnimationData();
                    this._updateUIState();
                    this._notify("info", "Image Animator cargado.");
                } else {
                    setTimeout(checkCanvas, 500);
                }
            };
            checkCanvas();
        }

        _setupUI() {
            this._ui.mainContainer = document.createElement('div');
            this._ui.mainContainer.id = 'image-animator-menu';
            this._ui.mainContainer.style.cssText = `
                position: fixed;
                top: 20px;
                left: 20px;
                width: 250px;
                background: #2b2b2b;
                border: 1px solid #444;
                border-radius: 8px;
                box-shadow: 0 4px 10px rgba(0,0,0,0.5);
                font-family: Arial, sans-serif;
                color: #f0f0f0;
                z-index: 10000;
                display: flex;
                flex-direction: column;
                user-select: none;
            `;
            document.body.appendChild(this._ui.mainContainer);

            this._ui.header = document.createElement('div');
            this._ui.header.style.cssText = `
                padding: 10px;
                background: #3c3c3c;
                border-bottom: 1px solid #555;
                border-top-left-radius: 8px;
                border-top-right-radius: 8px;
                font-weight: bold;
                cursor: grab;
                text-align: center;
            `;
            this._ui.header.textContent = "Drawaria Animator";
            this._ui.mainContainer.appendChild(this._ui.header);
            makeDraggable(this._ui.mainContainer, this._ui.header);

            this._ui.content = document.createElement('div');
            this._ui.content.style.cssText = `
                padding: 10px;
                display: flex;
                flex-direction: column;
                gap: 8px;
            `;
            this._ui.mainContainer.appendChild(this._ui.content);

            this._ui.moduleToggleButton = this._createButton('toggle-module-btn-invader', '<i class="fas fa-power-off"></i> Activar Animador', 'info');
            this._ui.moduleToggleButton.addEventListener('click', () => this._toggleModuleActive());
            this._ui.content.appendChild(this._ui.moduleToggleButton);

            this._ui.imagePreview = document.createElement('img');
            this._ui.imagePreview.id = 'imagePreview';
            this._ui.imagePreview.style.cssText = "width:60px;height:60px; margin: 10px auto; display: block;";
            this._ui.content.appendChild(this._ui.imagePreview);

            this._ui.prevAnimButton = this._createButton('prev-anim-btn', '<i class="fas fa-step-backward"></i> Anterior', 'info');
            this._ui.prevAnimButton.addEventListener('click', () => this._prevAnimation());
            this._ui.content.appendChild(this._ui.prevAnimButton);

            this._ui.nextAnimButton = this._createButton('next-anim-btn', '<i class="fas fa-step-forward"></i> Siguiente', 'info');
            this._ui.nextAnimButton.addEventListener('click', () => this._nextAnimation());
            this._ui.content.appendChild(this._ui.nextAnimButton);

            this._ui.speedInput = this._createInput('number', 50, 2500, 2500);
            this._ui.content.appendChild(this._createFormGroup("Velocidad (ms/frame):", this._ui.speedInput));

            this._ui.brushSizeInput = this._createInput('number', 1, 100, 40);
            this._ui.content.appendChild(this._createFormGroup("Grosor del Pincel (1-100):", this._ui.brushSizeInput));

            const btnGroup1 = document.createElement('div');
            btnGroup1.style.cssText = "display: flex; gap: 5px;";
            this._ui.startButton = this._createButton('start-anim-btn', '<i class="fas fa-play"></i> Iniciar Animación', 'success');
            this._ui.startButton.addEventListener('click', () => this._startAnimation());
            this._ui.stopButton = this._createButton('stop-anim-btn', '<i class="fas fa-stop"></i> Detener Animación', 'danger');
            this._ui.stopButton.addEventListener('click', () => this._stopAnimation());
            btnGroup1.append(this._ui.startButton, this._ui.stopButton);
            this._ui.content.appendChild(btnGroup1);

            this._ui.clearCanvasButton = this._createButton('clear-canvas-btn', 'Limpiar Lienzo', 'warning');
            this._ui.clearCanvasButton.title = "Limpia el lienzo con una línea blanca muy grande.";
            this._ui.clearCanvasButton.addEventListener('click', () => {
                _clearCanvas(this._mainCanvas, this._mainCtx, this._notify.bind(this));
            });
            this._ui.content.appendChild(this._ui.clearCanvasButton);

            this._ui.statusDisplay = document.createElement('div');
            this._ui.statusDisplay.style.cssText = "text-align:center; margin-top:10px; font-size:0.9em; color:#aaffaa;";
            this._ui.mainContainer.appendChild(this._ui.statusDisplay);
        }

        _createButton(id, html, type) {
            const button = document.createElement('button');
            button.id = id;
            button.innerHTML = html;
            button.style.cssText = `
                flex: 1; padding: 8px; border: none; border-radius: 4px;
                color: white; font-weight: bold; cursor: pointer;
                transition: background-color 0.2s;
                background: ${type === 'success' ? '#28a745' :
                            type === 'danger' ? '#dc3545' :
                            type === 'warning' ? '#ffc107' :
                            type === 'info' ? '#17a2b8' : '#6c757d'};
            `;
            button.onmouseover = () => button.style.backgroundColor = (type === 'success' ? '#218838' :
                                                                       type === 'danger' ? '#c82333' :
                                                                       type === 'warning' ? '#e0a800' :
                                                                       type === 'info' ? '#138496' : '#5a6268');
            button.onmouseout = () => button.style.backgroundColor = (type === 'success' ? '#28a745' :
                                                                      type === 'danger' ? '#dc3545' :
                                                                      type === 'warning' ? '#e0a800' :
                                                                      type === 'info' ? '#138496' : '#5a6268');
            return button;
        }

        _createInput(type, min, max, value) {
            const input = document.createElement('input');
            input.type = type;
            input.min = min;
            input.max = max;
            input.value = value;
            input.style.cssText = `
                width: 100%; padding: 6px; border: 1px solid #555; border-radius: 4px;
                background: #3c3c3c; color: #f0f0f0; box-sizing: border-box;
            `;
            return input;
        }

        _createFormGroup(labelText, inputElement) {
            const div = document.createElement('div');
            div.style.cssText = "display: flex; flex-direction: column; gap: 4px;";
            const label = document.createElement('label');
            label.textContent = labelText;
            label.style.cssText = "font-size:0.9em; color:#bbb;";
            div.append(label, inputElement);
            return div;
        }

        _toggleModuleActive() {
            this._isActive = !this._isActive;
            this._updateUIState();
            if (this._isActive) {
                this._notify("info", "Image Animator ACTIVADO.");
            } else {
                this._notify("info", "Image Animator DESACTIVADO.");
                if (this._isAnimating) {
                    this._stopAnimation();
                }
            }
        }

        _updateUIState() {
            const isConnected = _getGameSocket() !== null;
            const hasFrames = this._animationFrames.length > 0;
            this._ui.moduleToggleButton.innerHTML = this._isActive ? '<i class="fas fa-power-off"></i> Desactivar Animador' : '<i class="fas fa-power-off"></i> Activar Animador';
            this._ui.moduleToggleButton.classList.toggle('active', this._isActive);
            this._ui.startButton.disabled = !this._isActive || this._isAnimating || !isConnected || !hasFrames;
            this._ui.stopButton.disabled = !this._isAnimating;
            this._ui.speedInput.disabled = !this._isActive || this._isAnimating;
            this._ui.brushSizeInput.disabled = !this._isActive || this._isAnimating;
            this._ui.clearCanvasButton.disabled = !this._isActive || !isConnected || this._isAnimating;
            this._ui.prevAnimButton.disabled = this._isAnimating || !this._isActive;
            this._ui.nextAnimButton.disabled = this._isAnimating || !this._isActive;
            this._ui.statusDisplay.textContent = isConnected ? (this._isActive ? (this._isAnimating ? "Estado: Animando..." : "Estado: Listo.") : "Estado: Módulo Inactivo.") : "Estado: No conectado al juego. Conéctate a una sala primero.";
        }

        _notify(type, message) {
            if (this._notificationTimeout) {
                clearTimeout(this._notificationTimeout);
            }
            const statusColor = type === 'success' ? '#aaffaa' : type === 'info' ? '#aaddff' : type === 'warning' ? '#ffccaa' : '#ffaaaa';
            this._ui.statusDisplay.style.color = statusColor;
            this._ui.statusDisplay.textContent = `Estado: ${message}`;
            this._notificationTimeout = setTimeout(() => {
                this._ui.statusDisplay.style.color = '#aaffaa';
                this._ui.statusDisplay.textContent = _getGameSocket() ? (this._isActive ? (this._isAnimating ? "Animando..." : "Listo.") : "Módulo Inactivo.") : "No conectado al juego. Conéctate a una sala primero.";
            }, 3000);
        }

        _prevAnimation() {
            if (this._isAnimating) {
                this._notify("warning", "Detén la animación antes de cambiar de fuente.");
                return;
            }

            const animations = ['spaceInvader', 'mario', 'sonic'];
            const currentIndex = animations.indexOf(this._currentAnimationType);
            const prevIndex = (currentIndex - 1 + animations.length) % animations.length;
            this._currentAnimationType = animations[prevIndex];
            this._animationJsonUrl = this._getAnimationUrlByType(this._currentAnimationType);
            this._updateImagePreview();
            this._updateUIState();
            this._loadAnimationData();
            this._notify("info", `Cargada animación: ${this._currentAnimationType}`);
        }

        _nextAnimation() {
            if (this._isAnimating) {
                this._notify("warning", "Detén la animación antes de cambiar de fuente.");
                return;
            }

            const animations = ['spaceInvader', 'mario', 'sonic'];
            const currentIndex = animations.indexOf(this._currentAnimationType);
            const nextIndex = (currentIndex + 1) % animations.length;
            this._currentAnimationType = animations[nextIndex];
            this._animationJsonUrl = this._getAnimationUrlByType(this._currentAnimationType);
            this._updateImagePreview();
            this._updateUIState();
            this._loadAnimationData();
            this._notify("info", `Cargada animación: ${this._currentAnimationType}`);
        }

        _updateImagePreview() {
            switch (this._currentAnimationType) {
                case 'spaceInvader':
                    this._ui.imagePreview.src = "https://www.space-invaders.com/static/img/icons/icon.png";
                    break;
                case 'mario':
                    this._ui.imagePreview.src = "https://static.wikia.nocookie.net/fantendo/images/0/0e/NES_Mario.jpg";
                    break;
                case 'sonic':
                    this._ui.imagePreview.src = "https://www.sonicthehedgehog.com/wp-content/uploads/2021/08/sonic-animated.gif";
                    break;
            }
        }

        async _loadAnimationData() {
            this._notify("info", `Cargando datos de animación (${this._currentAnimationType})...`);
            try {
                const response = await fetch(this._animationJsonUrl);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const data = await response.json();
                if (!data || !data.frames || !Array.isArray(data.frames) || data.frames.length === 0) {
                    throw new Error("Formato de datos de animación JSON inválido o vacío.");
                }
                this._animationFrames = data.frames;
                this._animationMetadata = data.metadata || {};
                this._notify("success", `Datos de animación cargados: ${this._animationFrames.length} frames.`);
                this._updateUIState();
            } catch (error) {
                this._notify("error", `Error al cargar datos de animación: ${error.message}.`);
                console.error("Error loading animation data:", error);
                this._animationFrames = [];
                this._animationMetadata = {};
                this._updateUIState();
            }
        }

        _startAnimation() {
            if (this._isAnimating) return;
            if (this._animationFrames.length === 0) {
                this._notify("warning", "No hay frames cargados para animar.");
                return;
            }
            if (!_getGameSocket()) {
                this._notify("error", "No conectado al juego. Conéctate a una sala primero.");
                return;
            }
            this._isAnimating = true;
            this._currentFrameIndex = 0;
            this._loopCount = 0;
            this._updateUIState();
            this._notify("info", `Iniciando animación ${this._currentAnimationType}...`);
            _clearCanvas(this._mainCanvas, this._mainCtx, this._notify.bind(this)).then(() => {
                this._animateLoop();
            });
        }

        _stopAnimation() {
            this._isAnimating = false;
            if (this._animationTimeoutId) {
                clearTimeout(this._animationTimeoutId);
                this._animationTimeoutId = null;
            }
            this._updateUIState();
            this._notify("info", "Animación detenida.");
        }

        async _animateLoop() {
            if (!this._isAnimating) return;

            const gameSocket = _getGameSocket();
            if (!gameSocket || gameSocket.readyState !== WebSocket.OPEN) {
                console.warn("WebSocket no conectado. Esperando reconexión para la animación...");
                this._animationTimeoutId = setTimeout(() => this._animateLoop(), 500);
                return;
            }

            const frameDelay = parseInt(this._ui.speedInput.value);
            const brushThickness = parseInt(this._ui.brushSizeInput.value);
            const currentFrameCommands = this._animationFrames[this._currentFrameIndex];

            const animWidthGameUnits = this._animationMetadata.width || this._defaultAnimationDisplaySizePx;
            const animHeightGameUnits = this._animationMetadata.height || this._defaultAnimationDisplaySizePx;

            const animWidthFraction = animWidthGameUnits / 100;
            const animHeightFraction = animHeightGameUnits / 100;

            const offsetXForCentering = (1 - animWidthFraction) / 2;
            const offsetYForCentering = (1 - animHeightFraction) / 2;

            const centerXOfAnimArea = offsetXForCentering + (animWidthFraction / 2);
            const centerYOfAnimArea = offsetYForCentering + (animHeightFraction / 2);

            _sendAndRenderDrawCmd(
                this._mainCanvas, this._mainCtx,
                [centerXOfAnimArea, centerYOfAnimArea],
                [centerXOfAnimArea, centerYOfAnimArea],
                '#FFFFFF',
                Math.max(animWidthGameUnits, animHeightGameUnits) * 1.2,
                true
            );

            await _delay(30);

            for (let i = 0; i < currentFrameCommands.length; i++) {
                if (!this._isAnimating) break;
                const cmd = currentFrameCommands[i];

                const p1x_final = (cmd.start_norm[0] * animWidthFraction) + offsetXForCentering;
                const p1y_final = (cmd.start_norm[1] * animHeightFraction) + offsetYForCentering;
                const p2x_final = (cmd.end_norm[0] * animWidthFraction) + offsetXForCentering;
                const p2y_final = (cmd.end_norm[1] * animHeightFraction) + offsetYForCentering;

                _sendAndRenderDrawCmd(
                    this._mainCanvas, this._mainCtx,
                    [p1x_final, p1y_final],
                    [p2x_final, p2y_final],
                    cmd.color,
                    brushThickness
                );

                if ((i + 1) % COMMANDS_PER_CHUNK === 0) {
                    await _delay(CHUNK_DELAY_MS);
                    if (!this._isAnimating) break;
                }
            }

            this._currentFrameIndex = (this._currentFrameIndex + 1) % this._animationFrames.length;

            if (this._currentFrameIndex === 0) {
                this._loopCount++;
                if (this._loopCount >= LOOPS_BEFORE_PAUSE) {
                    this._notify("info", `Pausa de ${PAUSE_DURATION_MS / 1000} segundos para aliviar el servidor.`);
                    this._stopAnimation();
                    setTimeout(() => {
                        this._startAnimation();
                    }, PAUSE_DURATION_MS);
                    return;
                }
            }

            this._animationTimeoutId = setTimeout(() => this._animateLoop(), frameDelay);
        }
    }

    window.addEventListener('load', () => {
        new DrawariaAnimator();
    });
})();