Lolz Paint

Ручной paint

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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