Drawaria ideas and art generator (Drawing Tools Only)

Enhanced drawing tools and generative art effects for Drawaria.online (Requires separate bot script)

当前为 2025-05-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         Drawaria ideas and art generator (Drawing Tools Only)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Enhanced drawing tools and generative art effects for Drawaria.online (Requires separate bot script)
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    const EL = (sel) => document.querySelector(sel);
    const ELL = (sel) => document.querySelectorAll(sel);

    // --- Globals for Drawing ---
    let drawing_active = false;
    let previewCanvas = document.createElement('canvas');
    let originalCanvas = null; // Will be assigned later
    var data; // Stores image pixel data
    let cw = 0; // Canvas width
    let ch = 0; // Canvas height
    let executionLine = []; // Stores draw commands for image drawing

    // Note: This script relies on another script creating and managing window.___BOT

    // --- Utility Functions ---
    function delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }

    // This function is not used internally anymore after removing bot join,
    // but kept in case it's needed for compatibility or future features.
    function nullify(value = null) {
        return value == null ? null : String().concat('"', value, '"');
    }

    function getRandomColor() {
        const r = Math.floor(Math.random() * 256);
        const g = Math.floor(Math.random() * 256);
        const b = Math.floor(Math.random() * 256);
        return `rgb(${r},${g},${b})`;
    }

    // Function to send a raw draw command message via the bot's socket
    function sendDrawCmd(socket, start, end, color, thickness, isEraser = false, algo = 0) {
        if (!socket || socket.readyState !== WebSocket.OPEN) {
            console.warn("Bot socket not open, cannot send draw command.");
            return false; // Indicate failure
        }
        // Ensure coordinates are normalized (0-1)
        const p1x = Math.max(0, Math.min(1, start[0]));
        const p1y = Math.max(0, Math.min(1, start[1]));
        const p2x = Math.max(0, Math.min(1, end[0]));
        const p2y = Math.max(0, Math.min(1, end[1]));

        const gameThickness = isEraser ? thickness : 0 - thickness;

        socket.send(`42["drawcmd",0,[${p1x},${p1y},${p2x},${p2y},${isEraser},${gameThickness},"${color}",0,0,{"2":${algo},"3":0.5,"4":0.5}]]`);
        return true; // Indicate success
    }

    // --- Image Drawing Logic ---
    function loadImage(url) {
        originalCanvas = document.getElementById('canvas'); // Ensure this is fresh
        if (!originalCanvas) {
            console.error("Original canvas not found!");
            alert("Error: Game canvas not found. Cannot load image.");
            return;
        }

        var img = new Image();
        img.onload = () => {
            previewCanvas.width = originalCanvas.width;
            previewCanvas.height = originalCanvas.height;
            cw = previewCanvas.width;
            ch = previewCanvas.height;

            var ctx = previewCanvas.getContext('2d');
            let scaledWidth = img.width;
            let scaledHeight = img.height;
            const canvasAspect = cw / ch;
            const imgAspect = img.width / img.height;

            // Calculate scaling to fit image within canvas
            let scaleFactor = 1;
            if (img.width > cw || img.height > ch) {
                scaleFactor = Math.min(cw / img.width, ch / img.height);
            }
             scaledWidth = img.width * scaleFactor;
             scaledHeight = img.height * scaleFactor;


            const offsetX = (cw - scaledWidth) / 2;
            const offsetY = (ch - scaledHeight) / 2;

            ctx.clearRect(0, 0, cw, ch); // Clear preview
            // Draw image centered and scaled
            ctx.drawImage(img, offsetX, offsetY, scaledWidth, scaledHeight);

            try {
                var imgData = ctx.getImageData(0, 0, cw, ch);
                data = imgData.data;
                console.log('Image loaded and processed for drawing.');
                alert('Image loaded and ready for drawing!');
            } catch (e) {
                console.error("Error getting image data (CORS issue?):", e);
                alert("Error processing image. If loaded from a URL, it might be a CORS issue. Try downloading and selecting the file locally.");
                data = null;
            }
        };
        img.onerror = () => {
            console.error("Failed to load image from URL:", url);
            alert("Failed to load image. Check the URL or file.");
        };
        img.crossOrigin = 'anonymous'; // Attempt to handle CORS for URL loads
        img.src = url;
    }

    // Generates the drawing commands for the loaded image
    function generateImageDrawingCommands(pixelStep, brushThickness, offsetPercent, forceBW = false) {
        if (!data) {
            alert('No image data loaded. Please load an image first.');
            return []; // Return empty array if no data
        }

        executionLine = [];
        const step = Math.max(1, parseInt(pixelStep, 10));
        const thickness = parseInt(brushThickness, 10);
        // Calculate offset in canvas pixels, then normalize to 0-1
        const offsetX = parseInt(offsetPercent.x, 10) / 100 * cw;
        const offsetY = parseInt(offsetPercent.y, 10) / 100 * ch;

        for (let y = 0; y < ch; y += step) {
            let lineStart = null; // Store the normalized start coordinate [0-1, 0-1]
            let lastColor = null;

            for (let x = 0; x < cw; x += step) {
                 // Ensure we don't go out of bounds when checking pixels
                 if (y >= ch || x >= cw) continue;

                let index = (y * cw + x) * 4;
                // Check if index is within data bounds
                if (index + 3 >= data.length) continue;

                let r = data[index];
                let g = data[index + 1];
                let b = data[index + 2];
                let a = data[index + 3];

                let currentColor = `rgb(${r},${g},${b})`;
                if (forceBW) {
                    const grayscale = (r + g + b) / 3;
                    currentColor = grayscale < 128 ? 'rgb(0,0,0)' : 'rgb(255,255,255)';
                }

                const currentPointNormalized = [
                    (x + offsetX) / cw, // Normalize x with offset
                    (y + offsetY) / ch  // Normalize y with offset
                ];

                if (a > 128) { // Consider pixel opaque enough
                    if (!lineStart) { // Start of a new line segment
                        lineStart = [...currentPointNormalized]; // Copy current point
                        lastColor = currentColor;
                    } else if (currentColor !== lastColor || x + step >= cw) { // Color changed or end of row
                        // End the previous segment at the previous point
                        const endPointX = ((x - step) + offsetX) / cw;
                        const endPointY = (y + offsetY) / ch;
                        executionLine.push({
                            pos1: lineStart,
                            pos2: [endPointX, endPointY],
                            color: lastColor,
                            thickness: thickness,
                        });
                        lineStart = [...currentPointNormalized]; // Start new segment with current pixel
                        lastColor = currentColor;
                    }
                    // If it's the last pixel in the row and part of an ongoing line
                    // Add condition: check if the next pixel is transparent
                    let nextIndex = (y * cw + x + step) * 4;
                    let nextIsTransparent = (x + step >= cw) || (nextIndex + 3 >= data.length) || data[nextIndex + 3] <= 128;

                    if (x + step >= cw && lineStart && !nextIsTransparent && currentColor === lastColor) {
                         // End at current point if it's the very last opaque pixel in the row
                         executionLine.push({
                            pos1: lineStart,
                            pos2: currentPointNormalized,
                            color: lastColor,
                            thickness: thickness,
                        });
                        lineStart = null; // Reset for next row
                    }

                } else { // Transparent pixel, end current line segment
                    if (lineStart) {
                        // End the segment at the previous point before the transparent one
                         const endPointX = ((x - step) + offsetX) / cw;
                        const endPointY = (y + offsetY) / ch;
                        executionLine.push({
                            pos1: lineStart,
                            pos2: [endPointX, endPointY],
                            color: lastColor,
                            thickness: thickness,
                        });
                        lineStart = null;
                    }
                }
            }
            // After the inner loop finishes a row, check if a line segment is still open
            if (lineStart) {
                 // End the last segment at the last valid x coordinate in the row that was part of the line
                 // This needs careful calculation... simplest is just ending at the last point checked
                 const lastValidX = cw - (cw % step || step); // Find the last x coordinate we would have checked in the row
                 const endPointX = (lastValidX + offsetX) / cw;
                 const endPointY = (y + offsetY) / ch;

                 executionLine.push({
                    pos1: lineStart,
                    pos2: [endPointX, endPointY],
                    color: lastColor,
                    thickness: thickness,
                });
                lineStart = null; // Reset for next row
            }
        }
        console.log(`Image processing complete. ${executionLine.length} draw commands generated.`);
        return executionLine; // Return the generated commands
    }


    // Executes a list of drawing commands (like the one generated by drawImageFromPreview)
    async function executeDrawingCommands(commands, socket) {
         if (!socket || socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected to a room. Cannot start drawing.");
            drawing_active = false; // Ensure flag is false
            return;
        }

        drawing_active = true;
        console.log(`Starting to draw ${commands.length} lines.`);
        const drawDelay = parseInt(EL('#engine_draw_delay').value, 10) || 10;

        for (let i = 0; i < commands.length; i++) {
            if (!drawing_active) {
                console.log("Drawing stopped by user.");
                break;
            }
            let currentLine = commands[i];
            // Use sendDrawCmd which checks socket state
            if (!sendDrawCmd(socket, currentLine.pos1, currentLine.pos2, currentLine.color, currentLine.thickness)) {
                 console.warn("Socket closed during drawing. Stopping.");
                 break; // Stop if sending fails
            }
            if (drawDelay > 0) await delay(drawDelay);
        }
        drawing_active = false;
        console.log('Finished executing drawing commands.');
    }


    // --- New Generative Art Functions ---
    // These functions now get the socket from window.___BOT and call executeDrawingCommands or sendDrawCmd

    async function floodDots() {
         if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket || window.___BOT.conn.socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected. Join a room with another script first!");
            return;
        }
        drawing_active = true;
        const socket = window.___BOT.conn.socket;
        const numDots = 2000; // Number of dots
        const dotSize = 2;    // Thickness of dots
        const floodDelay = 2; // Milliseconds between dots

        console.log("Starting Flood of Dots...");
        for (let i = 0; i < numDots; i++) {
            if (!drawing_active) break;
            const x = Math.random();
            const y = Math.random();
            const color = getRandomColor();
            // A dot is a tiny line
            if (!sendDrawCmd(socket, [x, y], [x + 0.001, y + 0.001], color, dotSize)) break; // Stop if send fails
            if (floodDelay > 0) await delay(floodDelay);
        }
        drawing_active = false;
        console.log("Flood of Dots finished.");
    }

    async function explosionOfColors() {
         if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket || window.___BOT.conn.socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected. Join a room with another script first!");
            return;
        }
        drawing_active = true;
         const socket = window.___BOT.conn.socket;
        console.log("Starting Explosion of Colors...");

        const centerX = 0.5, centerY = 0.5;
        const maxRadius = 0.7; // Max radius in normalized coords
        const radiusStep = 0.01;
        const pointsOnCircle = 36; // Lower for faster, more jagged
        const thickness = 10;
        const explosionDelay = 15; // ms per circle

        for (let r = 0.01; r < maxRadius; r += radiusStep) {
            if (!drawing_active) break;
            const color = getRandomColor();
            let prevX = centerX + r * Math.cos(0);
            let prevY = centerY + r * Math.sin(0);

            for (let i = 1; i <= pointsOnCircle; i++) {
                if (!drawing_active) break;
                const angle = (i / pointsOnCircle) * 2 * Math.PI;
                const currentX = centerX + r * Math.cos(angle);
                const currentY = centerY + r * Math.sin(angle);
                 if (!sendDrawCmd(socket, [prevX, prevY], [currentX, currentY], color, thickness)) break; // Stop if send fails
                prevX = currentX;
                prevY = currentY;
                // Small delay between segments to spread out commands within one circle step
                if (explosionDelay > 0) await delay(Math.max(1, Math.floor(explosionDelay / pointsOnCircle)));
            }
             if (!drawing_active) break; // Check again after drawing circle segments
             // Delay between full circles
             if (explosionDelay > 0) await delay(explosionDelay);
             if (!sendDrawCmd(socket, [prevX, prevY], [centerX + r * Math.cos(0), centerY + r * Math.sin(0)], color, thickness)) break; // Close the circle
        }
        drawing_active = false;
        console.log("Explosion of Colors finished.");
    }

    async function colorPartyCrash() {
        if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket || window.___BOT.conn.socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected. Join a room with another script first!");
            return;
        }
        drawing_active = true;
         const socket = window.___BOT.conn.socket;
        console.log("Starting Color Party (Attempting Crash)... Be careful!");
        alert("Color Party is starting. This might be very disruptive or unstable!");

        // Send a massive amount of commands with no delay. This is very likely to cause issues.
        const commandsToSend = 20000; // Increased amount for more impact
        console.log(`Attempting to send ${commandsToSend} commands rapidly.`);
        for (let i = 0; i < commandsToSend; i++) {
            if (!drawing_active) break;
            const x1 = Math.random(), y1 = Math.random();
            const x2 = Math.random(), y2 = Math.random();
            const color = getRandomColor();
            const thickness = Math.random() * 50 + 5;
             if (!sendDrawCmd(socket, [x1, y1], [x2, y2], color, thickness)) break; // Stop if send fails
            // NO DELAY HERE! This is the "crash" part.
        }
        drawing_active = false;
        console.log("Color Party finished (or crashed).");
    }

    async function colorFestival() {
        if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket || window.___BOT.conn.socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected. Join a room with another script first!");
            return;
        }
        drawing_active = true;
         const socket = window.___BOT.conn.socket;
        console.log("Starting Color Festival...");
        const numShapes = 150;
        const festivalDelay = 50; // ms per shape

        for (let i = 0; i < numShapes; i++) {
            if (!drawing_active) break;
            const x = Math.random() * 0.8 + 0.1; // Avoid edges for full shape
            const y = Math.random() * 0.8 + 0.1;
            const size = Math.random() * 0.1 + 0.02; // Normalized size
            const color = getRandomColor();
            const thickness = Math.floor(Math.random() * 15) + 3;

            const type = Math.floor(Math.random() * 3);
            let sentOk = true;
            if (type === 0) { // Rectangle
                sentOk = sentOk && sendDrawCmd(socket, [x - size/2, y - size/2], [x + size/2, y - size/2], color, thickness);
                if (!sentOk || !drawing_active) break; await delay(5);
                sentOk = sentOk && sendDrawCmd(socket, [x + size/2, y - size/2], [x + size/2, y + size/2], color, thickness);
                 if (!sentOk || !drawing_active) break; await delay(5);
                sentOk = sentOk && sendDrawCmd(socket, [x + size/2, y + size/2], [x - size/2, y + size/2], color, thickness);
                 if (!sentOk || !drawing_active) break; await delay(5);
                sentOk = sentOk && sendDrawCmd(socket, [x - size/2, y + size/2], [x - size/2, y - size/2], color, thickness);
            } else if (type === 1) { // Triangle
                sentOk = sentOk && sendDrawCmd(socket, [x, y - size/2], [x + size/2, y + size/2], color, thickness);
                 if (!sentOk || !drawing_active) break; await delay(5);
                sentOk = sentOk && sendDrawCmd(socket, [x + size/2, y + size/2], [x - size/2, y + size/2], color, thickness);
                 if (!sentOk || !drawing_active) break; await delay(5);
                sentOk = sentOk && sendDrawCmd(socket, [x - size/2, y + size/2], [x, y - size/2], color, thickness);
            } else { // Cross
                sentOk = sentOk && sendDrawCmd(socket, [x - size/2, y], [x + size/2, y], color, thickness);
                 if (!sentOk || !drawing_active) break; await delay(5);
                sentOk = sentOk && sendDrawCmd(socket, [x, y - size/2], [x, y + size/2], color, thickness);
            }
             if (!sentOk || !drawing_active) break; // Check again before main delay
            if(festivalDelay > 0) await delay(festivalDelay);
        }
        drawing_active = false;
        console.log("Color Festival finished.");
    }

    async function lightSpeedFireworks() {
        if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket || window.___BOT.conn.socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected. Join a room with another script first!");
            return;
        }
        drawing_active = true;
         const socket = window.___BOT.conn.socket;
        console.log("Starting Light Speed Fireworks...");
        const numFireworks = 10;
        const fireworkBaseDelay = 500; // ms between fireworks

        for (let i = 0; i < numFireworks; i++) {
            if (!drawing_active) break;
            const startX = Math.random() * 0.6 + 0.2; // Launch from near bottom center
            const startY = 0.95;
            const peakX = startX + (Math.random() - 0.5) * 0.2;
            const peakY = Math.random() * 0.3 + 0.1; // Explode in upper part
            const launchColor = getRandomColor();
            const launchThickness = 5;
            const particleCount = 30 + Math.floor(Math.random()*30);
            const particleThickness = 3 + Math.floor(Math.random()*3);
            const launchSteps = 20;
            const launchStepDelay = 5; // ms between launch segments
            const explosionParticleDelay = 10; // ms per particle

            // Launch rocket
            let sentOk = true;
            for(let s=0; s<launchSteps; s++){
                if (!drawing_active) break;
                let y_progress = s / launchSteps;
                let next_y_progress = (s + 1) / launchSteps;

                let currentX = startX + (peakX - startX) * y_progress;
                let currentY = startY + (peakY - startY) * y_progress;
                let nextX = startX + (peakX - startX) * next_y_progress;
                let nextY = startY + (peakY - startY) * next_y_progress;

                sentOk = sentOk && sendDrawCmd(socket, [currentX, currentY], [nextX, nextY], launchColor, launchThickness);
                if (!sentOk) break;
                if(launchStepDelay > 0) await delay(launchStepDelay);
            }
            if (!sentOk || !drawing_active) break;

            // Explosion
            const explosionColor = getRandomColor();
            for (let j = 0; j < particleCount; j++) {
                if (!drawing_active) break;
                const angle = Math.random() * 2 * Math.PI;
                const dist = Math.random() * 0.15 + 0.05; // Explosion radius
                const endX = peakX + dist * Math.cos(angle);
                const endY = peakY + dist * Math.sin(angle);
                 sentOk = sentOk && sendDrawCmd(socket, [peakX, peakY], [endX, endY], explosionColor, particleThickness);
                 if (!sentOk) break;
                 if(explosionParticleDelay > 0) await delay(explosionParticleDelay);
            }
            if (!sentOk || !drawing_active) break; // Check before next firework delay
            if(fireworkBaseDelay > 0) await delay(fireworkBaseDelay);
        }
        drawing_active = false;
        console.log("Light Speed Fireworks finished.");
    }

     async function botClearCanvas() {
         if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket || window.___BOT.conn.socket.readyState !== WebSocket.OPEN) {
            alert("Bot is not connected. Join a room with another script first!");
            return;
        }
        const socket = window.___BOT.conn.socket;
        console.log("Attempting to 'clear' canvas with a large white line using the bot.");
        // Draw a massive white line to simulate clear.
        // This is a whiteout, not a true clear. True clear requires the 'clear' command
        // and being the actual drawer.
        sendDrawCmd(socket, [0.01, 0.5], [0.99, 0.5], "#FFFFFF", 2000, false); // Huge horizontal line
         // Or maybe a big circle/dot in the middle
         // sendDrawCmd(socket, [0.5, 0.5], [0.501, 0.501], "#FFFFFF", 2000, false); // Huge dot
     }


    // --- UI Creation ---
    function addBoxIcons() {
        if (document.querySelector('link[href*="boxicons"]')) return;
        let boxicons = document.createElement('link');
        boxicons.href = 'https://unpkg.com/[email protected]/css/boxicons.min.css';
        boxicons.rel = 'stylesheet';
        document.head.appendChild(boxicons);
    }

    function createStylesheet() {
        if (document.getElementById('drawaria-enhancer-style')) return;
        let style = document.createElement('style');
        style.id = 'drawaria-enhancer-style';
        style.innerHTML = `
            #enhancer-menu {
                position: fixed;
                top: 70px;
                left: 10px;
                width: 320px;
                background-color: #333A44; /* Darker theme */
                color: #E0E0E0; /* Light text */
                border: 1px solid #555E69;
                border-radius: 8px;
                box-shadow: 0 5px 15px rgba(0,0,0,0.3);
                z-index: 10001;
                font-family: 'Arial', sans-serif;
                font-size: 14px;
                display: none; /* Initially hidden */
            }
            #enhancer-menu-header {
                padding: 10px;
                background-color: #4A525E; /* Slightly lighter header */
                color: #FFF;
                cursor: move;
                border-top-left-radius: 7px;
                border-top-right-radius: 7px;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            #enhancer-menu-header h3 {
                margin: 0;
                font-size: 16px;
                font-weight: bold;
            }
            #enhancer-menu-close {
                background: none;
                border: none;
                color: #FFF;
                font-size: 20px;
                cursor: pointer;
            }
            #enhancer-menu-body {
                padding: 10px;
                max-height: 70vh;
                overflow-y: auto;
            }
            .enhancer-section {
                margin-bottom: 15px;
                padding-bottom: 10px;
                border-bottom: 1px solid #4A525E;
            }
            .enhancer-section:last-child {
                border-bottom: none;
            }
            .enhancer-section h4 {
                margin-top: 0;
                margin-bottom: 8px;
                color: #A0D2EB; /* Accent color for headings */
                font-size: 15px;
            }
            .enhancer-row {
                display: flex;
                margin-bottom: 8px;
                align-items: center;
            }
            .enhancer-row > label {
                flex-basis: 100px;
                margin-right: 5px;
                font-weight: normal;
                min-width: 80px; /* Prevent shrinking */
            }
            .enhancer-row > input[type="text"],
            .enhancer-row > input[type="number"],
            .enhancer-row > input[type="file"],
            .enhancer-row > select {
                flex-grow: 1;
                padding: 6px;
                background-color: #2A3038; /* Dark input fields */
                border: 1px solid #555E69;
                color: #E0E0E0;
                border-radius: 4px;
                box-sizing: border-box;
            }
             .enhancer-row > input[type="number"] { text-align: center; -moz-appearance: textfield; }
             .enhancer-row > input[type="number"]::-webkit-inner-spin-button,
             .enhancer-row > input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0;}

            .enhancer-button-grid {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
                gap: 8px;
            }
            .enhancer-button {
                padding: 8px 10px;
                background-color: #4CAF50; /* Green accent */
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                text-align: center;
                font-size: 13px;
                transition: background-color 0.2s;
            }
            .enhancer-button:hover {
                background-color: #45a049;
            }
            .enhancer-button.danger {
                background-color: #F44336; /* Red for dangerous actions */
            }
            .enhancer-button.danger:hover {
                background-color: #da190b;
            }
            .enhancer-button.special {
                background-color: #2196F3; /* Blue for special */
            }
            .enhancer-button.special:hover {
                background-color: #0b7dda;
            }
            #enhancer-toggle-button { /* Style for the main toggle button */
                margin-left: 5px; /* Add some space from the chat input */
            }
            details {
                 margin-bottom: 8px;
                 border: 1px solid #4A525E;
                 border-radius: 4px;
                 padding: 5px;
            }
             summary {
                 cursor: pointer;
                 font-weight: bold;
                 color: #E0E0E0;
                 margin-left: 5px;
             }
             details > div {
                 margin-top: 8px;
                 padding-top: 8px;
                 border-top: 1px solid #4A525E;
             }
             /* Style for the Clear Canvas button - moved out of bot section */
             .enhancer-clear-button {
                 padding: 8px 10px;
                 background-color: #ff9800; /* Orange */
                 color: white;
                 border: none;
                 border-radius: 4px;
                 cursor: pointer;
                 text-align: center;
                 font-size: 13px;
                 transition: background-color 0.2s;
                 width: 100%; /* Make it full width */
                 margin-top: 8px; /* Add space above it */
             }
             .enhancer-clear-button:hover {
                 background-color: #f57c00;
             }


        `;
        document.head.appendChild(style);
    }

    function makeDraggable(menuElement, headerElement) {
        let offsetX, offsetY, isDragging = false;

        headerElement.onmousedown = (e) => {
            // Prevent dragging if clicking on interactive elements inside header
            if (e.target.closest('button, input, select, a')) return;
            isDragging = true;
            offsetX = e.clientX - menuElement.getBoundingClientRect().left;
            offsetY = e.clientY - menuElement.getBoundingClientRect().top;
            menuElement.style.userSelect = 'none'; // Prevent text selection while dragging
            menuElement.style.cursor = 'grabbing'; // Indicate dragging
        };

        document.onmousemove = (e) => {
            if (!isDragging) return;
            // Calculate new position
            let newLeft = e.clientX - offsetX;
            let newTop = e.clientY - offsetY;

            // Optional: Keep menu within viewport bounds
            // const menuRect = menuElement.getBoundingClientRect();
            // const viewportWidth = window.innerWidth;
            // const viewportHeight = window.innerHeight;
            // newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuRect.width));
            // newTop = Math.max(0, Math.min(newTop, viewportHeight - menuRect.height));


            menuElement.style.left = `${newLeft}px`;
            menuElement.style.top = `${newTop}px`;
        };

        document.onmouseup = () => {
            if (isDragging) {
                isDragging = false;
                menuElement.style.userSelect = ''; // Restore text selection
                headerElement.style.cursor = 'move'; // Restore move cursor
            }
        };
         headerElement.style.cursor = 'move'; // Set initial cursor
    }

    function buildMenu() {
        addBoxIcons();
        createStylesheet();

        const menu = document.createElement('div');
        menu.id = 'enhancer-menu';

        const header = document.createElement('div');
        header.id = 'enhancer-menu-header';
        header.innerHTML = `<h3>Drawaria Ideas & Art Generator</h3><button id="enhancer-menu-close" title="Close Menu"><i class='bx bx-x'></i></button>`;
        menu.appendChild(header);

        const body = document.createElement('div');
        body.id = 'enhancer-menu-body';
        body.innerHTML = `
            <div class="enhancer-section">
                <h4><i class='bx bxs-image-add'></i> Image Drawing</h4>
                <div class="enhancer-row">
                    <label for="img-file-input">Load Image:</label>
                    <input type="file" id="img-file-input" accept="image/*" />
                </div>
                 <div class="enhancer-row">
                    <label for="img-url-input">Or Image URL:</label>
                    <input type="text" id="img-url-input" placeholder="Enter image URL (CORS may apply)" />
                </div>
                <details>
                    <summary>Drawing Settings</summary>
                    <div class="enhancer-row">
                        <label for="engine_imagesize" title="Detail factor. Smaller value = more detail/pixels. (Renamed from 'Image Size' for clarity)">Pixel Step:</label>
                        <input type="number" id="engine_imagesize" min="1" max="50" value="5">
                    </div>
                    <div class="enhancer-row">
                        <label for="engine_brushsize" title="Brush thickness for drawing">Brush Size:</label>
                        <input type="number" id="engine_brushsize" min="1" max="100" value="3">
                    </div>
                    <div class="enhancer-row">
                        <label for="engine_offset_x" title="Horizontal offset %">Offset X (%):</label>
                        <input type="number" id="engine_offset_x" min="-100" max="100" value="0">
                    </div>
                    <div class="enhancer-row">
                        <label for="engine_offset_y" title="Vertical offset %">Offset Y (%):</label>
                        <input type="number" id="engine_offset_y" min="-100" max="100" value="0">
                    </div>
                     <div class="enhancer-row">
                        <label for="engine_draw_delay" title="Delay (ms) between draw commands">Draw Delay:</label>
                        <input type="number" id="engine_draw_delay" min="0" max="1000" value="10">
                    </div>
                </details>
                <div class="enhancer-button-grid" style="margin-top:10px;">
                    <button id="img-draw-start" class="enhancer-button"><i class='bx bx-play'></i> Draw Image</button>
                    <button id="img-draw-stop" class="enhancer-button danger"><i class='bx bx-stop'></i> Stop Drawing</button>
                </div>
            </div>

            <div class="enhancer-section">
                <h4><i class='bx bxs-magic-wand'></i> Generative Art Effects</h4>
                <div class="enhancer-button-grid">
                    <button id="effect-flood-dots" class="enhancer-button special"><i class='bx bx-dots-horizontal-rounded'></i> Flood o' Dots</button>
                    <button id="effect-explosion" class="enhancer-button special"><i class='bx bxs-bomb'></i> Color Explosion</button>
                    <button id="effect-color-festival" class="enhancer-button special"><i class='bx bxs-party'></i> Color Festival</button>
                    <button id="effect-fireworks" class="enhancer-button special"><i class='bx bxs-hot'></i> Fireworks</button>
                    <button id="effect-retro-machine" class="enhancer-button special" title="Draws loaded image in B&W with high pixel step. Load image first."><i class='bx bx-tv'></i> Retro Machine</button>
                    <button id="effect-anime-girl" class="enhancer-button special" title="Uses image drawing. Load an anime girl image first."><i class='bx bx-face'></i> Anime Girl (Trace)</button>
                </div>
                <button id="bot-clear-canvas" class="enhancer-clear-button"><i class='bx bxs-eraser'></i> Bot Clear (Whiteout)</button>
                <button id="effect-color-party" class="enhancer-button danger" style="width:100%; margin-top: 8px;"><i class='bx bx-error-alt'></i> Color Party (CRASH)</button>
            </div>
        `;
        menu.appendChild(body);
        document.body.appendChild(menu);

        makeDraggable(menu, header);

        // --- Event Listeners for Menu Buttons ---
        EL('#enhancer-menu-close').onclick = () => menu.style.display = 'none';

        // Image Drawing
        EL('#img-file-input').onchange = function() {
            if (!this.files || !this.files[0]) return;
            const FR = new FileReader();
            FR.onload = (evt) => loadImage(evt.target.result);
            FR.readAsDataURL(this.files[0]);
            EL('#img-url-input').value = ''; // Clear URL input if file is chosen
        };
         EL('#img-url-input').onchange = function() {
            if (this.value.trim() !== "") {
                loadImage(this.value.trim());
                EL('#img-file-input').value = ''; // Clear file input
            }
        };

        EL('#img-draw-start').onclick = () => {
             // Get settings from UI
            const pixelStep = EL('#engine_imagesize').value;
            const brushSize = EL('#engine_brushsize').value;
            const offsetX = EL('#engine_offset_x').value;
            const offsetY = EL('#engine_offset_y').value;

            // Generate commands based on current image data and settings
            const commands = generateImageDrawingCommands(pixelStep, brushSize, { x: offsetX, y: offsetY });

            // Execute commands using the bot socket (if available)
            if (commands.length > 0 && window.___BOT && window.___BOT.conn && window.___BOT.conn.socket) {
                 executeDrawingCommands(commands, window.___BOT.conn.socket);
            } else if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket) {
                alert("Bot is not connected. Join a room with another script first!");
            } else {
                 alert("No drawing commands generated from image.");
            }
        };
        EL('#img-draw-stop').onclick = () => drawing_active = false;

        // Generative Art Effects & Clear Button (now independent of bot control section)
        EL('#bot-clear-canvas').onclick = botClearCanvas;
        EL('#effect-flood-dots').onclick = floodDots;
        EL('#effect-explosion').onclick = explosionOfColors;
        EL('#effect-color-festival').onclick = colorFestival;
        EL('#effect-fireworks').onclick = lightSpeedFireworks;
        EL('#effect-color-party').onclick = () => {
            if (confirm("WARNING: Color Party (CRASH) will send a massive number of commands very quickly. This can make the room unusable, crash your browser, or get you kicked. Proceed with caution?")) {
                colorPartyCrash();
            }
        };
        EL('#effect-retro-machine').onclick = () => {
             if (!data) { alert('Load an image first for Retro Machine!'); return; }
            // Use high pixel step for "blocky" retro look, and force B&W
            const pixelStep = 20;
            const brushSize = 5;
            const offsetX = 0;
            const offsetY = 0;
             const commands = generateImageDrawingCommands(pixelStep, brushSize, { x:offsetX, y:offsetY }, true); // forceBW=true
             if (commands.length > 0 && window.___BOT && window.___BOT.conn && window.___BOT.conn.socket) {
                 executeDrawingCommands(commands, window.___BOT.conn.socket);
            } else if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket) {
                alert("Bot is not connected. Join a room with another script first!");
            } else {
                 alert("No drawing commands generated for Retro Machine.");
            }
            alert("Retro Machine will draw the loaded image in Black & White with a blocky style.");
        };
        EL('#effect-anime-girl').onclick = () => {
            if (!data) { alert('Load an anime girl image first!'); return; }
            // Uses standard image drawing settings, user should adjust for line art
            // Trigger the normal image draw function after generating commands
             const pixelStep = EL('#engine_imagesize').value;
            const brushSize = EL('#engine_brushsize').value;
            const offsetX = EL('#engine_offset_x').value;
            const offsetY = EL('#engine_offset_y').value;
            const commands = generateImageDrawingCommands(pixelStep, brushSize, { x: offsetX, y: offsetY }, false); // Don't force BW
            if (commands.length > 0 && window.___BOT && window.___BOT.conn && window.___BOT.conn.socket) {
                 executeDrawingCommands(commands, window.___BOT.conn.socket);
            } else if (!window.___BOT || !window.___BOT.conn || !window.___BOT.conn.socket) {
                alert("Bot is not connected. Join a room with another script first!");
            } else {
                 alert("No drawing commands generated for Anime Girl.");
            }
            alert("Anime Girl (Trace) uses the standard image drawing. Adjust settings for best results with line art.");
        };


        // --- Create Toggle Button ---
        let chatInputTarget = EL('#chatbox_textinput');
        if (chatInputTarget && !EL('#enhancer-toggle-button')) {
            let btnContainer = document.createElement('div');
            btnContainer.className = 'input-group-append';

            let toggleBtn = document.createElement('button');
            toggleBtn.id = 'enhancer-toggle-button';
            toggleBtn.className = 'btn btn-outline-secondary'; // Standard Drawaria button style
            toggleBtn.innerHTML = `<i class='bx bx-palette'></i>`; // Using a palette icon
            toggleBtn.title = "Toggle Art Generator Menu";
            toggleBtn.onclick = (e) => {
                e.preventDefault();
                menu.style.display = (menu.style.display === 'none' || menu.style.display === '') ? 'block' : 'none';
                toggleBtn.classList.toggle('active', menu.style.display === 'block');
            };
            // Find the parent of the chat input and append the button container
            const chatInputParent = chatInputTarget.parentElement;
            if (chatInputParent) {
                 chatInputParent.appendChild(btnContainer);
                 btnContainer.appendChild(toggleBtn);
            } else {
                 // Fallback if structure changes, append after chat input directly (less ideal)
                 chatInputTarget.after(btnContainer);
                 btnContainer.appendChild(toggleBtn);
            }
        }
    }


    // --- Initialization ---
    function initializeWhenReady() {
        originalCanvas = document.getElementById('canvas');
        // Also check for the main chat input as a sign the UI is ready
        const chatInput = document.getElementById('chatbox_textinput');
        if (!originalCanvas || !chatInput) {
            // console.log("Drawaria UI not ready, retrying..."); // Can be chatty
            setTimeout(initializeWhenReady, 500);
            return;
        }

        console.log("Drawaria Enhancer (Drawing Tools Only) initializing...");
        // No bot creation here, just UI and functionality depending on external bot
        window['___ENGINE'] = {
            loadImage: loadImage,
            generateImageDrawingCommands: generateImageDrawingCommands, // Expose generate function
            executeDrawingCommands: executeDrawingCommands, // Expose execute function
            floodDots: floodDots,
            explosionOfColors: explosionOfColors,
            colorFestival: colorFestival,
            lightSpeedFireworks: lightSpeedFireworks,
            colorPartyCrash: colorPartyCrash,
            botClearCanvas: botClearCanvas // Expose clear function
            // Retro Machine and Anime Girl use combinations of the above
        };

        buildMenu();
        console.log("Drawaria Ideas & Art Generator (Drawing Tools Only) Loaded!");
        alert("Drawaria Ideas & Art Generator (Drawing Tools Only) Loaded! Please use a separate script to connect a bot (creating window.___BOT) before using drawing functions.");
    }

    // Wait for the page to be fully loaded, especially the canvas element
    if (document.readyState === "complete" || document.readyState === "interactive") {
        initializeWhenReady();
    } else {
        window.addEventListener('load', initializeWhenReady);
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址