MNG Support

Replaces img, object, embed, and CSS background-image *.mng references with animated canvas elements

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MNG Support
// @namespace    https://github.com/Denveous/mng.js
// @version      1.0.1
// @description  Replaces img, object, embed, and CSS background-image *.mng references with animated canvas elements
// @author       Denveous
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @license      GPL-3.0-or-later
// ==/UserScript==

(function() {
const PNG_SIG = new Uint8Array([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]);
const MNG_SIG = new Uint8Array([0x8A,0x4D,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]);

function readChunks(buf) {
    const data = new Uint8Array(buf), view = new DataView(buf);
    for (let i = 0; i < 8; i++) if (data[i] !== MNG_SIG[i]) throw new Error('Not MNG');
    const chunks = []; let pos = 8;
    while (pos < data.length - 12) {
        const len = view.getUint32(pos), type = String.fromCharCode(data[pos+4],data[pos+5],data[pos+6],data[pos+7]);
        chunks.push({ type, len, data: data.slice(pos+8, pos+8+len), offset: pos });
        pos += len + 12;
    }
    return chunks;
}

function parseFRAM(d) {
    let pos = 1;
    while (pos < d.length && d[pos]) pos++;
    pos++;
    if (pos >= d.length) return null;
    const hasDelay = d[pos++]; pos += 3;
    if (!hasDelay || pos + 4 > d.length) return null;
    return new DataView(d.buffer, d.byteOffset + pos).getUint32(0);
}

function parse(buf) {
    const raw = new Uint8Array(buf), chunks = readChunks(buf);
    let ticksPerSec = 100, pendingDelay = null, frameParts = [], inPNG = false;
    const frames = [];
    for (const c of chunks) {
        if (c.type === 'MHDR') { const t = new DataView(c.data.buffer, c.data.byteOffset).getUint32(8); if (t > 0) ticksPerSec = t; }
        else if (c.type === 'FRAM') { const t = parseFRAM(c.data); if (t != null) pendingDelay = Math.round(t / ticksPerSec * 1000); }
        else if (c.type === 'IHDR') { frameParts = [c]; inPNG = true; }
        else if (inPNG) {
            frameParts.push(c);
            if (c.type === 'IEND') {
                inPNG = false;
                const png = new Uint8Array(8 + frameParts.reduce((s,x) => s + x.len + 12, 0));
                png.set(PNG_SIG); let p = 8;
                for (const f of frameParts) { png.set(raw.slice(f.offset, f.offset + f.len + 12), p); p += f.len + 12; }
                frames.push({ png, delay: pendingDelay || 100 }); pendingDelay = null;
            }
        }
    }
    return frames;
}

function loadImages(frames) {
    return Promise.all(frames.map(f => new Promise((res, rej) => {
        const img = new Image(), url = URL.createObjectURL(new Blob([f.png], { type: 'image/png' }));
        img.onload = () => { URL.revokeObjectURL(url); res({ img, delay: f.delay }); };
        img.onerror = rej; img.src = url;
    })));
}

function makeCanvas(frames) {
    const canvas = document.createElement('canvas');
    canvas.width = frames[0].img.width; canvas.height = frames[0].img.height;
    const ctx = canvas.getContext('2d');
    let idx = 0, timer = null, running = false;
    const draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(frames[idx].img, 0, 0); };
    const tick = () => { draw(); timer = setTimeout(() => { idx = (idx + 1) % frames.length; tick(); }, frames[idx].delay); };
    canvas.play  = () => { if (!running) { running = true; tick(); } };
    canvas.pause = () => { clearTimeout(timer); running = false; };
    canvas.stop  = () => { canvas.pause(); idx = 0; draw(); };
    draw(); return canvas;
}

function gmFetch(url) {
    return new Promise((res, rej) => GM_xmlhttpRequest({
        method: 'GET', url, responseType: 'arraybuffer',
        onload: r => res(r.response), onerror: rej
    }));
}
function fetchBuf(url) {
    return fetch(url).then(r => r.arrayBuffer()).catch(() => gmFetch(url));
}

async function processEl(el, url) {
    if (el.dataset.mngProcessed) return;
    el.dataset.mngProcessed = '1';
    try {
        const buf = await fetchBuf(url);
        const frames = await loadImages(parse(buf));
        const canvas = makeCanvas(frames);
        canvas.style.cssText = el.style.cssText;
        if (el.width)  canvas.style.width  = el.width  + 'px';
        if (el.height) canvas.style.height = el.height + 'px';
        if (el.className) canvas.className = el.className;
        if (frames.length > 1) canvas.play();
        el.replaceWith(canvas);
    } catch(e) { console.warn('[MNG]', url, e.message); }
}

async function processBg(el) {
    if (el.dataset.mngBgProcessed) return;
    const url = el.style.backgroundImage.match(/url\(["']?([^"')]+\.mng)["']?\)/)?.[1];
    if (!url) return;
    el.dataset.mngBgProcessed = '1';
    try {
        const buf = await fetchBuf(url);
        const frames = await loadImages(parse(buf));
        const canvas = makeCanvas(frames);
        const cs = getComputedStyle(el);
        canvas.style.width = cs.width; canvas.style.height = cs.height;
        canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.left = '0';
        el.style.position = el.style.position || 'relative';
        el.style.backgroundImage = 'none';
        el.prepend(canvas);
        if (frames.length > 1) canvas.play();
    } catch(e) { console.warn('[MNG bg]', url, e.message); }
}

function scan(root) {
    if (!root.querySelectorAll) return;
    root.querySelectorAll('img[src$=".mng"]').forEach(el => processEl(el, el.src));
    root.querySelectorAll('object[data$=".mng"], embed[src$=".mng"]').forEach(el => processEl(el, el.data || el.src));
    root.querySelectorAll('*').forEach(el => { if (el.style.backgroundImage?.includes('.mng')) processBg(el); });
}

scan(document);
new MutationObserver(mutations => {
    for (const m of mutations)
        for (const n of m.addedNodes)
            if (n.nodeType === 1) scan(n);
}).observe(document.body, { childList: true, subtree: true });
})();