MNG Support

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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 });
})();