wplace.live Guia Visual Avançado

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

// ==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);
})();