Greasy Fork is available in English.

Drawaria fetched from JSON Animation

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 fetched from JSON Animation
// @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();
    });
})();