MNG Support

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

スクリプトをインストールするには、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         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 });
})();