Lolz Paint

Ручной paint

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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('Ошибка доступа к буферу обмена.', 'Ошибка');
                });
            });
        }
    }
})();