wplace.live Guia Visual Avançado

Ferramenta completa para sobrepor imagens no wplace.live com controles avançados

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         wplace.live Guia Visual Avançado
// @namespace    ViolentMonkey
// @version      3.0
// @description  Ferramenta completa para sobrepor imagens no wplace.live com controles avançados
// @author       Você
// @match        https://wplace.live/*
// @license      MIT  //
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(function() {
    'use strict';

    // =============================================
    // CONFIGURAÇÕES E CONSTANTES
    // =============================================
    const DIMENSOES = {
        PAINEL: {
            NORMAL: { width: 320, height: 'auto' },
            MINIMIZADO: { width: 150, height: 40 }
        },
        MARGENS: {
            PADRAO: 20,
            MINIMA: 10
        },
        OPACIDADE: {
            MIN: 0.1,
            MAX: 1,
            STEP: 0.1
        },
        TAMANHO: {
            MIN: 20,
            MAX: 200,
            STEP: 10
        }
    };

    const config = {
        urlImagem: '',
        opacidade: 0.6,
        deslocamento: { x: 0, y: 0 },
        tamanho: 100,
        janelaMinimizada: false,
        posicaoJanela: { x: null, y: null }
    };

    // =============================================
    // ELEMENTOS DA INTERFACE
    // =============================================
    const elementos = {
        guia: null,
        controles: null,
        urlInput: null,
        carregarBtn: null,
        status: null,
        opacidade: null,
        tamanhoValor: null,
        diminuirBtn: null,
        aumentarBtn: null,
        tamanhoSlider: null,
        minimizarBtn: null,
        dragHandle: null
    };

    // =============================================
    // ESTILOS CSS
    // =============================================
    GM_addStyle(`
        #wplace-guia {
            position: absolute;
            pointer-events: none;
            z-index: 9998;
            opacity: ${config.opacidade};
            image-rendering: pixelated;
            border: 2px dashed rgba(255,255,255,0.3);
            display: ${config.urlImagem ? 'block' : 'none'};
            transform-origin: top left;
            will-change: transform;
        }

        #wplace-controles {
            position: fixed;
            z-index: 9999;
            background: rgba(0,0,0,0.9);
            color: white;
            padding: 15px;
            padding-top: 35px;
            border-radius: 8px;
            font-family: Arial, sans-serif;
            box-shadow: 0 0 15px rgba(0,0,0,0.7);
            user-select: none;
            will-change: transform;
            transition: all 0.2s ease;
            width: ${DIMENSOES.PAINEL.NORMAL.width}px;
        }

        #wplace-controles.minimizado {
            height: ${DIMENSOES.PAINEL.MINIMIZADO.height}px;
            width: ${DIMENSOES.PAINEL.MINIMIZADO.width}px;
            padding-top: 15px;
            overflow: hidden;
        }

        #wplace-controles.minimizado .conteudo-controles {
            display: none;
        }

        #wplace-drag-handle {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 30px;
            cursor: move;
            touch-action: none;
        }

        #wplace-minimizar-btn {
            position: absolute;
            top: 35px;
            right: 5px;
            background: transparent;
            border: none;
            color: white;
            font-size: 16px;
            cursor: pointer;
            padding: 5px;
            z-index: 10000;
        }

        #wplace-controles.minimizado #wplace-minimizar-btn {
            top: 5px;
            position: static;
            margin: 0 auto;
            display: block;
        }

        .grupo-controle {
            margin-bottom: 12px;
        }

        #wplace-url-input {
            width: 100%;
            padding: 8px;
            margin-bottom: 10px;
            background: rgba(255,255,255,0.1);
            border: 1px solid #444;
            color: white;
            border-radius: 4px;
        }

        #wplace-carregar-btn {
            width: 100%;
            padding: 8px;
            background: #4CAF50;
            border: none;
            color: white;
            border-radius: 4px;
            cursor: pointer;
            margin-bottom: 10px;
            transition: background 0.2s;
        }

        #wplace-carregar-btn:hover {
            background: #45a049;
        }

        #wplace-carregar-btn.carregando {
            background: #367c39;
        }

        #wplace-status {
            margin-top: 10px;
            padding: 8px;
            background: rgba(255,255,255,0.1);
            border-radius: 4px;
            font-size: 13px;
        }

        .controle-tamanho {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .botao-tamanho {
            width: 30px;
            height: 30px;
            background: #555;
            border: none;
            color: white;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s;
        }

        .botao-tamanho:hover {
            background: #666;
        }

        #wplace-tamanho-valor {
            min-width: 40px;
            text-align: center;
        }

        #wplace-controles.dragging {
            cursor: grabbing;
            transition: none;
        }

        .loader {
            display: inline-block;
            width: 12px;
            height: 12px;
            border: 2px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s ease-in-out infinite;
            margin-right: 5px;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }
    `);

    // =============================================
    // FUNÇÕES PRINCIPAIS
    // =============================================

    /**
     * Cria todos os elementos da interface
     */
    function criarInterface() {
        elementos.controles = $(`
            <div id="wplace-controles" class="${config.janelaMinimizada ? 'minimizado' : ''}">
                <div id="wplace-drag-handle"></div>
                <button id="wplace-minimizar-btn">${config.janelaMinimizada ? '⬛' : '➖'}</button>
                <div class="conteudo-controles">
                    <div class="grupo-controle">
                        <label>URL da Imagem:</label>
                        <input type="text" id="wplace-url-input" placeholder="https://exemplo.com/imagem.png" value="${config.urlImagem}">
                        <button id="wplace-carregar-btn">Carregar Imagem</button>
                        <div id="wplace-status">${config.urlImagem ? 'Imagem pronta' : 'Nenhuma imagem carregada'}</div>
                    </div>

                    <div class="grupo-controle">
                        <label>Opacidade: ${config.opacidade.toFixed(1)}</label>
                        <input type="range" id="wplace-opacidade" min="${DIMENSOES.OPACIDADE.MIN}" max="${DIMENSOES.OPACIDADE.MAX}" step="${DIMENSOES.OPACIDADE.STEP}" value="${config.opacidade}">
                    </div>

                    <div class="grupo-controle">
                        <label>Tamanho da Imagem:</label>
                        <div class="controle-tamanho">
                            <button class="botao-tamanho" id="wplace-diminuir">-</button>
                            <span id="wplace-tamanho-valor">${config.tamanho}%</span>
                            <button class="botao-tamanho" id="wplace-aumentar">+</button>
                            <input type="range" id="wplace-tamanho-slider" min="${DIMENSOES.TAMANHO.MIN}" max="${DIMENSOES.TAMANHO.MAX}" value="${config.tamanho}" style="flex-grow:1;">
                        </div>
                    </div>

                    <div class="grupo-controle">
                        <label>Posição Atual:</label>
                        <div>X: ${config.deslocamento.x}px, Y: ${config.deslocamento.y}px</div>
                    </div>
                </div>
            </div>
        `);

        elementos.guia = $(`<img id="wplace-guia" src="${config.urlImagem}" alt="Guia visual">`);

        $('body').append(elementos.guia, elementos.controles);

        // Cache de elementos
        elementos.urlInput = $('#wplace-url-input');
        elementos.carregarBtn = $('#wplace-carregar-btn');
        elementos.status = $('#wplace-status');
        elementos.opacidade = $('#wplace-opacidade');
        elementos.tamanhoValor = $('#wplace-tamanho-valor');
        elementos.diminuirBtn = $('#wplace-diminuir');
        elementos.aumentarBtn = $('#wplace-aumentar');
        elementos.tamanhoSlider = $('#wplace-tamanho-slider');
        elementos.minimizarBtn = $('#wplace-minimizar-btn');
        elementos.dragHandle = $('#wplace-drag-handle');
    }

    /**
     * Ajusta a posição da janela para ficar visível
     * @param {HTMLElement} element - Elemento do painel de controles
     */
    function ajustarPosicaoJanela(element) {
        if (config.janelaMinimizada) {
            // Posiciona no canto inferior direito quando minimizado
            element.style.left = 'auto';
            element.style.right = DIMENSOES.MARGENS.PADRAO + 'px';
            element.style.top = 'auto';
            element.style.bottom = DIMENSOES.MARGENS.PADRAO + 'px';
            return;
        }

        const width = DIMENSOES.PAINEL.NORMAL.width;
        const height = element.offsetHeight;

        // Verifica se a posição está fora da tela
        if (config.posicaoJanela.x === null ||
            config.posicaoJanela.x > window.innerWidth - DIMENSOES.MARGENS.MINIMA ||
            config.posicaoJanela.x < -width + DIMENSOES.MARGENS.MINIMA) {
            config.posicaoJanela.x = window.innerWidth - width - DIMENSOES.MARGENS.PADRAO;
        }

        if (config.posicaoJanela.y === null ||
            config.posicaoJanela.y > window.innerHeight - DIMENSOES.MARGENS.MINIMA ||
            config.posicaoJanela.y < -height + DIMENSOES.MARGENS.MINIMA) {
            config.posicaoJanela.y = window.innerHeight - height - DIMENSOES.MARGENS.PADRAO;
        }

        // Aplica a posição ajustada
        element.style.left = `${Math.max(DIMENSOES.MARGENS.MINIMA, Math.min(config.posicaoJanela.x, window.innerWidth - width - DIMENSOES.MARGENS.MINIMA))}px`;
        element.style.top = `${Math.max(DIMENSOES.MARGENS.MINIMA, Math.min(config.posicaoJanela.y, window.innerHeight - height - DIMENSOES.MARGENS.MINIMA))}px`;
        element.style.right = 'auto';
        element.style.bottom = 'auto';
    }

    /**
     * Torna a janela arrastável
     * @param {HTMLElement} element - Elemento do painel de controles
     */
    function makeDraggable(element) {
        let isDragging = false;
        let startX, startY, startLeft, startTop;

        const dragMouseDown = (e) => {
            if (e.target !== elementos.dragHandle[0] || config.janelaMinimizada) return;

            e.preventDefault();
            isDragging = true;

            startX = e.clientX;
            startY = e.clientY;
            startLeft = element.offsetLeft;
            startTop = element.offsetTop;

            element.classList.add('dragging');
            document.addEventListener('mousemove', elementDrag);
            document.addEventListener('mouseup', closeDragElement);
        };

        const elementDrag = (e) => {
            if (!isDragging) return;
            e.preventDefault();

            const newLeft = startLeft + (e.clientX - startX);
            const newTop = startTop + (e.clientY - startY);

            const maxTop = window.innerHeight - (config.janelaMinimizada ? DIMENSOES.PAINEL.MINIMIZADO.height : element.offsetHeight);
            const maxLeft = window.innerWidth - (config.janelaMinimizada ? DIMENSOES.PAINEL.MINIMIZADO.width : element.offsetWidth);

            element.style.left = `${Math.min(Math.max(newLeft, DIMENSOES.MARGENS.MINIMA), maxLeft - DIMENSOES.MARGENS.MINIMA)}px`;
            element.style.top = `${Math.min(Math.max(newTop, DIMENSOES.MARGENS.MINIMA), maxTop - DIMENSOES.MARGENS.MINIMA)}px`;

            element.style.right = 'auto';
            element.style.bottom = 'auto';

            config.posicaoJanela = {
                x: parseInt(element.style.left),
                y: parseInt(element.style.top)
            };
        };

        const closeDragElement = () => {
            isDragging = false;
            element.classList.remove('dragging');
            salvarConfiguracao();
            document.removeEventListener('mousemove', elementDrag);
            document.removeEventListener('mouseup', closeDragElement);
        };

        elementos.dragHandle.on('mousedown', dragMouseDown);
    }

    /**
     * Carrega a imagem da URL
     * @param {string} url - URL da imagem a ser carregada
     * @returns {boolean} Retorna true se o carregamento foi iniciado com sucesso
     */
    function carregarImagem(url) {
        if (!validarURL(url)) {
            elementos.status.html('<span style="color:#ff6b6b">URL inválida. Use links que terminem com .png, .jpg, .jpeg, .gif ou .webp</span>');
            return false;
        }

        mostrarCarregamento(true);
        elementos.status.text('Carregando imagem...');

        const img = new Image();
        img.crossOrigin = "Anonymous";

        img.onload = function() {
            config.urlImagem = url;
            elementos.guia.attr('src', url).show();
            elementos.status.text('Imagem carregada com sucesso!');
            mostrarCarregamento(false);
            atualizarTamanho();
            atualizarPosicao();
            salvarConfiguracao();
        };

        img.onerror = function() {
            elementos.status.html(`
                <span style="color:#ff6b6b">Falha ao carregar imagem</span>
                <div style="font-size:12px;color:#aaa;margin-top:5px;">
                    Dica: Use links diretos de serviços como Imgur (ex: https://i.imgur.com/abc123.png)
                </div>
            `);
            mostrarCarregamento(false);
        };

        img.src = url;
        return true;
    }

    /**
     * Valida a URL da imagem
     * @param {string} url - URL a ser validada
     * @returns {boolean} Retorna true se a URL é válida
     */
    function validarURL(url) {
        return /\.(png|jpe?g|gif|webp)(\?.*)?$/i.test(url);
    }

    /**
     * Mostra/oculta o indicador de carregamento
     * @param {boolean} mostrar - Se true, mostra o indicador
     */
    function mostrarCarregamento(mostrar) {
        elementos.carregarBtn.toggleClass('carregando', mostrar)
            .html(mostrar ? '<span class="loader"></span> Carregando...' : 'Carregar Imagem');
    }

    /**
     * Atualiza o tamanho da imagem guia
     */
    function atualizarTamanho() {
        elementos.guia.css({
            'width': `${config.tamanho}%`,
            'height': 'auto'
        });
        elementos.tamanhoValor.text(`${config.tamanho}%`);
        elementos.tamanhoSlider.val(config.tamanho);
        salvarConfiguracao();
    }

    /**
     * Atualiza a posição da imagem guia
     */
    function atualizarPosicao() {
        const canvas = document.querySelector('canvas');
        if (!canvas || !config.urlImagem) return;

        const rect = canvas.getBoundingClientRect();
        elementos.guia.css({
            left: `${rect.left + config.deslocamento.x}px`,
            top: `${rect.top + config.deslocamento.y}px`
        });
    }

    /**
     * Alterna entre minimizar e maximizar a janela
     */
    function toggleMinimizar() {
        config.janelaMinimizada = !config.janelaMinimizada;
        elementos.minimizarBtn.text(config.janelaMinimizada ? '⬛' : '➖');
        elementos.controles.toggleClass('minimizado', config.janelaMinimizada);

        ajustarPosicaoJanela(elementos.controles[0]);
        salvarConfiguracao();
    }

    /**
     * Salva a configuração atual
     */
    function salvarConfiguracao() {
        GM_setValue('wplaceConfig', config);
    }

    /**
     * Configura os event listeners
     */
    function setupEventListeners() {
        // Carregar imagem
        elementos.carregarBtn.on('click', () => {
            const url = elementos.urlInput.val().trim();
            if (url) carregarImagem(url);
        });

        // Controle de opacidade
        elementos.opacidade.on('input', () => {
            config.opacidade = parseFloat(elementos.opacidade.val());
            elementos.guia.css('opacity', config.opacidade);
            elementos.opacidade.prev('label').text(`Opacidade: ${config.opacidade.toFixed(1)}`);
            salvarConfiguracao();
        });

        // Controles de tamanho
        elementos.diminuirBtn.on('click', () => {
            config.tamanho = Math.max(config.tamanho - DIMENSOES.TAMANHO.STEP, DIMENSOES.TAMANHO.MIN);
            atualizarTamanho();
        });

        elementos.aumentarBtn.on('click', () => {
            config.tamanho = Math.min(config.tamanho + DIMENSOES.TAMANHO.STEP, DIMENSOES.TAMANHO.MAX);
            atualizarTamanho();
        });

        elementos.tamanhoSlider.on('input', () => {
            config.tamanho = parseInt(elementos.tamanhoSlider.val());
            atualizarTamanho();
        });

        // Botão de minimizar/maximizar
        elementos.minimizarBtn.on('click', toggleMinimizar);

        // Atalhos de teclado
        $(document).on('keydown', (e) => {
            const passo = e.shiftKey ? DIMENSOES.TAMANHO.STEP * 2 : DIMENSOES.TAMANHO.STEP;
            let atualizar = false;

            // Movimento (Ctrl + setas)
            if (e.ctrlKey && !e.altKey && !e.metaKey) {
                if (e.key === 'ArrowUp') {
                    config.deslocamento.y -= passo;
                    atualizar = true;
                } else if (e.key === 'ArrowDown') {
                    config.deslocamento.y += passo;
                    atualizar = true;
                } else if (e.key === 'ArrowLeft') {
                    config.deslocamento.x -= passo;
                    atualizar = true;
                } else if (e.key === 'ArrowRight') {
                    config.deslocamento.x += passo;
                    atualizar = true;
                }
            }
            // Tamanho (Alt + setas)
            else if (e.altKey && !e.ctrlKey && !e.metaKey) {
                if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
                    config.tamanho = Math.min(config.tamanho + passo, DIMENSOES.TAMANHO.MAX);
                    atualizar = true;
                } else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
                    config.tamanho = Math.max(config.tamanho - passo, DIMENSOES.TAMANHO.MIN);
                    atualizar = true;
                }
            }
            // Opacidade (Ctrl + Alt + setas para cima/baixo)
            else if ((e.ctrlKey && e.altKey) || (e.metaKey && e.altKey)) {
                if (e.key === 'ArrowUp') {
                    config.opacidade = Math.min(config.opacidade + DIMENSOES.OPACIDADE.STEP, DIMENSOES.OPACIDADE.MAX);
                    atualizar = true;
                } else if (e.key === 'ArrowDown') {
                    config.opacidade = Math.max(config.opacidade - DIMENSOES.OPACIDADE.STEP, DIMENSOES.OPACIDADE.MIN);
                    atualizar = true;
                }
            }

            if (atualizar) {
                if (e.altKey && !e.ctrlKey) {
                    atualizarTamanho();
                } else if ((e.ctrlKey && e.altKey) || (e.metaKey && e.altKey)) {
                    elementos.guia.css('opacity', config.opacidade);
                    elementos.opacidade.val(config.opacidade);
                    elementos.opacidade.prev('label').text(`Opacidade: ${config.opacidade.toFixed(1)}`);
                } else {
                    atualizarPosicao();
                }
                salvarConfiguracao();
                e.preventDefault();
            }
        });

        // Redimensionamento da janela
        const debouncedResize = debounce(() => {
            ajustarPosicaoJanela(elementos.controles[0]);
            atualizarPosicao();
        }, 100);

        $(window).on('resize', debouncedResize);
    }

    /**
     * Debounce para eventos frequentes
     * @param {Function} fn - Função a ser executada
     * @param {number} delay - Tempo de espera em ms
     * @returns {Function} Função debounced
     */
    function debounce(fn, delay) {
        let timer;
        return function() {
            clearTimeout(timer);
            timer = setTimeout(fn, delay);
        };
    }

    // =============================================
    // INICIALIZAÇÃO
    // =============================================
    function init() {
        // Carrega configurações salvas
        const savedConfig = GM_getValue('wplaceConfig');
        if (savedConfig) Object.assign(config, savedConfig);

        // Cria a interface
        criarInterface();

        // Configura a posição inicial
        ajustarPosicaoJanela(elementos.controles[0]);

        // Torna a janela arrastável
        makeDraggable(elementos.controles[0]);

        // Configura os event listeners
        setupEventListeners();

        // Carrega a imagem se já existir uma URL configurada
        if (config.urlImagem) carregarImagem(config.urlImagem);
    }

    // Inicia o script quando o DOM estiver pronto
    $(document).ready(init);
})();