LZTQuoteBackground

Add custom SVG background and stylish borders to quotes on lolz.live

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         LZTQuoteBackground
// @namespace    MeloniuM/LZT
// @version      1.3
// @description  Add custom SVG background and stylish borders to quotes on lolz.live
// @author       MeloniuM
// @match        https://lolz.live/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lolz.live
// @grant        none
// ==/UserScript==
(function () {
    'use strict';

    $("<style/>").text(`
    .lzt-quote {
        position: relative;
        padding-left: 10px;
        overflow: inherit;
        border-radius: 6px;
        background-color: var(--bg-color, rgba(0, 0, 0, 0.03)) !important;
        background-image: var(--bg-image) !important;
        background-repeat: no-repeat;
        background-size: auto 100%;
        background-position: right center;
        border-left: var(--border-left, 5px solid #2bad72) !important;
        /*border-image: var(--border-image, none) !important;*/
        border-image-slice: 1 !important;
        box-shadow: var(--box-shadow, none) !important;
    }

    .lzt-quote::before {
        content: "";
        position: absolute;
        top: 0; left: 0; bottom: 0;
        width: 5px;
        border-radius: 6px 0 0 6px;
        background: var(--border-image) !important; /* или background: <svg data-url> */
        pointer-events: none;
    }

    `).appendTo("head");

    // Конфигурация
    const MASK_GROUPS = [{
        x: 68,
        y: 1,
        originalScale: 0.2
    }, {
        x: 70,
        y: 28,
        originalScale: 0.3
    }, {
        x: 30,
        y: 12,
        originalScale: 0.17
    }, {
        x: 6,
        y: 30,
        originalScale: 0.11
    }, {
        x: 30,
        y: 50,
        originalScale: 0.13
    }];
    const MIN_PIXEL_SIZE = 10;
    const MAX_PIXEL_SIZE = 20;
    const ORIGINAL_SCALES = MASK_GROUPS.map(g => g.originalScale);
    const MIN_ORIGINAL_SCALE = Math.min(...ORIGINAL_SCALES);
    const MAX_ORIGINAL_SCALE = Math.max(...ORIGINAL_SCALES);
    const DEFAULT_COLOR = '#2BAD72';
    const DEFAULT_WIDTH = 320;
    const DEFAULT_HEIGHT = 512;
    // Кеш для SVG-фонов
    const backgroundCache = new Map();

    function generateSvgBackground(svgContent, iconWidth, iconHeight, iconColor) {
        const cacheKey = `${svgContent}|${iconColor}`;
        if (backgroundCache.has(cacheKey)) {
            return backgroundCache.get(cacheKey);
        }
        const maskContent = MASK_GROUPS.map(group => {
            const normalizedScale = (group.originalScale - MIN_ORIGINAL_SCALE) / (MAX_ORIGINAL_SCALE - MIN_ORIGINAL_SCALE);
            const pixelSize = MIN_PIXEL_SIZE + normalizedScale * (MAX_PIXEL_SIZE - MIN_PIXEL_SIZE);
            const scale = pixelSize / iconWidth;
            const cx = iconWidth / 2;
            const cy = iconHeight / 2;
            return `
                <g transform="translate(${group.x}, ${group.y}) scale(${scale})">
                    <g fill="${iconColor}" style="transform-origin: ${cx}px ${cy}px;">
                        ${svgContent}
                    </g>
                </g>
            `;
        }).join('');
        const outputSvg = `
            <svg width="112" height="68" viewBox="0 0 112 68" fill="none" xmlns="http://www.w3.org/2000/svg">
                <defs>
                    <radialGradient id="fadeGradient" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5">
                        <stop offset="0%" stop-color="white" stop-opacity="1"/>
                        <stop offset="100%" stop-color="white" stop-opacity="0"/>
                    </radialGradient>
                    <mask id="fadeMask" maskUnits="userSpaceOnUse" x="0" y="0" width="112" height="68">
                        <rect width="112" height="68" fill="url(#fadeGradient)" />
                    </mask>
                </defs>
                <g mask="url(#fadeMask)">
                    ${maskContent}
                </g>
            </svg>
        `;

        const base64Svg = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(outputSvg)))}`;
        backgroundCache.set(cacheKey, base64Svg);
        return base64Svg;
    }

    function getAnalogousColor(hex) {
        const {
            r,
            g,
            b
        } = hexToRgb(hex);
        let {
            h,
            s,
            l
        } = rgbToHsl(r, g, b);
        h = (h + 30) % 360;
        s = Math.min(s * 0.5, 0.5);
        l = Math.min(l * 0.7, 0.7);
        const {
            r: newR,
            g: newG,
            b: newB
        } = hslToRgb(h / 360, s, l);
        return `rgba(${newR}, ${newG}, ${newB}, 0.12)`;
    }

    function hexToRgb(hex) {
        hex = hex.replace(/^#/, '');
        const bigint = parseInt(hex, 16);
        return {
            r: (bigint >> 16) & 255,
            g: (bigint >> 8) & 255,
            b: bigint & 255
        };
    }

    function rgbToHsl(r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;
        const max = Math.max(r, g, b),
            min = Math.min(r, g, b);
        let h, s, l = (max + min) / 2;
        if (max === min) {
            h = s = 0;
        } else {
            const d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch (max) {
            case r:
                h = (g - b) / d + (g < b ? 6 : 0);
                break;
            case g:
                h = (b - r) / d + 2;
                break;
            case b:
                h = (r - g) / d + 4;
                break;
            }
            h *= 60;
        }
        return {
            h,
            s,
            l
        };
    }

    function hslToRgb(h, s, l) {
        let r, g, b;
        if (s === 0) {
            r = g = b = l;
        } else {
            const hue2rgb = (p, q, t) => {
                if (t < 0) t += 1;
                if (t > 1) t -= 1;
                if (t < 1 / 6) return p + (q - p) * 6 * t;
                if (t < 1 / 2) return q;
                if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
                return p;
            };
            const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            const p = 2 * l - q;
            r = hue2rgb(p, q, h + 1 / 3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1 / 3);
        }
        return {
            r: Math.round(r * 255),
            g: Math.round(g * 255),
            b: Math.round(b * 255)
        };
    }

    function extractBorderStyle(usernameElement) {
        if (!usernameElement) return {
            border: `5px solid ${DEFAULT_COLOR}`,
            shadow: 'none',
            image: null
        };

        const style = usernameElement.getAttribute('style') || '';
        const computedStyle = window.getComputedStyle(usernameElement);

        const bgMatch = style.match(/background\s*:\s*([^;]+)/i);
        const colorMatch = style.match(/color\s*:\s*([^;]+)/i) || (computedStyle.color !== 'rgba(0, 0, 0, 0)' ? [null, computedStyle.color] : null);
        const textShadowMatch = style.match(/text-shadow\s*:\s*([^;]+)/i) || (computedStyle.textShadow !== 'none' ? [null, computedStyle.textShadow] : null);
        const gradientMatches = style.match(/(linear|radial)-gradient\((?:(?:rgba?\([^)]+\)|[^)])+)\)/gi) ||
              (computedStyle.background.includes('gradient') ? [computedStyle.background] : []);

        let border = '';
        let image = null;
        let shadow = 'none';

        if (bgMatch && !gradientMatches.length) {
            border = `5px solid ${bgMatch[1]}`;
        } else if (colorMatch && colorMatch[1] !== 'transparent') {
            border = `5px solid ${colorMatch[1]}`;
        } else {
            border = `5px solid ${DEFAULT_COLOR}`;
        }

        if (textShadowMatch) {
            shadow = textShadowMatch[1];
        }

        if (gradientMatches.length > 0) {
            const cleanedGradients = gradientMatches.map(g => g.trim());
            // объединяем в строку через запятую
            const combinedGradient = cleanedGradients.join(', ');
            image = combinedGradient;
        }

        return {
            border,
            shadow,
            image
        };
    }


    function applyQuoteBackground($target) {
        const iconElement = $target.find('.quoteAuthor').first().find('.uniqUsernameIcon--custom svg').get(0);
        let backgroundSvg = '';
        let iconColor = DEFAULT_COLOR;

        if (iconElement) {
            const inputSvgContent = iconElement.innerHTML;

            // Поиск цвета из иконки
            const pathElement = iconElement.querySelector('path');
            if (pathElement) {
                iconColor = pathElement.getAttribute('fill') || (pathElement.getAttribute('style')?.match(/fill:\s*([^;]+)/)?.[1]);
            }
            if (!iconColor) {
                iconColor = iconElement.getAttribute('fill') || iconElement.getAttribute('style')?.match(/fill:\s*([^;]+)/)?.[1];
            }
            if (!iconColor) {
            iconColor = iconElement.getAttribute('style')?.match(/color:\s*([^;]+)/)?.[1];
            }

            if (!iconColor) {
                const gradient = iconElement.getAttribute('style')?.match(/fill:\s*url\(#(\w+)\)/)?.[1];
                if (gradient) {
                    const gradientElement = iconElement.querySelector(`#${gradient}`);
                    if (gradientElement) {
                        const firstStop = gradientElement.querySelector('stop');
                        if (firstStop) {
                            iconColor = firstStop.getAttribute('stop-color');
                        }
                    }
                }
        }

            iconColor = (iconColor || DEFAULT_COLOR).replace(/["']/g, '');

            let iconWidth = DEFAULT_WIDTH;
            let iconHeight = DEFAULT_HEIGHT;
            const viewBox = iconElement.getAttribute('viewBox');
            if (viewBox) {
            const [, , width, height] = viewBox.split(' ').map(Number);
                iconWidth = width || iconWidth;
                iconHeight = height || iconHeight;
            } else {
                iconWidth = parseFloat(iconElement.getAttribute('width')) || iconWidth;
                iconHeight = parseFloat(iconElement.getAttribute('height')) || iconHeight;
        }

            backgroundSvg = generateSvgBackground(inputSvgContent, iconWidth, iconHeight, iconColor);
        }

        // Извлекаем стиль границы из ника
        const usernameElement = $target.find('.quoteAuthor .username').first().children().first().get(0);

        const bgColor = getAnalogousColor(iconColor);
        const { border, shadow, image } = extractBorderStyle(usernameElement);

        $target.addClass('lzt-quote');

        const el = $target[0];
        el.style.setProperty('--bg-color', bgColor);
        el.style.setProperty('--bg-image', `url(${backgroundSvg})`);
        el.style.setProperty('--border-left', border);

        el.style.setProperty('--box-shadow', shadow || 'none');

        if (image) {
            // Есть border-image — делаем border-left прозрачным, чтобы показать border-image
            el.style.setProperty('--border-left', '0 solid transparent');
            el.style.setProperty('--border-image', image);
        } else {
            // Обычная граница
            el.style.setProperty('--border-left', border);
            el.style.removeProperty('--border-image');
        }

    }

    // Регистрация и начальная обработка
    XenForo.LZTQuoteBackground = function ($target) {
        applyQuoteBackground($target);
    };
    XenForo.register('.message .bbCodeQuote, .comment .bbCodeQuote', 'XenForo.LZTQuoteBackground');
    // Обработка существующих цитат
    $('.message .bbCodeQuote, .comment .bbCodeQuote').each(function () {
        applyQuoteBackground($(this));
    });
    // Обработка динамически загружаемых цитат
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) { // Element node
                    $(node).find('.bbCodeQuote').each(function () {
                        applyQuoteBackground($(this));
                    });
                }
            });
        });
    });
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();