Lolz Paint

Ручной paint

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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('Ошибка доступа к буферу обмена.', 'Ошибка');
                });
            });
        }
    }
})();