Lolz Paint

Ручной paint

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Lolz Paint
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Ручной paint
// @author       Forest
// @license      MIT
// @match        https://lolz.live/*
// @match        https://zelenka.guru/*
// @match        https://lolz.guru/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const CSS_STYLES = `
        .lz-paint-modal {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.85); z-index: 99999; display: flex;
            justify-content: center; align-items: center; flex-direction: column;
            user-select: none; font-family: 'Segoe UI', sans-serif;
        }
        .lz-editor-box {
            background: #222; padding: 10px; border-radius: 8px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; gap: 10px;
            max-width: 98vw; max-height: 98vh; position: relative;
        }
        .lz-toolbar { display: flex; gap: 10px; align-items: center; background: #333; padding: 8px; border-radius: 6px; flex-wrap: wrap; }
        .lz-btn {
            padding: 6px 10px; background: #333; border: 1px solid #444; color: #ccc;
            cursor: pointer; border-radius: 4px; font-size: 14px; transition: 0.2s;
            display: flex; align-items: center; justify-content: center; min-width: 32px;
        }
        .lz-btn:hover { background: #444; color: #fff; }
        .lz-btn.active { background: #555; border-color: #666; color: #fff; }
        .lz-canvas-wrap {
            position: relative; width: 800px; height: 500px;
            background-color: #eee;
            background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%);
            background-size: 20px 20px;
            overflow: hidden; border: 2px solid #444;
        }
        .lz-swatch { width: 20px; height: 20px; border-radius: 3px; cursor: pointer; border: 1px solid #555; }
        .lz-paint-trigger {
            float: left; display: flex; align-items: center; justify-content: center;
            cursor: pointer !important; width: 30px; height: 30px;
            color: #a4a4a4;
            background: none; border: none; margin: 0 2px;
        }
        .lz-paint-trigger:hover { color: #fff; background: rgba(255,255,255,0.1); border-radius: 4px; }
        .lz-paint-trigger i { font-size: 14px; }
        .lz-bottom-bar { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; }
        .lz-btn-green { background: #2d8a31; border: none; color: white; }
        .lz-btn-green:hover { background: #36a53b; }
    `;

    $('<style>').text(CSS_STYLES).appendTo('head');

    window.XenForo.LolzPaintBtn = function($element) {
        if ($element.find('.lz-paint-trigger').length) return;

        const $btn = $('<button type="button" class="lz-paint-trigger fr-command fr-btn Tooltip" title="Paint"><i class="fa fa-paint-brush"></i></button>');

        $btn.click(function(e) {
            e.preventDefault();
            new LolzPaintApp();
        });

        $element.append($btn);
        $element.xfActivate();
    };

    XenForo.register('.fr-toolbar, .bbCodeEditor-toolbar', 'XenForo.LolzPaintBtn');

    $(document).ready(function() {
        $('.fr-toolbar, .bbCodeEditor-toolbar').each(function() {
            XenForo.LolzPaintBtn($(this));
        });
    });

    class LolzPaintApp {
        constructor() {
            this.COLORS = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#00FFFF', '#FF00FF', '#FFFFFF', '#000000'];
            this.history = [];
            this.historyStep = -1;
            this.currentTool = 'brush';
            this.currentColor = '#FF0000';
            this.currentLineWidth = 3;
            this.isDrawing = false;
            this.currentBgType = 'color';
            this.currentBgColor = '#FFFFFF';
            this.activeTextObj = null;

            this.initUI();
        }

        initUI() {
            this.$modal = $('<div class="lz-paint-modal">');
            this.$box = $('<div class="lz-editor-box">');
            this.$toolbar = $('<div class="lz-toolbar">');
            this.$canvasWrap = $('<div class="lz-canvas-wrap">');
            this.$bottomBar = $('<div class="lz-bottom-bar">');

            this.canvas = document.createElement('canvas');
            this.canvas.width = 800;
            this.canvas.height = 500;
            this.canvas.style.display = 'block';
            this.ctx = this.canvas.getContext('2d');
            this.ctx.fillStyle = '#ffffff';
            this.ctx.fillRect(0, 0, 800, 500);

            this.$canvasWrap.append(this.canvas);
            this.initResizer();
            this.buildTools();
            this.buildPalette();
            this.buildControls();
            this.bindEvents();
            this.saveState();

            this.$box.append(this.$toolbar, this.$canvasWrap, this.$bottomBar);
            this.$modal.append(this.$box).appendTo('body');
        }

        buildTools() {
            const tools = [
                { id: 'brush', icon: '<i class="fa fa-paint-brush"></i>', title: 'Кисть' },
                { id: 'rect', icon: '<i class="fa fa-stop"></i>', title: 'Прямоугольник' },
                { id: 'arrow', icon: '<i class="fa fa-location-arrow"></i>', title: 'Стрелка' },
                { id: 'text', icon: '<i class="fa fa-font"></i>', title: 'Текст' },
                { id: 'blur', icon: '<i class="fa fa-tint"></i>', title: 'Блюр' },
                { id: 'eraser', icon: '<i class="fa fa-eraser"></i>', title: 'Ластик' }
            ];

            const $container = $('<div style="display:flex; gap:5px">');
            tools.forEach(t => {
                $('<button>', { class: 'lz-btn', html: t.icon, title: t.title })
                    .toggleClass('active', t.id === 'brush')
                    .click((e) => {
                        this.applyText();
                        this.currentTool = t.id;
                        this.$toolbar.find('.lz-btn').removeClass('active');
                        $(e.currentTarget).addClass('active');
                    })
                    .appendTo($container);
            });
            this.$toolbar.append($container);

            this.$toolbar.append('<div style="width:1px; height:20px; background:#555; margin:0 5px;"></div>');

            const bgBtns = [
                { icon: '<i class="fa fa-flask"></i>', title: 'Заливка цветом', type: 'color' },
                { icon: '<i class="fa fa-th-large"></i>', title: 'Прозрачный', type: 'transparent' }
            ];
            bgBtns.forEach(b => {
                 $('<button>', { class: 'lz-btn', html: b.icon, title: b.title }).click(() => {
                     this.applyText();
                     this.currentBgType = b.type;
                     if(b.type === 'color') this.currentBgColor = this.currentColor;
                     this.fillCanvasBackground();
                     this.saveState();
                 }).appendTo(this.$toolbar);
            });
        }

        buildPalette() {
            const $pal = $('<div style="display:flex; gap:4px; margin-left:10px; align-items:center;">');
            this.COLORS.forEach(c => {
                $('<div>', { class: 'lz-swatch' }).css('background', c)
                    .click((e) => {
                        this.updateColor(c);
                        $pal.find('.lz-swatch').css('border-color', '#555');
                        $(e.currentTarget).css('border-color', 'white');
                    }).appendTo($pal);
            });
            const $input = $('<input type="color">').val(this.currentColor).css({width:0, height:0, visibility:'hidden', position:'absolute'});
            $('<label>', { html: '🌈', style: 'cursor:pointer; font-size:20px; margin-left:5px;' })
                .append($input).appendTo($pal);
            $input.on('input', (e) => {
                 this.updateColor(e.target.value);
                 $pal.find('.lz-swatch').css('border-color', '#555');
            });
            this.$toolbar.append($pal);
        }

        buildControls() {
            $('<input>', { type: 'range', min: 1, max: 40, val: this.currentLineWidth })
                .css({width: '80px', margin: '0 10px'})
                .on('input', (e) => {
                    this.currentLineWidth = parseInt(e.target.value);
                    if(this.activeTextObj) this.activeTextObj.style.fontSize = (this.currentLineWidth + 12) + 'px';
                }).appendTo(this.$toolbar);

            $('<button>', { class: 'lz-btn', html: '<i class="fa fa-copyright"></i>', title: 'Добавить водяной знак' })
                .click(() => this.addWatermark())
                .appendTo(this.$toolbar);

            $('<button>', { class: 'lz-btn', html: '<i class="fa fa-reply"></i>', style: 'margin-left:auto' })
                .click(() => this.undo())
                .appendTo(this.$toolbar);

            $('<button>', { class: 'lz-btn', text: 'Закрыть' })
                .click(() => this.$modal.remove())
                .appendTo(this.$bottomBar);

            $('<button>', { class: 'lz-btn lz-btn-green', html: '<i class="fa fa-copy"></i> Скопировать' })
                .click(() => this.copyImage())
                .appendTo(this.$bottomBar);
        }

        updateColor(c) {
            this.currentColor = c;
            if(this.activeTextObj) this.activeTextObj.style.color = c;
        }

        fillCanvasBackground() {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            if (this.currentBgType === 'color') {
                this.ctx.fillStyle = this.currentBgColor;
                this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
            }
        }

        bindEvents() {
            const $c = $(this.canvas);
            let startX, startY, snapshot;

            $c.mousedown((e) => {
                if (this.activeTextObj && e.target !== this.activeTextObj) this.applyText();
                const rect = this.canvas.getBoundingClientRect();
                const mouseX = e.clientX - rect.left;
                const mouseY = e.clientY - rect.top;

                if (this.currentTool === 'text') {
                    this.createFloatingText(mouseX, mouseY);
                    return;
                }
                this.isDrawing = true;
                startX = mouseX; startY = mouseY;
                snapshot = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);

                if (this.currentTool === 'blur') this.pixelate(startX, startY, this.currentLineWidth * 2);
                else {
                    this.ctx.beginPath();
                    this.ctx.moveTo(startX, startY);
                }
            });

            $(window).mousemove((e) => {
                if (!this.isDrawing) return;
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left; const y = e.clientY - rect.top;

                if (this.currentTool === 'blur') {
                    this.pixelate(x, y, this.currentLineWidth * 2);
                    return;
                }
                this.ctx.lineWidth = this.currentLineWidth;
                this.ctx.strokeStyle = (this.currentTool === 'eraser') ? (this.currentBgType === 'color' ? this.currentBgColor : 'rgba(0,0,0,1)') : this.currentColor;
                this.ctx.globalCompositeOperation = (this.currentTool === 'eraser') ? 'destination-out' : 'source-over';
                this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round';

                if (this.currentTool === 'brush' || this.currentTool === 'eraser') {
                    this.ctx.lineTo(x, y); this.ctx.stroke();
                } else if (this.currentTool === 'rect') {
                    this.ctx.globalCompositeOperation = 'source-over';
                    this.ctx.putImageData(snapshot, 0, 0); this.ctx.strokeRect(startX, startY, x - startX, y - startY);
                } else if (this.currentTool === 'arrow') {
                    this.ctx.globalCompositeOperation = 'source-over';
                    this.ctx.putImageData(snapshot, 0, 0); this.drawArrow(startX, startY, x, y);
                }
                if (this.currentTool !== 'eraser') this.ctx.globalCompositeOperation = 'source-over';
            });

            $(window).mouseup(() => {
                if (this.isDrawing) { this.isDrawing = false; this.saveState(); }
                this.ctx.beginPath();
            });
            $(window).on('keydown.lzpaint', (e) => {
                if (e.ctrlKey && e.code === 'KeyZ') { e.preventDefault(); this.undo(); }
            });
            $(window).on('paste.lzpaint', (e) => {
                const items = (e.clipboardData || e.originalEvent.clipboardData).items;
                for (let item of items) {
                    if (item.kind === 'file' && item.type.includes('image/')) {
                        this.applyText();
                        const blob = item.getAsFile();
                        const reader = new FileReader();
                        reader.onload = (event) => {
                            const img = new Image();
                            img.onload = () => {
                                let w = img.width, h = img.height;
                                const maxW = window.innerWidth - 100, maxH = window.innerHeight - 200;
                                if (w > maxW) { h *= maxW/w; w = maxW; }
                                if (h > maxH) { w *= maxH/h; h = maxH; }
                                this.resizeCanvas(w, h, true);
                                this.ctx.drawImage(img, 0, 0, w, h);
                                this.saveState();
                            };
                            img.src = event.target.result;
                        };
                        reader.readAsDataURL(blob);
                    }
                }
            });
        }

        createFloatingText(x, y) {
            this.applyText();
            const div = document.createElement('div');
            div.contentEditable = true; div.innerHTML = 'Текст';
            div.style.cssText = `position: absolute; left: ${x}px; top: ${y}px; color: ${this.currentColor}; font-size: ${this.currentLineWidth + 12}px; font-family: Arial; border: 1px dashed #000; padding: 2px; min-width: 20px; z-index: 15; cursor: move; outline: none; background: rgba(255,255,255,0.3);`;

            this.$canvasWrap.append(div);
            this.activeTextObj = div;
            setTimeout(() => div.focus(), 0);

            let isDrag = false, offX, offY;
            div.onmousedown = (e) => { isDrag = true; offX = e.offsetX; offY = e.offsetY; };
            $(window).mousemove((e) => {
                if(isDrag) {
                    const r = this.$canvasWrap[0].getBoundingClientRect();
                    div.style.left = (e.clientX - r.left - offX) + 'px';
                    div.style.top = (e.clientY - r.top - offY) + 'px';
                }
            });
            $(window).mouseup(() => isDrag = false);
            div.onkeydown = (e) => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.applyText(); } };
        }

        applyText() {
            if (!this.activeTextObj) return;
            const rect = this.activeTextObj.getBoundingClientRect();
            const canvasRect = this.canvas.getBoundingClientRect();
            const x = rect.left - canvasRect.left; const y = rect.top - canvasRect.top;
            const fontSize = parseInt(this.activeTextObj.style.fontSize);
            this.ctx.font = `${fontSize}px Arial`; this.ctx.fillStyle = this.activeTextObj.style.color; this.ctx.textBaseline = 'top';
            this.ctx.fillText(this.activeTextObj.innerText, x, y + 2);
            this.activeTextObj.remove(); this.activeTextObj = null; this.saveState();
        }

        addWatermark() {
            this.applyText();
            this.ctx.save();
            const text = "Lolzteam";
            this.ctx.font = "bold 24px Arial";
            this.ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
            this.ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
            this.ctx.lineWidth = 1;
            const w = this.canvas.width;
            const h = this.canvas.height;
            const textW = this.ctx.measureText(text).width;
            this.ctx.fillText(text, w - textW - 20, h - 20);
            this.ctx.strokeText(text, w - textW - 20, h - 20);
            this.ctx.restore();
            this.saveState();
        }

        saveState() { this.historyStep++; if (this.historyStep < this.history.length) this.history.length = this.historyStep; this.history.push(this.canvas.toDataURL()); }
        undo() { if (this.historyStep > 0) { this.historyStep--; const img = new Image(); img.src = this.history[this.historyStep]; img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); }; } }

        pixelate(x, y, size) {
            const pixelSize = 6; const w = size*2; const h = size*2; const sx = x-size; const sy = y-size;
            try {
                const sampleW = Math.max(1, Math.floor(w/pixelSize)); const sampleH = Math.max(1, Math.floor(h/pixelSize));
                this.ctx.imageSmoothingEnabled = false;
                this.ctx.drawImage(this.canvas, sx, sy, w, h, sx, sy, sampleW, sampleH);
                this.ctx.drawImage(this.canvas, sx, sy, sampleW, sampleH, sx, sy, w, h);
                this.ctx.imageSmoothingEnabled = true;
            } catch(e){}
        }

        drawArrow(fromx, fromy, tox, toy) {
            const headlen = 15 + this.currentLineWidth; const dx = tox - fromx, dy = toy - fromy, angle = Math.atan2(dy, dx);
            this.ctx.beginPath(); this.ctx.moveTo(fromx, fromy); this.ctx.lineTo(tox, toy);
            this.ctx.lineTo(tox - headlen * Math.cos(angle - Math.PI/6), toy - headlen * Math.sin(angle - Math.PI/6));
            this.ctx.moveTo(tox, toy); this.ctx.lineTo(tox - headlen * Math.cos(angle + Math.PI/6), toy - headlen * Math.sin(angle + Math.PI/6));
            this.ctx.stroke();
        }

        initResizer() {
            const $resizer = $('<div>').css({
                width: '15px', height: '15px', background: 'linear-gradient(135deg, transparent 50%, #e91e63 50%)',
                position: 'absolute', bottom: 0, right: 0, cursor: 'nwse-resize', zIndex: 20
            }).appendTo(this.$canvasWrap);

            let isResizing = false;
            $resizer.mousedown((e) => { isResizing = true; e.preventDefault(); this.applyText(); });
            $(window).mouseup(() => isResizing = false);
            $(window).mousemove((e) => {
                if (!isResizing) return;
                const rect = this.$canvasWrap[0].getBoundingClientRect();
                const newW = e.clientX - rect.left; const newH = e.clientY - rect.top;
                if (newW > 100 && newH > 100) this.resizeCanvas(newW, newH);
            });
        }

        resizeCanvas(w, h, skipSave = false) {
             const tempCanvas = document.createElement('canvas');
             tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height;
             tempCanvas.getContext('2d').drawImage(this.canvas, 0, 0);
             this.$canvasWrap.css({width: w + 'px', height: h + 'px'});
             this.canvas.width = w; this.canvas.height = h;
             if (this.currentBgType === 'color') { this.ctx.fillStyle = this.currentBgColor; this.ctx.fillRect(0, 0, w, h); }
             else { this.ctx.clearRect(0, 0, w, h); }
             this.ctx.drawImage(tempCanvas, 0, 0);
             if (!skipSave) this.saveState();
        }

        copyImage() {
            this.applyText();
            this.canvas.toBlob(blob => {
                const item = new ClipboardItem({ "image/png": blob });
                navigator.clipboard.write([item]).then(() => {
                    XenForo.alert('Изображение скопировано!<br>Нажми Ctrl+V в редакторе.', 'Успешно');
                    this.$modal.remove();
                }).catch(err => {
                    XenForo.alert('Ошибка доступа к буферу обмена.', 'Ошибка');
                });
            });
        }
    }
})();