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 });
})();