tiled_map_web_viewer 0.2.2

A Tiled map viewer with WASM support for web deployment
Documentation
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tiled Map Web Viewer</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: #111;
        }
        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
        }
        #tmwv-loader {
            position: fixed;
            inset: 0;
            z-index: 99999;
            display: flex;
            align-items: center;
            justify-content: center;
            background: #0f1115;
            color: #f3f4f6;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
            transition: opacity 220ms ease, visibility 220ms ease;
        }
        #tmwv-loader.is-hidden {
            opacity: 0;
            visibility: hidden;
            pointer-events: none;
        }
        .tmwv-loader-card {
            width: min(360px, calc(100vw - 48px));
            padding: 24px 0;
        }
        .tmwv-loader-title {
            margin: 0;
            font-size: 18px;
            font-weight: 600;
            line-height: 1.35;
            letter-spacing: -0.01em;
        }
        .tmwv-loader-status {
            margin-top: 8px;
            color: rgba(243, 244, 246, 0.78);
            font-size: 14px;
            line-height: 1.5;
        }
        .tmwv-loader-track {
            margin-top: 16px;
            height: 3px;
            overflow: hidden;
            border-radius: 999px;
            background: rgba(255, 255, 255, 0.12);
        }
        .tmwv-loader-bar {
            height: 100%;
            width: 2%;
            border-radius: inherit;
            background: #f3f4f6;
            transition: width 140ms ease;
        }
        .tmwv-loader-meta {
            margin-top: 8px;
            color: rgba(243, 244, 246, 0.5);
            font-size: 12px;
            line-height: 1.45;
        }
    </style>
    <script>
        (() => {
            const state = {
                progress: 0.02,
                title: "Loading viewer",
                status: "Preparing page shell…",
                meta: "Waiting for the WebAssembly bundle…",
                finished: false,
            };

            function onDomReady(callback) {
                if (document.readyState === "loading") {
                    document.addEventListener("DOMContentLoaded", callback, { once: true });
                } else {
                    callback();
                }
            }

            function formatBytes(bytes) {
                if (!Number.isFinite(bytes) || bytes <= 0) {
                    return "unknown size";
                }
                const units = ["B", "KB", "MB", "GB"];
                let value = bytes;
                let unit = units[0];
                for (let i = 0; i < units.length; i += 1) {
                    unit = units[i];
                    if (value < 1024 || i === units.length - 1) {
                        break;
                    }
                    value /= 1024;
                }
                return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${unit}`;
            }

            function render() {
                const loader = document.getElementById("tmwv-loader");
                if (!loader) {
                    return;
                }
                const title = document.getElementById("tmwv-loader-title");
                const status = document.getElementById("tmwv-loader-status");
                const meta = document.getElementById("tmwv-loader-meta");
                const bar = document.getElementById("tmwv-loader-bar");
                if (title) title.textContent = state.title;
                if (status) status.textContent = state.status;
                if (meta) meta.textContent = state.meta;
                if (bar) bar.style.width = `${Math.max(2, Math.min(100, state.progress * 100))}%`;
            }

            function update(partial) {
                Object.assign(state, partial);
                onDomReady(render);
            }

            function complete() {
                if (state.finished) {
                    return;
                }
                state.finished = true;
                update({
                    progress: 1,
                    title: "Viewer ready",
                    status: "Starting the interactive map viewer…",
                    meta: "The page shell is ready. Handing off to the app.",
                });
                onDomReady(() => {
                    window.requestAnimationFrame(() => {
                        window.requestAnimationFrame(() => {
                            const loader = document.getElementById("tmwv-loader");
                            if (!loader) {
                                return;
                            }
                            loader.classList.add("is-hidden");
                            window.setTimeout(() => loader.remove(), 260);
                        });
                    });
                });
            }

            function fail(message) {
                update({
                    title: "Viewer failed to load",
                    status: message,
                    meta: "Open the browser console for more details.",
                });
            }

            window.__tmwvLoader = {
                update,
                complete,
                fail,
                markAppReady: complete,
            };

            onDomReady(render);

            const originalFetch = window.fetch.bind(window);
            window.fetch = async function patchedFetch(input, init) {
                const url =
                    typeof input === "string"
                        ? input
                        : input instanceof Request
                          ? input.url
                          : String(input);
                if (!/\.wasm(?:\?|$)/.test(url)) {
                    return originalFetch(input, init);
                }

                update({
                    title: "Loading WebAssembly",
                    status: "Requesting the viewer runtime…",
                    meta: "Waiting for the server to start streaming the .wasm bundle.",
                    progress: 0.04,
                });

                let response;
                try {
                    response = await originalFetch(input, init);
                } catch (error) {
                    fail("The WebAssembly bundle could not be downloaded.");
                    throw error;
                }

                if (!response.ok) {
                    fail(`The server returned ${response.status} while loading the viewer.`);
                    return response;
                }

                update({
                    title: "Loading WebAssembly",
                    status: "Downloading the viewer runtime…",
                    meta: "Streaming the .wasm bundle from the server.",
                    progress: 0.08,
                });

                const totalBytes = Number(response.headers.get("content-length") || 0);
                if (!response.body || !Number.isFinite(totalBytes) || totalBytes <= 0) {
                    update({
                        progress: 0.45,
                        meta: "Download size not provided by the server. Progress is approximate.",
                    });
                    return response;
                }

                const reader = response.body.getReader();
                let loadedBytes = 0;
                const trackedStream = new ReadableStream({
                    async pull(controller) {
                        const { done, value } = await reader.read();
                        if (done) {
                            update({
                                title: "Initializing WebAssembly",
                                status: "Download complete. Preparing the runtime…",
                                meta: `${formatBytes(loadedBytes)} downloaded.`,
                                progress: 0.82,
                            });
                            controller.close();
                            return;
                        }

                        loadedBytes += value.byteLength;
                        update({
                            progress: 0.08 + (loadedBytes / totalBytes) * 0.7,
                            meta: `${formatBytes(loadedBytes)} / ${formatBytes(totalBytes)}`,
                        });
                        controller.enqueue(value);
                    },
                    cancel(reason) {
                        return reader.cancel(reason);
                    },
                });

                return new Response(trackedStream, {
                    status: response.status,
                    statusText: response.statusText,
                    headers: response.headers,
                });
            };

            if (typeof WebAssembly.instantiateStreaming === "function") {
                const originalInstantiateStreaming = WebAssembly.instantiateStreaming.bind(WebAssembly);
                WebAssembly.instantiateStreaming = async (...args) => {
                    update({
                        title: "Compiling WebAssembly",
                        status: "Optimizing the viewer runtime for this browser…",
                        meta: "This can take a moment on slower devices.",
                        progress: 0.9,
                    });
                    try {
                        const result = await originalInstantiateStreaming(...args);
                        update({
                            title: "Starting viewer",
                            status: "Booting the app and preparing the first frame…",
                            meta: "Almost there.",
                            progress: 0.97,
                        });
                        window.setTimeout(complete, 180);
                        return result;
                    } catch (error) {
                        fail("WebAssembly compilation failed.");
                        throw error;
                    }
                };
            }

            const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
            WebAssembly.instantiate = async (...args) => {
                update({
                    title: "Starting viewer",
                    status: "Initializing the WebAssembly module…",
                    meta: "Waiting for the runtime to become interactive.",
                    progress: Math.max(state.progress, 0.92),
                });
                try {
                    const result = await originalInstantiate(...args);
                    window.setTimeout(complete, 180);
                    return result;
                } catch (error) {
                    fail("WebAssembly initialization failed.");
                    throw error;
                }
            };

            window.addEventListener("error", () => {
                fail("An unexpected page error interrupted startup.");
            });
            window.addEventListener("unhandledrejection", () => {
                fail("An unhandled startup error interrupted the viewer.");
            });
        })();
    </script>
    <link data-trunk rel="rust" data-bin="tiled_map_web_viewer" />
    <link data-trunk rel="copy-dir" href="assets" />
</head>
<body>
    <div id="tmwv-loader" aria-live="polite">
        <div class="tmwv-loader-card">
            <h1 id="tmwv-loader-title" class="tmwv-loader-title">Loading viewer</h1>
            <div id="tmwv-loader-status" class="tmwv-loader-status">Preparing page shell…</div>
            <div class="tmwv-loader-track">
                <div id="tmwv-loader-bar" class="tmwv-loader-bar"></div>
            </div>
            <div id="tmwv-loader-meta" class="tmwv-loader-meta">
                Waiting for the WebAssembly bundle…
            </div>
        </div>
    </div>
    <canvas id="the_canvas_id"></canvas>
</body>
</html>