// ==UserScript==
// @name Drawaria Tunnel Visualizer
// @namespace http://tampermonkey.net/
// @version 2.1
// @description Recreate the visual effect of an expanding tunnel of squares, using ultra-optimization of sockets.
// @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';
/* ---------- COMPONENTES COMPARTIDOS DEL SISTEMA (ULTRA OPTIMIZADOS) ---------- */
let drawariaSocket = null;
let drawariaCanvas = null;
let drawariaCtx = null;
// OPTIMIZACIÓN ULTRA: Reducido a 15 FPS para máxima fluidez y menor carga.
const FRAME_RATE = 10;
// Cola de comandos optimizada con agrupamiento inteligente (ULTRA BATCHING)
const commandQueue = [];
let batchProcessor = null;
// OPTIMIZACIÓN ULTRA: Tamaño del lote aumentado a 100 para procesar más comandos de una vez.
const BATCH_SIZE = 100;
// OPTIMIZACIÓN ULTRA: Intervalo reducido a 5ms para envío más rápido.
const BATCH_INTERVAL = 5;
// Intercepta WebSocket para capturar el socket del juego (ORIGINAL)
const originalWebSocketSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(...args) {
if (!drawariaSocket && this.url && this.url.includes('drawaria')) {
drawariaSocket = this;
console.log('🔗 Drawaria WebSocket capturado para el Túnel Visualizer.');
startBatchProcessor();
}
return originalWebSocketSend.apply(this, args);
};
function startBatchProcessor() {
if (batchProcessor) return;
batchProcessor = setInterval(() => {
if (!drawariaSocket || drawariaSocket.readyState !== WebSocket.OPEN || commandQueue.length === 0) {
return;
}
const batch = commandQueue.splice(0, BATCH_SIZE);
batch.forEach(cmd => {
try {
drawariaSocket.send(cmd);
} catch (e) {
console.warn('⚠️ Fallo al enviar el comando:', e);
}
});
}, BATCH_INTERVAL);
}
/**
* Función unificada para encolar comandos de dibujo.
* CRÍTICO: Usa grosor positivo para línea, grosor negativo para relleno.
* Si thickness es negativo (relleno), usa la implementación de Drawaria:
* - Envía el valor absoluto del grosor como *negativo* en el comando.
* OPTIMIZACIÓN ULTRA: Reducida precisión a toFixed(3) para comandos más ligeros.
* @param {number} x1 - Coordenada X inicial
* @param {number} y1 - Coordenada Y inicial
* @param {number} x2 - Coordenada X final
* @param {number} y2 - Coordenada Y final
* @param {string} color - Color del objeto (ej. '#FFFFFF')
* @param {number} thickness - Grosor (Positivo para línea, Negativo para relleno)
*/
function enqueueDrawCommand(x1, y1, x2, y2, color, thickness) {
if (!drawariaCanvas || !drawariaSocket) return;
const normX1 = (x1 / drawariaCanvas.width).toFixed(3);
const normY1 = (y1 / drawariaCanvas.height).toFixed(3);
const normX2 = (x2 / drawariaCanvas.width).toFixed(3);
const normY2 = (y2 / drawariaCanvas.height).toFixed(3);
// CORRECCIÓN CRÍTICA: Aplica el truco de grosor negativo solo si se pide relleno (thickness < 0)
const cmdThickness = thickness < 0 ? -Math.abs(thickness) : thickness;
const cmd = `42["drawcmd",0,[${normX1},${normY1},${normX2},${normY2},false,${cmdThickness},"${color}",0,0,{}]]`;
commandQueue.push(cmd);
// Renderizado local para retroalimentación visual inmediata
if (drawariaCtx) {
if (thickness > 0) {
// Para líneas (positivo)
drawariaCtx.strokeStyle = color;
drawariaCtx.lineWidth = thickness;
drawariaCtx.lineCap = 'butt';
drawariaCtx.lineJoin = 'miter';
drawariaCtx.beginPath();
drawariaCtx.moveTo(x1, y1);
drawariaCtx.lineTo(x2, y2);
drawariaCtx.stroke();
} else {
// Para rellenos (negativo), simula un relleno local si es todo el canvas
if (x1 === 0 && y1 === 0 && x2 === drawariaCanvas.width && y2 === drawariaCanvas.height) {
drawariaCtx.fillStyle = color;
drawariaCtx.fillRect(0, 0, drawariaCanvas.width, drawariaCanvas.height);
}
}
}
}
// Función auxiliar para forzar un relleno (usando el método de línea gruesa negativa)
// CORRECCIÓN: Usa diagonal completa (0,0 a width,height) como en el script de Snake para asegurar que funcione.
function enqueueDrawFillCommand(width, height, color) {
if (!drawariaCanvas || !drawariaSocket) return;
// CORRECCIÓN CRÍTICA: Llama a enqueueDrawCommand con grosor negativo y diagonal completa.
// Se usa un grosor negativo muy grande para asegurar el relleno completo.
const fillThickness = -(Math.max(width, height) * 2);
enqueueDrawCommand(0, 0, width, height, color, fillThickness);
}
/* ---------- LÓGICA DEL TUNNEL VISUALIZER (USANDO LA ESTRUCTURA) ---------- */
class TunnelVisualizer {
constructor() {
this.isActive = false;
this.animationInterval = null;
this.frame = 0;
// OPTIMIZACIÓN ULTRA: Reducido número de cuadrados a 10 para menor carga por frame.
this.squareCount = 1;
this.maxSquareSize = 0;
this.minSquareSize = 1;
this.center = { x: 0, y: 0 };
this.init();
}
init() {
const checkGameReady = () => {
const gameCanvas = document.getElementById('canvas');
if (gameCanvas) {
drawariaCanvas = gameCanvas;
drawariaCtx = gameCanvas.getContext('2d');
this.center = {
x: drawariaCanvas.width / 2,
y: drawariaCanvas.height / 2
};
// Multiplicador ligeramente mayor para que el cuadrado más grande esté fuera de vista
this.maxSquareSize = Math.max(drawariaCanvas.width, drawariaCanvas.height) * 1.8;
this.createGamePanel();
console.log('✅ Tunnel Visualizer inicializado.');
} else {
setTimeout(checkGameReady, 100);
}
};
checkGameReady();
}
createGamePanel() {
const existingPanel = document.getElementById('tunnel-panel');
if (existingPanel) existingPanel.remove();
const panel = document.createElement('div');
panel.id = 'tunnel-panel';
panel.style.cssText = `
position: fixed !important;
top: 250px !important;
right: 20px !important;
width: 250px !important;
z-index: 2147483647 !important;
background: linear-gradient(135deg, #2a2a3a, #1a1a2e) !important;
border: 2px solid #5d5dff !important;
border-radius: 12px !important;
color: white !important;
font-family: 'Segoe UI', Arial, sans-serif !important;
box-shadow: 0 0 20px rgba(93, 93, 255, 0.3) !important;
padding: 15px !important;
text-align: center !important;
`;
panel.innerHTML = `
<h3 style="margin-top: 0; color: #5d5dff; cursor: grab;">🌌 Drawaria Tunnel Effect</h3>
<div id="status-display" style="margin-bottom: 10px; color: #ffc107;">
Status: Paused
</div>
<button id="toggle-animation" style="
width: 100%;
padding: 10px;
background: #5d5dff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
">▶️ Start Animation</button>
<button id="draw-black-bg" style="
width: 100%;
padding: 10px;
background: #333;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin-top: 10px;
">⚫ Draw Black Background (Click First!)</button>
`;
document.body.appendChild(panel);
this.setupEventListeners();
this.makePanelDraggable(panel);
}
setupEventListeners() {
document.getElementById('toggle-animation').addEventListener('click', () => this.toggleAnimation());
document.getElementById('draw-black-bg').addEventListener('click', () => this.drawBlackBackground());
}
drawBlackBackground() {
if (!drawariaCanvas || !drawariaSocket) return;
// Llama a la función de relleno corregida
enqueueDrawFillCommand(drawariaCanvas.width, drawariaCanvas.height, '#000000');
document.getElementById('status-display').textContent = 'Status: Background Black';
}
toggleAnimation() {
const btn = document.getElementById('toggle-animation');
const statusEl = document.getElementById('status-display');
if (!this.isActive) {
this.startAnimation();
btn.textContent = '⏸️ Pause Animation';
btn.style.background = '#ffc107';
statusEl.textContent = 'Status: Running';
} else {
this.pauseAnimation();
btn.textContent = '▶️ Resume Animation';
btn.style.background = '#5d5dff';
statusEl.textContent = 'Status: Paused';
}
}
startAnimation() {
if (this.isActive) return;
this.isActive = true;
this.animationLoop();
}
pauseAnimation() {
this.isActive = false;
if (this.animationInterval) {
clearTimeout(this.animationInterval);
this.animationInterval = null;
}
}
animationLoop() {
if (!this.isActive) return;
this.drawTunnelFrame();
this.frame++;
this.animationInterval = setTimeout(() => this.animationLoop(), Math.floor(1000 / FRAME_RATE));
}
drawTunnelFrame() {
// CORRECCIÓN CRÍTICA: Limpia el fondo cada 10 fotogramas para ultra-reducir la carga del servidor (optimización).
// Esto es necesario para que no se vean los rastros de los cuadrados anteriores.
if (this.frame % 10 === 0) {
enqueueDrawFillCommand(drawariaCanvas.width, drawariaCanvas.height, '#000000');
}
const baseThickness = 2;
const sizeIncrement = this.maxSquareSize / this.squareCount;
// OPTIMIZACIÓN ULTRA: Velocidad de animación ajustada para mayor suavidad y menor cómputo.
const speed = 1.5;
for (let i = 0; i < this.squareCount; i++) {
const sizeOffset = (this.frame * speed) % (sizeIncrement * this.squareCount);
let size = (i * sizeIncrement) + sizeOffset;
// Cálculo modular para un bucle continuo de tamaño.
const normalizedSize = size % this.maxSquareSize;
const color = '#FFFFFF';
// Dibuja el contorno del cuadrado usando el método original con grosor POSITIVO
this.drawSquareOutline(normalizedSize, color, baseThickness);
}
// Cuadrado central azul
this.drawSquareOutline(this.minSquareSize + 5, '#87CEEB', baseThickness * 1.5);
}
drawSquareOutline(size, color, thickness) {
const cX = this.center.x;
const cY = this.center.y;
const halfSize = size / 2;
const x1 = cX - halfSize;
const y1 = cY - halfSize;
const x2 = cX + halfSize;
const y2 = cY + halfSize;
// Usando la función original, ahora corregida para líneas
enqueueDrawCommand(x1, y1, x2, y1, color, thickness); // Superior
enqueueDrawCommand(x2, y1, x2, y2, color, thickness); // Derecha
enqueueDrawCommand(x2, y2, x1, y2, color, thickness); // Inferior
enqueueDrawCommand(x1, y2, x1, y1, color, thickness); // Izquierda
}
makePanelDraggable(panel) {
let isDragging = false;
let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
const header = panel.querySelector('h3');
if (!header) return;
const dragStart = (e) => {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
header.style.cursor = 'grabbing';
};
const dragEnd = () => {
initialX = currentX;
initialY = currentY;
isDragging = false;
header.style.cursor = 'grab';
};
const drag = (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
panel.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
};
header.addEventListener("mousedown", dragStart);
document.addEventListener("mouseup", dragEnd);
document.addEventListener("mousemove", drag);
}
}
// Inicialización del visualizador (MISMO MÉTODO QUE SNAKE)
const initVisualizer = () => {
const visualizer = new TunnelVisualizer();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initVisualizer);
} else {
setTimeout(initVisualizer, 500);
}
})();