// src/calibration.json var calibration_default = { randomWalk: 0.8068229166666667, measured: { "Timestral-5M": { randomWalk: { mean: 0.8068229166666667, min: 0.7520833333333333, max: 0.8708333333333333, n: 20 }, sine: { mean: 0.930885416666667, min: 0.7916666666666666, max: 0.9895833333333334, n: 20 }, flatThenJump: { mean: 0.8483333333333333, min: 0.6854166666666667, max: 0.9854166666666667, n: 20 }, noise: { mean: 0.7763020833333334, min: 0.7572916666666667, max: 0.7927083333333333, n: 20 } }, "Timestral-28M": { randomWalk: { mean: 0.8169270833333334, min: 0.746875, max: 0.8802083333333334, n: 20 }, sine: { mean: 0.9510416666666668, min: 0.884375, max: 0.9979166666666667, n: 20 }, flatThenJump: { mean: 0.84734375, min: 0.6552083333333333, max: 0.9947916666666666, n: 20 }, noise: { mean: 0.80234375, min: 0.7854166666666667, max: 0.8260416666666667, n: 20 } } } }; // src/config.ts var SAMPLE_HZ = 32; var PATCH = 32; var CONTEXT = 512; var NQ = 21; var WARMUP = 2 * SAMPLE_HZ; var PLAY = 30 * SAMPLE_HZ; var LO = 2; var HI = 18; var OUTER_LO = 0; var OUTER_HI = 20; var MEDIAN = 10; var MODELS = { normal: { name: "Timestral-5M", file: "models/timestral-5m.onnx", bytes: 21212151, backend: "wasm" }, hard: { name: "Timestral-28M", file: "models/timestral-28m.onnx", bytes: 111840101, backend: "webgpu" } }; var REFERENCE = calibration_default.randomWalk; var SHARE_URL = "hf.co/spaces/shreyask/unpredictable"; // src/game/sampler.ts function createSampler(hz = SAMPLE_HZ, lagMs = 50) { const dt = 1e3 / hz; let events = []; let next = NaN; const valueAt = (t) => { let i = events.length - 1; while (i > 0 && events[i].t > t) i--; const a = events[i], b = events[i + 1]; if (!b || t <= a.t) return a.y; return a.y + (b.y - a.y) * (t - a.t) / (b.t - a.t); }; return { push(tMs, y) { if (events.length && tMs <= events.at(-1).t) return; events.push({ t: tMs, y }); if (Number.isNaN(next)) next = tMs; }, drain(nowMs) { const out = []; if (!events.length) return out; while (next <= nowMs - lagMs) { out.push(valueAt(next)); next += dt; } let keep = events.length - 1; while (keep > 0 && events[keep].t > next) keep--; events = events.slice(keep); return out; }, reset() { events = []; next = NaN; } }; } // src/game/round.ts var bandFor = (i, warmup = WARMUP, patch = PATCH) => warmup + Math.floor((i - warmup) / patch) * patch; function createRound({ warmup = WARMUP, play: play2 = PLAY, patch = PATCH, context = CONTEXT } = {}) { const samples = []; const bands = /* @__PURE__ */ new Map(); const pending = /* @__PURE__ */ new Set(); const end = warmup + play2; return { samples, bands, get done() { return samples.length >= end; }, push(y) { if (samples.length >= end) return null; samples.push(Math.min(1, Math.max(0, y))); const n = samples.length; if (n >= warmup && n < end && (n - warmup) % patch === 0) { pending.add(n); return { issue: n, context: samples.slice(Math.max(0, n - context)) }; } return null; }, deliver(issue, q) { if (!pending.delete(issue)) return; bands.set(issue, { issue, q, arrived: samples.length }); }, result() { let caught = 0, scored = 0, unscored = 0; const escapes = []; for (let i = warmup; i < Math.min(samples.length, end); i++) { const b = bands.get(bandFor(i, warmup, patch)); if (!b || i < b.arrived) { unscored++; continue; } const h2 = i - b.issue; const y = samples[i]; scored++; if (y >= b.q[h2 * NQ + LO] && y <= b.q[h2 * NQ + HI]) caught++; else escapes.push(i); } return { caught, scored, unscored, escapes, caughtPct: scored ? caught / scored : null }; } }; } // src/model/forecaster.ts var CACHE_NAME = "unpredictable-models-v1"; function pickBackend(preferred, hasWebGPU) { return preferred === "webgpu" && !hasWebGPU ? "wasm" : preferred; } async function detectWebGPU() { try { return !!await navigator.gpu?.requestAdapter(); } catch { return false; } } async function loadModelBytes(file, onProgress, expectedBytes) { const url = new URL(file, location.href).href; let cache2 = null; try { cache2 = await caches.open(CACHE_NAME); const hit = await cache2.match(url); if (hit) { const buf = await hit.arrayBuffer(); if (expectedBytes === void 0 || buf.byteLength === expectedBytes) return buf; await cache2.delete(url).catch(() => false); } } catch { cache2 = null; } const res = await fetch(url); if (!res.ok || !res.body) throw new Error(`model download failed (HTTP ${res.status})`); const total = Number(res.headers.get("content-length")) || 0; const reader = res.body.getReader(); const parts = []; let loaded = 0; for (; ; ) { const { done, value } = await reader.read(); if (done) break; parts.push(value); loaded += value.length; onProgress?.(loaded, expectedBytes || total || loaded); } const bytes = new Uint8Array(loaded); let o = 0; for (const p of parts) bytes.set(p, (o += p.length) - p.length); try { await cache2?.put(url, new Response(bytes.slice())); } catch { } return bytes.buffer; } async function createForecaster(file, preferred, onProgress, opts = {}) { const backend = pickBackend(preferred, opts.hasWebGPU ?? await detectWebGPU()); const bytes = await loadModelBytes(file, onProgress, opts.expectedBytes); opts.onStage?.("starting"); const worker = opts.makeWorker ? opts.makeWorker() : new Worker(new URL("./worker.js", location.href), { type: "module" }); let next = 0; let dead = null; let used = backend; const waiting = /* @__PURE__ */ new Map(); let rejectLoaded = () => { }; const fail = (err) => { dead ??= err; rejectLoaded(dead); for (const w of waiting.values()) w.reject(dead); waiting.clear(); }; const loaded = new Promise((resolve, reject) => { rejectLoaded = reject; worker.onmessage = (e) => { const m = e.data; if (m.type === "loaded") { used = m.backend; return resolve(); } if (m.type === "result") { waiting.get(m.id)?.resolve({ quantiles: m.quantiles, ms: m.ms }); waiting.delete(m.id); } else if (m.type === "error") { if (m.id === void 0) return fail(new Error(m.message)); waiting.get(m.id)?.reject(new Error(m.message)); waiting.delete(m.id); } }; worker.onerror = (e) => fail(new Error(e.message || "worker crashed")); worker.onmessageerror = () => fail(new Error("worker message could not be deserialized")); }); worker.postMessage({ type: "load", url: file, backend, bytes }, [bytes]); try { await loaded; } catch (err) { worker.terminate(); throw err; } return { backend: used, get dead() { return dead !== null; }, forecast(context) { if (dead) return Promise.reject(dead); const id = next++; const ctx = Float64Array.from(context); return new Promise((resolve, reject) => { waiting.set(id, { resolve, reject }); worker.postMessage({ type: "forecast", id, context: ctx }, [ctx.buffer]); }); }, close() { fail(new Error("forecaster closed")); worker.terminate(); } }; } // src/ui/geometry.ts function makeView(width, height) { const penX = 0.6 * width; const pad = Math.round(height * 0.06); return { width, height, penX, pxPerSample: penX / (6 * SAMPLE_HZ), top: pad, plotH: height - 2 * pad }; } var toY = (v, view) => view.top + (1 - v) * view.plotH; var sampleX = (i, n, view) => view.penX - (n - 1 - i) * view.pxPerSample; function bandPolygon(q, lo, hi, issue, n, view) { const steps = q.length / NQ; const upper = []; const lower = []; for (let h2 = 0; h2 < steps; h2++) { const x = sampleX(issue + h2, n, view); upper.push([x, toY(q[h2 * NQ + hi], view)]); lower.push([x, toY(q[h2 * NQ + lo], view)]); } return [...upper, ...lower.reverse()]; } var pointerToValue = (clientY, rectTop, rectHeight, view) => 1 - ((clientY - rectTop) * (view.height / rectHeight) - view.top) / view.plotH; // src/ui/card-layout.ts var pct = (x) => `${Math.round(x * 100)}%`; function cardLayout(i) { const texts = [ { text: i.caughtPct === null ? "Timestral couldn't score this round" : `Timestral caught me ${pct(i.caughtPct)} of the time`, x: 60, y: 110, size: 56, weight: 700, align: "left" } ]; if (i.reference !== null) texts.push({ text: `A random scribble: ${pct(i.reference)}`, x: 60, y: 165, size: 28, weight: 400, align: "left" }); texts.push( { text: `${i.model} \xB7 ran on my device in ${i.p50Ms.toFixed(1)} ms`, x: 60, y: 630, size: 26, weight: 400, align: "left" }, { text: i.url, x: 1140, y: 630, size: 26, weight: 700, align: "right" } ); return { width: 1200, height: 675, strip: { x: 60, y: 210, w: 1080, h: 360 }, texts }; } // src/ui/render.ts function readColors(el) { const s = getComputedStyle(el); const v = (n) => s.getPropertyValue(n).trim(); return { band: v("--band"), faint: v("--faint"), pen: v("--pen"), escape: v("--escape"), muted: v("--muted") }; } function fill(ctx, poly, color) { ctx.beginPath(); poly.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); } function drawRound(ctx, round, view, c, escapes, n, from, penWidth = 3) { ctx.save(); ctx.beginPath(); ctx.rect(0, view.top, view.width, view.plotH); ctx.clip(); for (const b of round.bands.values()) { if (sampleX(b.issue + PATCH, n, view) < 0 || b.issue < from) continue; fill(ctx, bandPolygon(b.q, OUTER_LO, OUTER_HI, b.issue, n, view), c.faint); fill(ctx, bandPolygon(b.q, LO, HI, b.issue, n, view), c.band); ctx.beginPath(); for (let h2 = 0; h2 < PATCH; h2++) { const x = sampleX(b.issue + h2, n, view), y = toY(b.q[h2 * NQ + MEDIAN], view); h2 ? ctx.lineTo(x, y) : ctx.moveTo(x, y); } ctx.strokeStyle = c.muted; ctx.lineWidth = 1; ctx.stroke(); } const s = round.samples; ctx.lineWidth = penWidth; ctx.lineJoin = "round"; let run = false, runColor = ""; const flush = () => run && (ctx.strokeStyle = runColor, ctx.stroke()); for (let i = Math.max(from + 1, 1); i < s.length; i++) { const x0 = sampleX(i - 1, n, view); if (x0 < -10) continue; const color = escapes.has(i) ? c.escape : c.pen; if (!run || color !== runColor) { flush(); ctx.beginPath(); ctx.moveTo(x0, toY(s[i - 1], view)); run = true; runColor = color; } ctx.lineTo(sampleX(i, n, view), toY(s[i], view)); } flush(); ctx.restore(); } function drawPlay(ctx, round, view, c, escapes) { ctx.clearRect(0, 0, view.width, view.height); drawRound(ctx, round, view, c, escapes, round.samples.length, 0); const s = round.samples; if (s.length) { ctx.beginPath(); ctx.arc(view.penX, toY(s.at(-1), view), 7, 0, 2 * Math.PI); ctx.fillStyle = c.pen; ctx.fill(); } } function drawStrip(ctx, round, w, h2, c, escapes) { const n = round.samples.length; const view = { ...makeView(w, h2), penX: w, pxPerSample: w / Math.max(1, n - WARMUP) }; ctx.clearRect(0, 0, w, h2); drawRound(ctx, round, view, c, escapes, n, WARMUP, 2); } // src/ui/card.ts function canvasToBlob(canvas) { return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("couldn't render the card")), "image/png")); } function prepareCard(round, p50, model) { const layout = cardLayout({ caughtPct: round.result().caughtPct, reference: REFERENCE, model, p50Ms: p50, url: SHARE_URL }); const canvas = document.createElement("canvas"); canvas.width = layout.width; canvas.height = layout.height; const ctx = canvas.getContext("2d"); const colors = readColors(document.documentElement); const s = layout.strip; ctx.save(); ctx.translate(s.x, s.y); drawStrip(ctx, round, s.w, s.h, colors, new Set(round.result().escapes)); ctx.restore(); ctx.globalCompositeOperation = "destination-over"; ctx.fillStyle = getComputedStyle(document.body).backgroundColor; ctx.fillRect(0, 0, layout.width, layout.height); ctx.globalCompositeOperation = "source-over"; ctx.fillStyle = getComputedStyle(document.body).color; for (const t of layout.texts) { ctx.font = `${t.weight} ${t.size}px system-ui, sans-serif`; ctx.textAlign = t.align; ctx.fillText(t.text, t.x, t.y); } return { text: layout.texts[0].text, blob: canvasToBlob(canvas) }; } async function runShare(btn, go) { if (btn.disabled) return; btn.disabled = true; try { const how = await go(); if (how !== "cancelled") btn.textContent = how === "copied" ? "Copied image" : how === "downloaded" ? "Downloaded image" : "Shared"; } catch { btn.textContent = "Couldn't share"; } finally { btn.disabled = false; } } async function deliverCard(blobOrPromise, text, nav = navigator, doc = document) { const blob = blobOrPromise instanceof Blob ? blobOrPromise : await blobOrPromise; const file = new File([blob], "unpredictable.png", { type: "image/png" }); if (nav.canShare?.({ files: [file] })) { try { await nav.share({ files: [file], text }); return "shared"; } catch (err) { if (err?.name === "AbortError") return "cancelled"; } } try { await nav.clipboard.write([new ClipboardItem({ "image/png": Promise.resolve(blob) })]); return "copied"; } catch { const url = URL.createObjectURL(blob); const a = Object.assign(doc.createElement("a"), { href: url, download: "unpredictable.png" }); doc.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1e4); return "downloaded"; } } // src/model/cache.ts var MAX_FAILS = 3; function createForecasterCache(create) { const have = /* @__PURE__ */ new Map(); const loading = /* @__PURE__ */ new Map(); const listeners = /* @__PURE__ */ new Map(); const last = /* @__PURE__ */ new Map(); const fails = /* @__PURE__ */ new Map(); const hooks = (m) => ({ onProgress: (l, t) => { last.set(m, { ...last.get(m), progress: [l, t] }); listeners.get(m)?.onProgress(l, t); }, onStage: (s) => { last.set(m, { ...last.get(m), starting: true }); listeners.get(m)?.onStage(s); } }); const begin = (m, backend) => { last.delete(m); const p = create(m, backend, hooks(m)).then( (f) => { if (loading.get(m) === p) have.set(m, f), loading.delete(m), fails.delete(m); else f.close(); return f; }, (err) => { if (loading.get(m) === p) loading.delete(m); return Promise.reject(err); } ); loading.set(m, p); return p; }; return { /** The latest listener for a mode wins; it is brought up to date with what already happened. */ get(m, listener) { listeners.set(m, listener); const f = have.get(m); if (f) return Promise.resolve(f); const seen = last.get(m); if (seen?.progress) listener.onProgress(...seen.progress); if (seen?.starting) listener.onStage("starting"); return loading.get(m) ?? begin(m); }, ok(m, f) { if (have.get(m) === f) fails.delete(m); }, /** * A forecast on `f` failed. When it is dead, or MAX_FAILS in a row failed, drop it so the next get() reloads; * a webgpu forecaster is replaced right away by a wasm one. Returns the replacement load, if any. */ fail(m, f) { if (have.get(m) !== f) return null; const n = (fails.get(m) ?? 0) + 1; fails.set(m, n); if (!f.dead && n < MAX_FAILS) return null; have.delete(m); fails.delete(m); f.close(); return f.backend === "webgpu" ? begin(m, "wasm") : null; }, has: (m) => have.has(m) }; } // src/game/pause.ts function createPause(sampler) { let lastY = null; let paused = false; return { /** Record the pen height whenever a sample is pushed. */ note(y) { lastY = y; }, hide() { sampler.reset(); paused = true; }, show(nowMs) { if (lastY !== null) sampler.push(nowMs, lastY); }, /** Call with the number of samples the latest drain produced; samples flowing ends the pause. */ flowed(n) { if (n > 0) paused = false; }, get paused() { return paused; } }; } // src/ui/copy.ts function unscoredLine(hard, p50Ms, unscored, arrived, issued) { if (p50Ms > 0 && arrived === issued && unscored <= 2 * issued) { return `${hard ? "Hard" : "Normal"}'s model takes ~${Math.round(p50Ms)} ms here, so the first sample or two of each second isn't scored (${unscored} in total).`; } const what = unscored === 1 ? "1 sample wasn't" : `${unscored} samples weren't`; const their = unscored === 1 ? "its forecast" : "their forecasts"; return `${what} scored because ${their} ${arrived < issued ? "never arrived" : "arrived late"}.`; } // src/app.ts var app = document.getElementById("app"); var h = (tag, props = {}, ...kids) => { const el = Object.assign(document.createElement(tag), props); el.append(...kids); return el; }; var pct2 = (x) => `${Math.round(x * 100)}%`; var START_LINE = "Timestral sees your last 16 seconds and bets on your next one. Escape its bet."; var mode = "normal"; var cache = createForecasterCache( (m, backend, hooks) => createForecaster(MODELS[m].file, backend ?? MODELS[m].backend, hooks.onProgress, { expectedBytes: MODELS[m].bytes, onStage: hooks.onStage }) ); var generation = 0; var cancelLoop = null; function start() { cancelLoop?.(); const g = ++generation; const m0 = mode; const bar = h("progress", { max: 1, value: 0 }); const status = h("p", { className: "muted" }, "Loading model\u2026"); const toggle = h("div", { className: "row" }); for (const m of ["normal", "hard"]) { const b = h("button", { className: m === mode ? "primary" : "" }, `${m === "normal" ? "Normal" : "Hard"} \xB7 ${MODELS[m].name} \xB7 ${Math.round(MODELS[m].bytes / 1e6)} MB`); b.onclick = () => { if (m === mode) return; mode = m; start(); }; toggle.append(b); } app.replaceChildren( h("h1", {}, "Can you outrun a forecast?"), h("p", {}, START_LINE), toggle, bar, status ); cache.get(m0, { onProgress: (l, t) => (bar.max = t, bar.value = l), onStage: () => (bar.removeAttribute("value"), status.textContent = "Starting the model\u2026") }).then( (f) => { if (g !== generation) return; bar.remove(); status.textContent = "Move to start"; play(m0, f); }, (err) => { if (g !== generation) return; status.textContent = `The model didn't load (${err instanceof Error ? err.message : String(err)}).`; const retry = h("button", {}, "Retry"); retry.onclick = () => start(); app.append(retry); } ); } function play(m, first) { let forecaster = first; cancelLoop?.(); generation++; const canvas = h("canvas", { className: "play" }); const info = h("div", { className: "bar" }); const hint = h("p", { className: "muted" }, "Move to start"); app.replaceChildren(h("h1", {}, "Unpredictable"), h("p", { className: "muted" }, START_LINE), info, canvas, hint); const ctx = canvas.getContext("2d"); let view = makeView(1, 1); const resize = () => { const dpr = devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = Math.round(rect.width * dpr); canvas.height = Math.round(rect.height * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); view = makeView(rect.width, rect.height); }; resize(); const observer = new ResizeObserver(resize); observer.observe(canvas); const colors = readColors(document.documentElement); const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; const sampler = createSampler(); const round = createRound(); const escapes = /* @__PURE__ */ new Set(); const latencies = []; let p50 = 0; let raf = 0, stopped = false; let failing = false; let started = false, keyY = 0.5, keyDir = 0, lastFrame = 0, flashUntil = 0; const pointerY = (e) => { const r = canvas.getBoundingClientRect(); return pointerToValue(e.clientY, r.top, r.height, view); }; canvas.onpointermove = (e) => { started = true; const y = pointerY(e); pause.note(y); sampler.push(performance.now(), y); }; canvas.onpointerdown = (e) => canvas.setPointerCapture(e.pointerId); const onKey = (e) => { if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return; e.preventDefault(); started = true; keyDir = e.type === "keyup" ? 0 : e.key === "ArrowUp" ? 1 : -1; }; addEventListener("keydown", onKey); addEventListener("keyup", onKey); const pause = createPause(sampler); const onVisibility = () => { if (document.hidden) { keyDir = 0; lastFrame = 0; pause.hide(); } else pause.show(performance.now()); }; document.addEventListener("visibilitychange", onVisibility); const cancel = () => { stopped = true; cancelAnimationFrame(raf); observer.disconnect(); removeEventListener("keydown", onKey); removeEventListener("keyup", onKey); document.removeEventListener("visibilitychange", onVisibility); canvas.onpointermove = canvas.onpointerdown = null; if (cancelLoop === cancel) cancelLoop = null; }; cancelLoop = cancel; const frame = (now) => { if (stopped) return; if (keyDir) { keyY = Math.min(1, Math.max(0, keyY + keyDir * 0.8 * ((now - (lastFrame || now)) / 1e3))); pause.note(keyY); sampler.push(now, keyY); } lastFrame = now; if (started && !document.hidden) { const ys = sampler.drain(now); pause.flowed(ys.length); for (const y of ys) { const req = round.push(y); if (req) { const f = forecaster; f.forecast(req.context).then( (r) => { cache.ok(m, f); failing = false; if (stopped) return; latencies.push(r.ms); p50 = [...latencies].sort((a, b) => a - b)[Math.floor(latencies.length / 2)]; round.deliver(req.issue, r.quantiles); }, (err) => { console.error("forecast failed", err); failing = true; const next = cache.fail(m, f); next?.then((nf) => { if (!stopped && forecaster === f) forecaster = nf, failing = false; }, () => { }); } ); } } } const res = round.result(); for (const i of res.escapes) { if (!escapes.has(i)) { escapes.add(i); flashUntil = reduced ? 0 : now + 120; } } drawPlay(ctx, round, view, colors, escapes); if (now < flashUntil) { ctx.fillStyle = colors.escape; ctx.globalAlpha = 0.08; ctx.fillRect(0, 0, view.width, view.height); ctx.globalAlpha = 1; } const left = Math.max(0, (PLAY + WARMUP - round.samples.length) / SAMPLE_HZ); info.textContent = `${left.toFixed(1)} s left \xB7 caught ${res.caughtPct === null ? "\u2014" : pct2(res.caughtPct)} \xB7 ${p50.toFixed(1)} ms/forecast (${forecaster.backend})${failing ? " \xB7 Forecasts are failing on this device." : ""}`; hint.textContent = !started ? "Move to start" : pause.paused ? "Paused, move to resume" : ""; if (round.done) { cancel(); return result(round, escapes, p50, m); } raf = requestAnimationFrame(frame); }; raf = requestAnimationFrame(frame); } function result(round, escapes, p50, m) { const res = round.result(); const strip = h("canvas", { className: "strip" }); const head = res.caughtPct === null ? "No forecasts arrived, so this round has no score." : `Timestral caught you ${pct2(res.caughtPct)} of the time`; const again = h("button", { className: "primary" }, "Play again"); again.onclick = () => (mode = m, start()); const share = h("button", {}, "Share card"); const card = prepareCard(round, p50, MODELS[m].name); let ready = null; card.blob.then((b) => ready = b, () => { }); share.onclick = () => runShare(share, () => deliverCard(ready ?? card.blob, card.text)); const buttons = h("div", { className: "row" }, share, again); if (m === "normal") { const hard = h("button", {}, "Try Hard mode"); hard.onclick = () => (mode = "hard", start()); buttons.append(hard); } app.replaceChildren( h("p", { className: "headline" }, head), ...REFERENCE !== null ? [h("p", { className: "muted" }, `A random scribble: ${pct2(REFERENCE)}`)] : [], ...res.unscored ? [h("p", { className: "muted" }, unscoredLine(m === "hard", p50, res.unscored, round.bands.size, PLAY / PATCH))] : [], strip, buttons ); const dpr = devicePixelRatio || 1; const r = strip.getBoundingClientRect(); strip.width = r.width * dpr; strip.height = r.height * dpr; const sctx = strip.getContext("2d"); sctx.scale(dpr, dpr); drawStrip(sctx, round, r.width, r.height, readColors(document.documentElement), escapes); } start(); //# sourceMappingURL=app.js.map