Files
workbuddy_skills/browser-harness/src/browser_harness/video-template.html
T
admin 777f7fe5d0 重开仓库内容:改推五个技能(browser-harness / humanizer / humanizer-zh / product-planning / session-mechanism)
按授权清空原有内容后重新提交(原 oil-ui-pro 一并移出,可从历史恢复)。
browser-harness 剔除 .venv 等运行环境;根 .gitignore 补记 .venv/ 与 node_modules/。
2026-10-07 16:06:02 +08:00

971 lines
69 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!-- browser-harness video template: renders a recording as a structured,
privacy-safe explanatory video — chapter plan, semantic routes, readable
narration, cursor-led camera, immediate click results, explanation/outcome
cards — and exports webm via MediaRecorder. Everything is a pure function
of the playhead: seek(t) is exact. See make-video.md for the workflow. -->
<html>
<head>
<meta charset="utf-8">
<title>browser-harness video</title>
<style>
html, body { margin: 0; height: 100%; background: #0b0e14; overflow: hidden; }
#stage { display: block; width: 100vw; height: 100vh; object-fit: contain; }
#hud { position: fixed; left: 12px; bottom: 10px; color: #9aa; font: 12px/1.4 ui-monospace, monospace;
background: rgba(0,0,0,.55); padding: 6px 10px; border-radius: 8px; user-select: none; }
</style>
</head>
<body>
<canvas id="stage" width="1920" height="1080"></canvas>
<div id="hud"></div>
<script src="composition.js"></script>
<script>
"use strict";
/* composition.js is compiler-owned. Author edit-brief.json; do not hand-edit
renderer fields or copy this template as an alternative workflow. */
const C = window.COMPOSITION;
const VW = () => C.viewport?.w || 1392, VH = () => C.viewport?.h || 1170;
// ------------------------------ tuning ------------------------------
const W = 1920, H = 1080;
const PAD = 108; // generous breathing room around the window
const FRAME_STYLE = C.frameStyle || "native";
const TABSTRIP = FRAME_STYLE === "browser" ? 36 : 32;
const TOOLBAR = FRAME_STYLE === "browser" ? 44 : 0;
const CHROME = FRAME_STYLE === "raw" ? 0 : TABSTRIP + TOOLBAR;
const MOTION = C.motion || {};
const PACING = C.pacing || {};
const REDUCED_MOTION = matchMedia("(prefers-reduced-motion: reduce)").matches;
const PUNCH_S = REDUCED_MOTION ? 0 : (MOTION.zoomDuration ?? 0.46);
const DRIFT_MAX = REDUCED_MOTION ? 0 : (MOTION.panDuration ?? 0.58);
const CUR_S = REDUCED_MOTION ? 0 : (MOTION.cursorDuration ?? 0.52);
const AUTO_ZOOM = MOTION.autoZoom ?? 1.7;
const WIDE_SCALE = MOTION.wideScale ?? 0.8;
const CLICK_SAFE = {
left: 140, right: 140, top: 150, bottom: 190,
...(MOTION.clickSafeInset || {}),
};
const REACTION_LAG = REDUCED_MOTION ? 0 : (MOTION.reactionLag ?? 0.025);
const REACTION_FADE = REDUCED_MOTION ? 0.001 : (MOTION.reactionFade ?? 0.04);
const RADIUS = 12;
const RAIL = H - 142; // readable narration above the action/time rail
const STAGE = 0.94; // camera output inset — backdrop margin at any zoom
const ACCENT = "#6ee7ff";
const MONO = "ui-monospace, 'SF Mono', Menlo, monospace";
const SANS = "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
const SERIF = "'Tiempos Headline', 'Tiempos Text', 'Iowan Old Style', Georgia, serif";
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
const easeOutQuart = t => 1 - Math.pow(1 - t, 4);
const easeOutQuint = t => 1 - Math.pow(1 - t, 5);
const easeInOut = t => t < .5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const lerp = (a, b, t) => a + (b - a) * t;
// ------------------------------ timeline ------------------------------
function cardText(b) {
const details = b.kind === "intro" ? (C.plan || [])
: b.kind === "explanation" ? (b.points || []).flatMap(p => [p.label, p.text])
: b.kind === "outcome" ? (b.outcomes || [])
: [];
return [b.title, b.sub, ...details].filter(Boolean).join(" ");
}
const wordCount = text => text ? String(text).trim().split(/\s+/).length : 0;
const cardReadSeconds = b => 0.4 + wordCount(cardText(b)) * 60 / (C.readingWpm || 380);
const cardBaseSeconds = b => b.kind === "intro" || b.kind === "outcome" ? 4.5
: b.kind === "explanation" ? 4 : 3.5;
const cardTargetSeconds = b => Math.max(cardBaseSeconds(b), cardReadSeconds(b));
function defaultDur(b) {
const text = b.card ? cardText(b) : (b.narration || b.caption);
const words = text ? text.trim().split(/\s+/).length : 0;
let base = b.card ? 3.5 : 0.7;
if (b.kind === "intro" || b.kind === "outcome") base = 4.5;
if (b.kind === "explanation") base = 4;
if (b.click) base = 1.15;
if (b.after) base = Math.max(base, 1.4); // let the consequence land
if (b.type) base = Math.max(base, 0.6 + b.type.text.length * 0.035);
if (b.card) return cardTargetSeconds(b);
const captionBase = PACING.captionBaseSeconds ?? 0.35;
const secondsPerWord = PACING.captionSecondsPerWord ?? 0.2;
return Math.max(base, words ? captionBase + secondsPerWord * words : base);
}
const beats = C.beats;
beats.forEach(b => { if (b.dur == null) b.dur = defaultDur(b); });
const starts = []; let T = 0;
for (const b of beats) { starts.push(T); T += b.dur; }
const DURATION = T;
const beatAt = t => { let i = beats.length - 1; while (i > 0 && t < starts[i]) i--; return i; };
const firstWindowBeat = beats.findIndex(b => !b.card);
beats.forEach(b => {
if (b.label === undefined) {
if (b.click && b.cursor) b.label = `click(${Math.round(b.cursor.x)}, ${Math.round(b.cursor.y)})`;
else if (b.type) b.label = `type("${b.type.text}")`;
}
});
const stepOf = []; let stepCount = 0;
beats.forEach((b, i) => { if (b.label) stepCount++; stepOf[i] = stepCount; });
// ------------------------------ preflight ------------------------------
const usedFrames = [...new Set(beats.flatMap(b => [b.frame, b.after]).filter(Boolean))];
const reviewedFrames = new Set(C.privacy?.reviewedFrames || []);
const preflightErrors = [];
const preflightWarnings = [];
const unsafeRoute = /@|[?#]|:\/\/|onmicrosoft|(?:tenant|user|object)[_-]?id|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
const opaqueHex = /^#[0-9a-f]{6}$/i;
if (C.schemaVersion !== 1)
preflightErrors.push("composition must use schema version 1");
if (C.frameStyle !== "native" || C.readingWpm !== 380)
preflightErrors.push("composition changed generated typography or frame style");
if (!Number.isFinite(C.durationBudget) || DURATION > C.durationBudget + 0.001)
preflightErrors.push(
`video is ${DURATION.toFixed(1)}s; shorten it to the ${Number(C.durationBudget).toFixed(1)}s budget`,
);
if (!Array.isArray(C.plan) || C.plan.length < 2 || C.plan.length > 5)
preflightErrors.push("plan must contain 2–5 steps");
if (!beats[0]?.card || beats[0]?.kind !== "intro" || beats[0]?.dur < 4)
preflightErrors.push("first beat must be an intro card lasting at least 4 seconds");
if (!beats.at(-1)?.card || beats.at(-1)?.kind !== "outcome" || !beats.at(-1)?.outcomes?.length)
preflightErrors.push("last beat must be an outcome card with achieved outcomes");
for (const frame of usedFrames)
if (!reviewedFrames.has(frame)) preflightErrors.push(`privacy review missing: ${frame}`);
for (const [i, b] of beats.entries()) {
if (b.card && b.dur + 0.001 < cardTargetSeconds(b))
preflightErrors.push(`beat ${i + 1} card is shorter than its structured scan target`);
if (b.kind === "explanation") {
const labels = (b.points || []).map(point => String(point.label || "").toLowerCase());
if (labels.join("|") !== "observed|mistake|correction")
preflightErrors.push(`beat ${i + 1} explanation must be Observed, Mistake, Correction`);
}
}
if (FRAME_STYLE === "browser" && C.authenticity?.allowSyntheticChrome !== true)
preflightErrors.push("synthetic browser chrome requires authenticity.allowSyntheticChrome=true");
for (const frame of usedFrames) {
for (const [i, rect] of (C.redact?.[frame] || []).entries()) {
const mask = C.privacy?.mask || {};
const fill = rect.fill ?? mask.fill ?? "#f2f4f7";
const stroke = rect.stroke ?? mask.stroke ?? "#e2e7ec";
if (!opaqueHex.test(fill))
preflightErrors.push(`${frame} mask ${i + 1} fill must be opaque six-digit hex`);
if (stroke && !opaqueHex.test(stroke))
preflightErrors.push(`${frame} mask ${i + 1} stroke must be opaque six-digit hex`);
}
}
for (let i = 0; i < beats.length; i++) {
const b = beats[i];
const text = b.narration || b.caption;
const wc = text ? String(text).trim().split(/\s+/).length : 0;
if (wc > 7) preflightErrors.push(`beat ${i + 1} narration exceeds 7 words`);
if (!b.card && (!Number.isInteger(b.chapter) || b.chapter < 0 || b.chapter >= (C.plan || []).length))
preflightErrors.push(`beat ${i + 1} has no valid chapter`);
if (!b.card && !b.route) preflightErrors.push(`beat ${i + 1} has no semantic route`);
if (b.url) preflightErrors.push(`beat ${i + 1} uses a raw url; use route`);
if (unsafeRoute.test(String(b.route || "")) || unsafeRoute.test(String(b.afterRoute || "")))
preflightErrors.push(`beat ${i + 1} route contains sensitive/raw URL material`);
}
window.videoPreflight = () => ({
errors: [...preflightErrors], warnings: [...preflightWarnings], frames: [...usedFrames],
});
// ------------------------------ assets ------------------------------
const images = new Map();
let ready = false;
const srcs = new Set();
beats.forEach(b => { if (b.frame) srcs.add(b.frame); if (b.after) srcs.add(b.after); });
Promise.all([...srcs].map(src => new Promise(res => {
const im = new Image();
im.onload = () => { images.set(src, im); res(); };
im.onerror = () => res();
im.src = src;
}))).then(() => { ready = true; draw(); });
window.videoReady = () => ready;
// browser-use logo as Path2D (1000×1000 viewBox), injected at build time.
const LOGO_D = "M585.941 27.9374C672.468 -1.01696 755.454 -8.30297 825.373 10.0229L826.395 10.2948L827 10.4392L827.641 10.6284C847.195 15.931 865.182 23.1177 881.555 32.0311L882.227 32.3732L882.764 32.6944C906.114 45.592 926.136 62.0317 942.682 81.5504L943.373 82.306L944.245 83.4133C946.291 85.8804 948.277 88.3966 950.214 90.9591L950.305 91.0486L950.355 91.109L950.764 91.6906C955.445 97.9479 959.8 104.485 963.827 111.286L964.182 111.755H964.105C965.309 113.795 966.482 115.86 967.623 117.947L967.736 118.16C967.754 118.194 967.773 118.228 967.791 118.261L968.136 118.852C968.418 119.369 968.673 119.888 968.95 120.408L969.173 120.816C970.409 123.152 971.6 125.517 972.764 127.909L973.191 128.769L973.268 128.959C974.382 131.276 975.459 133.619 976.5 135.988L976.668 136.32L976.841 136.761C979.995 143.992 982.832 151.46 985.345 159.154C985.564 159.824 985.782 160.496 985.995 161.169L986.032 161.204L986.064 161.32L986.186 161.773C988.514 169.161 990.55 176.753 992.291 184.537L992.386 184.823L992.404 184.879L993.014 187.773L993.091 188.222C993.645 190.862 994.164 193.524 994.65 196.208L994.745 196.627H994.841V197.246C996.318 205.56 997.473 214.078 998.3 222.786L998.309 222.849C999.132 231.502 999.627 240.388 999.8 249.407L999.818 249.586L999.823 249.643L999.864 250.083L999.823 250.618C999.882 254.587 999.886 258.592 999.823 262.628V263.4L999.818 263.827C999.645 272.954 999.132 282.249 998.314 291.701V291.914C997.127 305.459 995.259 319.327 992.723 333.483L992.414 335.331L992 337.438C986.227 368.283 977.241 400.477 964.936 433.68L964.459 435.07L964.132 436.006L963.7 435.256C963.627 435.192 963.577 435.106 963.555 435.011L954.727 419.751L945.673 404.357L936.286 389.309L936.041 388.924L926.414 374.112L926.35 374.017L916.232 358.915L910.195 350.317L905.823 343.979L905.691 343.8L894.95 329.184L888.609 320.729L883.923 314.384L883.809 314.228L883.841 314.052C883.841 314.047 883.841 314.057 883.841 314.052L885.195 306.069V305.778L886.255 298.195L887.091 291.914L887.168 291.374L887.209 291.159L887.314 290.632L888.064 283.111L888.518 275.73L888.968 268.646L889.118 261.552L889.123 261.489V261.107L889.273 254.779L889.132 250.083L889.123 249.675V248.142L888.818 241.65L888.4 236.643L888.364 236.23V235.466L887.764 229.309L887.014 223.577L886.109 217.852L885.327 213.031L885.205 212.282L884.609 210.191L883.995 206.979L882.795 202.035L882.777 201.975L881.536 197.835H881.423V197.452L881.418 197.382V197.148L879.932 192.541L878.423 188.011L877.227 185.173L876.768 183.805L874.968 179.752L873.186 175.897L873.159 175.839L873.118 175.758L871.205 172.075L869.25 168.471L867.15 165.018L864.891 161.703L862.636 158.547L857.964 152.519L852.845 147.102L849.091 143.651L845.195 140.201L840.977 136.887L837.068 134.192L836.623 133.883L833.164 131.651L831.954 130.872L829.104 129.372L826.977 128.159L821.718 125.455L816.164 123.053L810.286 120.792L805.627 119.44L803.977 118.838L797.332 117.026L790.409 115.371L783.032 113.866L782.509 113.783L775.341 112.659L767.2 111.754L758.623 111.151L749.75 110.851H740.432L730.868 110.999L720.918 111.754L716.018 112.187L710.65 112.662L700.255 113.865L689.391 115.524L678.373 117.639L667.059 120.052L667.009 120.063L655.518 122.749L655.441 122.768L643.673 125.936L631.759 129.402L631.318 129.543L619.541 133.325L607.309 137.705L597.955 141.325L595.055 142.429L594.786 142.533L582.264 147.66L569.604 153.085L556.75 158.983L544.077 165.244L543.936 165.318L543.841 165.368L530.95 171.955L517.964 178.904L504.982 186.149L492.005 193.843L482.95 199.428L478.864 201.849L475.436 204.039L465.891 210.146L463.341 211.946L453.068 218.742L452.82 218.917L444.462 224.784L440.077 227.805L427.248 237.012L415.051 246.158L414.57 246.52L408.533 251.197L401.89 256.332L392.468 263.977L391.527 264.74L391.477 264.782L389.356 266.449L376.99 276.851L364.764 287.567L352.744 298.537L352.687 298.59L340.755 309.613L329.135 321.082L328.985 321.229L317.509 332.708L306.345 344.471L300.758 350.514L295.279 356.287L295.168 356.406L284.456 368.622L273.9 380.837L273.781 381.016L263.471 393.383L261.663 395.642L253.357 405.912L249.588 410.884L244.251 417.862L243.698 418.588L234.189 431.261L224.982 444.093L216.073 456.928L207.474 469.9L199.456 482.436L199.17 482.881L199.092 483.008L191.359 495.798L191.321 495.862L186.791 503.563L183.619 508.851L183.59 508.901L178.039 519.106L176.381 521.814L176.255 522.059L169.739 534.649L169.705 534.713L169.653 534.763L169.559 534.858L163.1 547.771L162.045 550.038L157.062 560.602L151.356 573.364L148.156 580.829L145.894 586.104L141.097 598.544L141.069 598.621L136.413 610.934L136.414 611.152L136.415 611.611H136.28L132.157 623.388L131.256 626.387L128.387 635.455L126.729 641.33L124.915 647.382L122.804 655.519L121.9 658.991L119.614 668.845L119.485 669.404L119.182 670.613L116.92 681.772L114.96 692.771L114.95 692.849L113.452 703.489L113.425 703.73L112.697 710.423L112.254 713.817L112.245 713.885L111.497 723.917L111.495 723.962L111.497 724.417L110.889 733.808L110.887 735.289L110.889 736.798V737.252L110.737 743.304L110.889 749.624V752.355L111.34 761.06L111.445 762.018L111.491 762.418L112.094 769.52L112.848 774.79L113.143 777.453L113.151 777.512L114.502 785.186L116.01 792.569L117.62 799.302L117.673 799.511L119.621 805.963L121.177 810.343L121.266 810.597L121.283 810.643L121.695 812.019L121.736 812.156L123.997 817.881L126.55 823.283L129.105 828.544L131.817 833.065L131.881 833.174V833.46L132.91 835.005L134.816 837.863L137.984 842.238L141.276 846.277L144.743 850.048L148.349 853.806L148.519 853.96L152.074 857.232L152.109 857.263L156.025 860.576L160.23 863.733L164.031 866.219L164.172 866.31L164.547 866.591L164.749 866.741L169.549 869.594L170.837 870.276L174.671 872.307L179.947 874.869L185.633 877.259L190.177 878.968L191.685 879.531L197.488 881.33L197.996 881.485L204.788 883.298L211.858 884.801L219.39 886.155L222.52 886.605L222.57 886.61L227.251 887.364L234.028 887.982L235.514 888.114L239.674 888.409L244.109 888.718L253.163 889.018L262.511 888.868L272.308 888.418L282.264 887.814L292.659 886.76L303.22 885.251L314.044 883.448L314.28 883.402L314.442 883.525L314.7 883.72L315.045 883.979L316.738 885.247L316.83 885.315L316.867 885.342L317.027 885.478L320.67 888.227L320.951 888.418L321.048 888.482L321.087 888.509L321.566 888.868H321.871V889.1L322.77 889.776H322.721L329.142 894.624L329.238 894.697L329.412 894.824L344.031 905.415L349.681 909.409L357.014 914.47L358.974 915.979L369.541 922.921L374.07 926.093L374.657 926.47L389.322 935.906L389.608 936.084L393.695 938.624L404.562 945.411L404.646 945.461L419.961 954.471L420.311 954.671L421.437 955.316L435.111 963.235C435.239 963.208 435.368 963.245 435.467 963.317H437.75L436.209 963.867L435.292 964.199L431.062 965.707L430.99 965.734L428.649 966.557C425.328 967.756 422.018 968.919 418.719 970.051L415.66 971.146L415.586 971.168L404.47 974.776L400.397 976.13L392.19 978.57H392.625L389.2 979.46L386.558 980.146C382.626 981.259 378.709 982.323 374.812 983.34L374.093 983.536L373.182 983.763C370.499 984.458 367.824 985.126 365.158 985.771L362.552 986.425H363.001L359.02 987.307L358.97 987.32L356.32 987.907L354.894 988.225L344.165 990.492L340.665 991.119C337.84 991.664 335.026 992.187 332.223 992.677L329.354 993.213L327.899 993.418C324.249 994.027 320.618 994.59 317.007 995.108L314.694 995.481L312.883 995.68C310.298 996.026 307.724 996.353 305.16 996.653L300.335 997.293L300.058 997.312L297.084 997.525C294.19 997.811 291.31 998.066 288.444 998.293L286.117 998.507L281.579 998.806L278.829 998.938C276.793 999.052 274.765 999.152 272.744 999.234L272.203 999.261H272.087L272.02 999.265C267.626 999.433 263.27 999.533 258.954 999.556L258.75 999.561L258.04 999.565C254.292 999.574 250.575 999.533 246.887 999.433L244.84 999.411L243.418 999.324C241.22 999.247 239.032 999.152 236.856 999.034L231.539 998.806L228.682 998.507L227.022 998.357C225.509 998.234 224.001 998.098 222.5 997.952L218.562 997.598L215.504 997.293L205.72 995.939L205.665 995.935L202.683 995.485L202.631 995.476L193.269 993.818L190.239 993.213L181.022 991.246L178.134 990.487L169.224 988.22L168.177 987.907L166.185 987.311L166.135 987.293L163.226 986.425H163.379L157.642 984.613L157.541 984.581L154.952 983.572C153.309 983.004 151.677 982.418 150.055 981.818L146.453 980.533L146.366 980.505L143.648 979.447L135.486 976.121L132.899 974.753L125.047 971.132L122.955 969.905C121.649 969.228 120.352 968.542 119.063 967.843L114.909 965.684L112.321 964.162L105.068 959.782L101.299 957.152C100.11 956.357 98.9291 955.552 97.7582 954.739L95.695 953.431L86.7509 946.615L86.1509 946.161L82.5714 943.126C81.9282 942.585 81.2886 942.04 80.6523 941.495L78.1264 939.355L77.025 938.31C75.4459 936.888 73.8877 935.448 72.3518 933.98L69.9577 931.79L67.7432 929.432C64.7277 926.37 61.8018 923.207 58.9686 919.95L56.0309 916.656L54.8073 915.129L48.0064 906.365L47.9605 906.288L47.8145 906.001L47.2723 905.224C45.885 903.312 44.5254 901.372 43.1946 899.404L41.636 897.173L41.5085 896.992L40.4306 895.306L40.3311 895.147L39.9891 894.611L36.9545 889.713L36.9096 889.627L36.7835 889.372L36.6658 889.136C36.5694 888.977 36.4716 888.813 36.3756 888.654L35.9285 888.095L35.8865 888.045L35.8612 887.982L35.6131 887.359C35.5528 887.259 35.4925 887.155 35.4323 887.05L35.302 886.855L34.866 886.074C34.6739 885.742 34.482 885.406 34.2914 885.074L33.6284 883.97L33.2836 883.284C32.9002 882.598 32.5184 881.907 32.1412 881.212L31.5161 880.199L30.4214 878.145L30.3317 877.977L30.2953 877.909L30.113 877.546L29.9855 877.296L29.0828 875.483L29.0604 875.437L28.7549 874.824L28.4325 874.106C27.8661 872.97 27.3079 871.83 26.7575 870.685L26.6384 870.444L26.5402 870.249L26.5136 870.194L26.1856 869.54L25.1624 867.345L24.8807 866.923L24.8499 866.873L24.414 866.219H24.5807L23.7846 864.283L22.8006 861.989C22.6616 861.666 22.5208 861.344 22.383 861.021L20.9112 857.781L20.3085 856.123L19.6708 854.587L19.5446 854.292L19.2937 853.683L18.1864 850.516L18.0224 850.057C17.5904 848.894 17.1642 847.722 16.7469 846.545L16.0811 844.951L15.313 842.652L13.9506 838.267L12.4453 833.742L12.4214 833.674V833.356L11.9519 831.597C11.3994 829.685 10.8639 827.753 10.3484 825.809L9.72045 823.605L9.41486 822.383L9.10091 820.916C8.4085 818.103 7.75659 815.263 7.14559 812.406L6.70127 810.924L6.68868 810.87L6.17286 807.676C5.64627 805 5.15518 802.305 4.69832 799.593L4.58059 799.125L4.44043 798.566H4.4979L4.28204 797.053C3.93671 794.895 3.61244 792.728 3.3107 790.547L2.76125 787.158L2.60567 785.317L2.49774 784.223C1.94323 779.534 1.48815 774.799 1.12973 770.02L1.09469 769.597L1.07366 769.265C0.922441 767.189 0.7887 765.104 0.674195 763.014L0.641955 762.5L0.640555 762.364C0.421359 758.22 0.275339 754.045 0.199034 749.837L0.187821 749.66V749.31C-0.0836691 733.068 0.703895 716.397 2.51596 699.387L2.75705 696.742L2.9785 695.243C3.42475 691.39 3.92292 687.524 4.47266 683.639L4.47967 683.58C5.10395 679.173 5.79636 674.747 6.55273 670.304L6.68586 669.241L7.57173 664.529C7.9145 662.648 8.27095 660.763 8.63695 658.877L8.65236 658.791C10.7446 648.014 13.2175 637.118 16.0573 626.155C19.8754 611.416 24.366 596.495 29.4991 581.506L29.6253 581.106L29.8159 580.497L29.9098 580.202L30.364 578.998C33.6331 569.579 37.1571 560.115 40.931 550.629L41.2968 549.684L41.3753 549.511C42.0896 547.721 42.8116 545.931 43.5437 544.145C85.0132 442.803 154.799 338.709 246.82 246.698L251.142 242.401C252.682 240.878 254.228 239.363 255.775 237.853H255.679L256.35 237.292C306.627 188.292 360.299 145.873 415.01 111.001H413.892L416.209 110.236C421.134 107.108 426.069 104.043 431.009 101.037L431.209 100.916C436.304 97.8179 441.406 94.7874 446.512 91.8223L446.557 91.7955C479.1 72.9033 511.845 56.7112 544.309 43.427C558.245 37.724 572.141 32.5545 585.941 27.9374ZM672.236 174.276C672.377 174.134 672.591 174.099 672.764 174.189L672.841 174.24L677.227 177.805C862.445 329.25 988.982 541.987 999.318 720.046L999.541 724.24C1000.69 748.261 999.668 771.015 996.6 792.373L996.564 792.614C994.818 804.709 992.414 816.354 989.377 827.526L989.523 827.558L989.227 828.153L989.2 828.217L989.168 828.276C983.85 847.613 976.627 865.537 967.609 881.93L967.464 882.257L967.441 882.303L967.414 882.343L967.286 882.507C966.327 884.243 965.345 885.965 964.345 887.664L963.982 888.318L963.877 888.573L963.709 888.772L963.65 888.868H963.9L963.2 889.581C961.932 891.694 960.627 893.775 959.295 895.828L958.673 896.814L955.345 901.808L954.291 903.212C952.882 905.197 951.445 907.155 949.977 909.082L948.691 910.881L946.191 913.925C944.986 915.42 943.759 916.892 942.514 918.35L941.423 919.664L940.818 920.318C938.941 922.462 937.027 924.566 935.068 926.633L933.859 927.983L930.059 931.781L925.832 935.861L923.805 937.656C920.555 940.609 917.209 943.467 913.768 946.225L913.118 946.756L908.582 950.387L904.259 953.412C901.668 955.262 899.027 957.056 896.336 958.801L894.659 959.919L889.668 963.095L884.814 965.83L882.668 966.988C881.709 967.52 880.741 968.047 879.768 968.565L879.518 968.701L879.368 968.779C877.523 969.755 875.659 970.714 873.777 971.65L869.086 973.995L863.932 976.266L858.341 978.688L852.886 980.805L852.341 981.005C850.654 981.65 848.955 982.273 847.245 982.881L847.141 982.922L847.041 982.954C843.75 984.126 840.409 985.235 837.027 986.285L835.805 986.703L833.459 987.361C831.427 987.961 829.382 988.538 827.318 989.092L824 990.033L821.455 990.601C819.295 991.132 817.118 991.646 814.923 992.128L811.75 992.904L809.732 993.25L808.95 993.381C806.268 993.918 803.559 994.417 800.827 994.885L799.345 995.176L799.295 995.185L797.277 995.471C794.173 995.962 791.036 996.412 787.868 996.816L786.636 996.989L786.586 996.994L784.418 997.234C783.345 997.362 782.268 997.48 781.186 997.598L779.827 997.748L773.627 998.352L771.559 998.479C769.023 998.679 766.473 998.847 763.905 998.988L760.327 999.261H760.223L756.286 999.329C753.618 999.42 750.936 999.483 748.236 999.515L746.718 999.561L744.927 999.542C741.364 999.556 737.768 999.511 734.141 999.424L732.955 999.411L727.859 999.261H727.805L718.945 998.811H718.9L714.877 998.497C713.668 998.411 712.459 998.32 711.245 998.225L704.686 997.748L700.114 997.193C697.732 996.939 695.35 996.671 692.964 996.38L690.323 996.085L686.6 995.567C686.023 995.49 685.445 995.403 684.868 995.326L683.818 995.181L683.523 995.135C681.5 994.849 679.477 994.549 677.445 994.236L675.705 993.977L675.65 993.972L669.005 992.914L660.841 991.401L657.286 990.674C655.359 990.292 653.423 989.901 651.486 989.497L645.882 988.379L641.277 987.252C638.327 986.575 635.368 985.871 632.409 985.14L630.764 984.749L628.25 984.095C626.482 983.64 624.709 983.181 622.936 982.709L615.65 980.823L608.541 978.706L600.236 976.289L593.195 974.044L593.123 974.022L592.886 973.945L590.359 973.136H590.418L584.818 971.3L581.086 969.951C578.791 969.16 576.491 968.356 574.195 967.538L569.255 965.862L562.145 963.136L553.686 959.969L547.341 957.324C546.409 956.947 545.477 956.57 544.545 956.193L538.118 953.622L530.864 950.446L522.405 946.82L517.809 944.621C515.895 943.749 513.986 942.863 512.073 941.972L506.691 939.569L499.423 935.938L490.973 931.863L483.709 928.083L475.255 923.703L468 919.773L459.791 915.297L459.682 915.238L459.623 915.202L458.132 914.393H458.2L452.388 911.067L452.279 911.008L452.179 910.949L450.805 910.163L443.974 906.174L436.83 902.008L436.718 901.944L436.64 901.899L435.278 901.103L428.406 896.955L421.269 892.494L421.154 892.425L420.767 892.18L419.82 891.589L417.078 889.858L417.036 889.831L416.999 889.795L416.89 889.686L416.708 889.577L416.18 889.259L415.08 888.573L414.961 888.5L413.3 887.441L413.066 887.282L412.838 887.132L411.025 885.924L408.465 884.265L408.343 884.188L407.18 883.434L406.405 882.893C405.799 882.498 405.194 882.098 404.588 881.703L401.52 879.74L397.435 877.014L397.242 876.832L395.24 875.478C394.087 874.701 392.935 873.924 391.784 873.138L387.579 870.294L387.461 870.217L387.295 870.103L386.241 869.39L382.175 866.591L381.842 866.369L381.714 866.282L380.484 865.464H380.583L377.07 862.915C374.182 860.871 371.299 858.804 368.422 856.718L366.927 855.723L362.227 852.234L354.975 846.8L351.813 844.537L347.415 841.052L339.568 835.014L338.225 833.91C336.105 832.247 333.986 830.57 331.874 828.885L331.408 828.517L322.947 821.72L322.192 821.115L321.185 820.311L314.333 814.618L307.535 808.875L305.571 807.217L301.122 803.296C299.326 801.742 297.535 800.179 295.747 798.607L293.189 796.499L288.237 791.946C287.114 790.942 285.992 789.934 284.873 788.925L278.979 783.65L278.206 782.878L275.475 780.324C270.6 775.808 265.757 771.237 260.952 766.612L259.639 765.372L257.935 763.695C252.43 758.352 246.974 752.936 241.569 747.448L237.725 743.604L231.08 736.661L224.721 730.005L224.584 729.864L221.565 726.693L212.055 716.279L211.604 715.825L207.254 710.886C204.962 708.324 202.683 705.748 200.415 703.158L198.901 701.463L193.309 694.966L192.798 694.362C191.123 692.408 189.456 690.441 187.795 688.473L186.509 686.956L185.875 686.211L183.943 683.934L174.863 672.921L174.699 672.721L174.395 672.267L174.281 672.094L174.335 671.899L176.147 665.102L177.205 660.872L177.96 658.15L180.078 651.049L182.303 644.074L182.346 643.938L182.398 643.774L184.268 638.036L184.461 637.441L184.726 636.646L184.763 636.537L187.179 629.285L189.901 621.725L192.917 614.187L193.062 613.615L193.081 613.551L195.95 606.604L198.063 601.325L199.124 598.899L202.447 591.193L203.353 589.085L206.078 583.328L206.103 583.273L208.807 577.331L209.102 576.672L209.506 575.868L209.706 575.468L209.833 575.218L215.327 564.227L217.56 559.761L221.794 551.746L222.138 551.097L222.562 551.696L222.599 551.746L231.43 564.173L231.47 564.227L240.525 576.599L245.659 583.246L249.906 588.807L250.04 588.985L259.698 601.207L269.664 613.438L279.772 625.505L285.82 632.343L289.435 636.378L290.2 637.291L300.759 649.059L311.625 660.676L315.7 664.902L322.345 671.699L322.398 671.762L322.675 672.18L333.963 683.316L337.133 686.338L345.437 694.339L345.53 694.425L356.974 704.975L357.06 705.052L368.836 715.62L369.887 716.416H369.745L380.755 726.03L392.711 735.893L392.834 735.993L405.067 745.807L417.319 755.221L417.452 755.321L417.538 755.39L429.809 764.667L437.21 769.802L442.343 773.577L454.877 782.182L467.409 790.638L480.077 798.63L492.759 806.481L495.768 808.135L497.309 808.984H497.195L505.436 813.873L517.068 820.366L518.127 820.975L530.791 827.758L539.4 832.138L543.273 834.224L543.332 834.255L555.85 840.284L568.223 846.023L580.595 851.452L580.732 851.507L592.941 856.418L605.055 861.112L607.464 861.989L617.114 865.483L629.032 869.408L640.655 872.879L652.123 876.046L652.195 876.064L652.727 876.201L663.432 878.913L674.609 881.33L685.423 883.434L696.023 885.097L704.782 886.305L706.455 886.61L716.382 887.664L726.2 888.418L726.582 888.436L735.7 888.872L744.814 889.022H744.882L753.795 888.868H753.855L762.218 888.418L770.505 887.664L778.195 886.61L782.382 885.928L785.573 885.406L792.659 883.902L799.3 882.239L805.764 880.285L810.473 878.713L811.645 878.322L817.35 876.073L822.786 873.656L827.886 871.107L832.705 868.395L837.209 865.546L839.477 863.924L841.436 862.529L842.141 861.961L842.673 861.534H842.605L843.691 860.721L845.477 859.385L849.241 856.068L852.859 852.606L856.323 848.994L859.632 845.232L862.786 841.175L865.795 836.959L868.641 832.465L871.359 827.785L873.905 822.697L876.164 817.426L878.423 811.701L880.536 805.663L882.341 799.348L884.145 792.732L884.391 791.465L885.505 785.64L886.259 781.124L886.859 778.102L887.764 770.274L887.768 770.229L888.518 762.137L889.118 753.699L889.273 744.962L889.123 735.757L888.818 726.266L888.1 717.32L888.064 716.902V716.597L887.75 713.581L887.009 706.506L887 706.447L886.559 703.494L885.659 696.306L885.65 696.247L885.627 696.102L883.841 685.543L881.732 674.679L879.323 663.689L878.868 662.017L876.45 652.348L876.395 652.144L873.305 640.821L873.277 640.726L869.814 629.126L866.491 619.153L865.891 617.204L861.623 605.573L861.505 605.259L856.982 593.051L855.332 589.289L852.023 580.879L852 580.82L846.573 568.457L840.841 555.931L835.073 544.109H835.023L834.923 543.795L834.782 543.373L828.473 530.901L827.114 528.634L821.673 518.206L818.805 513.227L814.577 505.53L814.268 505.012L807.027 492.999L806.882 492.763L800.818 482.949L800.686 482.731L800.259 482.04L799.332 480.323L791.205 467.678L791.086 467.56L791.032 467.492L782.727 455.116L774.127 442.589L770.05 437.005L765.118 430.126L765.064 430.054L758.877 421.756L755.859 417.686L746.2 405.306L736.518 393.244L736.468 393.186L736.386 393.08L726.427 381.006L726.395 380.972L722.941 377.087L722.795 376.926L722.75 376.87L722.136 376.181L716.209 369.141L716.154 369.076L716.105 369.019L705.595 357.309L696.532 347.494L694.859 345.516L683.854 334.06L682.805 333.008L672.686 322.744L661.427 311.783L661.364 311.723L653.814 304.474L649.895 300.709L638.7 290.709L638.555 290.576L638.104 290.129L626.191 279.572L626.127 279.517L614.273 269.463L614.114 269.333L602.073 259.526L602.036 259.497L589.818 249.842L577.432 240.479L577.309 240.385L565.05 231.268L554.345 223.879L552.568 222.547L552.527 222.519L551.927 222.098L552.573 221.748L560.427 217.519L568.441 213.438L572.768 211.274H572.423L574.495 210.409L576.309 209.654L584.159 205.882L591.868 202.405L599.573 198.931L607.25 195.769L614.836 192.583L622.245 189.712L627.259 187.868H626.877L629.623 186.998L637.064 184.419L644.323 182.15L651.423 179.885L658.536 177.918L665.482 175.954L671.986 174.291L672.041 174.276H672.236ZM778.214 551.751L782.445 559.752L786.523 567.762L790.305 575.622L793.895 583.41L793.991 583.501L794.773 584.273H794.377L797.559 591.184L800.886 598.899L804.204 606.6L807.118 614.028H807.164L807.241 614.337L810.104 621.716L812.827 629.276L812.868 629.408L815.395 636.682L817.814 643.934L818.268 645.292L820.027 650.867L820.077 651.03L822.045 658.146L822.65 660.104L824.014 665.102L825.604 671.767C825.745 671.894 825.786 672.085 825.723 672.253L825.705 672.298C825.691 672.326 825.673 672.358 825.655 672.38C825.564 672.489 825.477 672.603 825.386 672.712L823.477 675.161L818.891 680.649C810.073 691.327 801.014 701.84 791.736 712.186L790.541 713.54L790.15 713.944C782.886 722.004 775.491 729.955 767.968 737.788L765.909 739.978L764.318 741.568C748.055 758.338 731.209 774.572 713.868 790.184L713.845 790.238L713.777 790.297L711.6 792.224C708.827 794.704 706.041 797.171 703.241 799.62L702 800.711L701.209 801.433H701.345L700.341 802.219L700.214 802.337L699.445 802.923C696.495 805.486 693.527 808.026 690.545 810.552L688.1 812.651L687.295 813.296C682.491 817.326 677.655 821.315 672.786 825.25L672.705 825.327L672.523 825.491L672.282 825.427L672.082 825.373L665.536 823.633L665.491 823.619L658.541 821.806L651.436 819.839L644.323 817.572L639.486 815.959L637.073 815.154L629.673 812.737L622.255 810.011L614.7 807.14L607.218 804L607.136 803.968L603.141 802.337L603.055 802.305L599.605 800.802L591.964 797.508L591.873 797.471L584.014 793.841L583.955 793.814L576.7 790.411L576.582 790.356L576.323 790.184L576.123 790.047L568.3 786.29L560.432 782.205L552.577 778.125L552.336 778.003V777.73V777.575L552.332 777.344L552.523 777.207L567.232 766.703L568.1 766.08L568.382 765.88L583.782 754.44L584.232 754.104L584.273 754.077L599.923 741.886L615.473 729.201L630.723 716.07L645.809 702.644L649.282 699.323L660.459 688.75L662.85 686.36L663.527 685.679L663.623 685.588L663.755 685.456L674.5 675.002L674.95 674.561L683.568 665.669L684.309 664.906L684.436 664.765L689.141 659.922L690.945 657.964L697.436 650.871L702.882 645.124L716.305 630.044L717.059 629.14L723.559 621.584L729.323 614.932L729.886 614.196L730.014 614.028L736.082 606.495L742.123 599.244L742.4 598.826L748.464 591.097L753.927 584.091L754.35 583.546L766.118 567.703L777.445 551.696L777.873 551.097L778.214 551.751ZM183.482 7.89257C239.685 -4.93716 305.612 -2.11512 376.84 16.7321L378.263 17.0727H378.123C395.74 21.7776 413.68 27.4585 431.875 34.1265L435.455 35.4496L435.536 35.4905C435.607 35.5345 435.661 35.5982 435.697 35.6695L436.216 35.8576L435.599 36.2065C435.549 36.252 435.49 36.2851 435.425 36.3047L435.362 36.3424L422.562 43.6192L419.887 45.1426L404.564 54.3046L395.865 59.5927L384.335 66.8327L378.447 70.6102L372.246 74.5948L365.895 78.8512L359.728 82.9871L348.285 90.9659L348.169 91.0854L348.11 91.134L339.571 97.1677L337.987 98.3159L337.662 98.5499L329.842 104.215L328.334 105.325L325.616 107.378H325.679L324.577 108.177L324.453 108.283L323.979 108.613L321.416 110.548V110.676L321.169 110.8L320.913 110.927L320.228 111.445L319.652 111.908H319.891L318.672 112.721L318.56 112.815L318.308 112.965L317.524 113.487L315.042 115.361L314.47 115.87L314.313 116.009L314.243 115.999L314.07 116.086L313.947 116.149L313.811 116.131L313.11 116.044L312.602 115.981L305.806 114.774L301.949 114.243L298.107 113.716L290.405 112.812L282.89 112.059L275.654 111.457L272.114 111.234L268.38 111.001L261.45 110.85H257.522L254.681 110.7L253.355 110.847L248.04 110.85L241.558 111.149L235.214 111.606L229.192 112.206L223.322 112.958L217.736 113.865L212.326 114.766L207.645 115.834L207.042 115.972L202.065 117.18L197.334 118.509L197.249 118.533L194.871 119.224L192.57 119.892L188.228 121.388L184.017 123.041L179.94 124.855L176.02 126.664L172.268 128.614L168.655 130.571L165.201 132.675L161.889 134.934L158.772 137.158L156.166 139.241L152.713 142.005L147.319 147.098L142.196 152.52L137.37 158.552L135.119 161.703L132.876 164.987L131.828 166.941L130.967 168.367L130.915 168.459L128.809 172.069L126.856 175.825L126.224 177.297L125.966 177.901L125.942 177.953L125.045 179.745L123.391 183.804L121.739 188.011L121.387 189.065L121.288 189.368L120.076 192.545L118.724 197.198L118.654 197.456L117.669 200.977L117.366 202.031L116.01 207.001L115.065 211.727L114.976 212.181L114.958 212.264L113.93 217.686L113.901 217.849L112.998 223.577L112.921 224.345L112.851 225.06L112.81 225.467L112.243 229.309L111.643 235.456L111.644 237.4L111.34 241.649L111.04 248.142L110.889 254.776V261.553L111.04 268.646L111.466 275.305L111.493 275.721L112.014 282.142L112.095 283.111L112.7 289.152L112.841 290.574L112.85 290.662L113.754 298.189L114.96 306.035L116.319 314.039L116.351 314.228L116.237 314.383L114.576 316.648L105.195 329.004L105.064 329.179L94.3409 343.978L83.9377 358.9L83.8314 359.116L75.7718 370.996L73.7 373.955L73.6582 374.02L73.5391 374.2L67.4714 383.675L63.8523 389.102L59.3205 396.502L54.3418 404.347L54.2959 404.429L51.4755 409.327L45.335 419.655L45.2831 419.745L43.9263 422.16L38.7907 431.222L36.6027 435.011C36.583 435.106 36.5345 435.192 36.4625 435.256L36.049 435.974L35.7085 435.088L34.1989 431.16L31.0255 422.096L30.1186 419.68L26.6412 409.244L25.6825 406.081C25.2547 404.746 24.8322 403.412 24.4154 402.08L22.561 396.406L20.4417 388.991L18.9321 383.553L16.2129 373.886L15.4546 370.854L12.5868 358.773L12.434 358.17L9.41068 344.109L9.40086 344.063V343.753L9.30695 343.26C8.65604 339.988 8.041 336.728 7.461 333.483L6.84568 330.231L6.54432 329.023L4.57359 316.45L4.43342 314.401C4.09856 311.974 3.78546 309.556 3.49151 307.146L3.36396 306.17L2.96309 303.09L2.90842 302.685L2.90282 302.632L2.84395 302.185L2.45569 298.304L1.74225 291.159L1.6974 290.751L1.59087 289.651L1.54742 289.221L1.50257 288.744L1.30354 284.374C1.12497 281.81 0.971064 279.255 0.840991 276.713L0.801745 276.107L0 275.305H0.632145L0.490577 268.673L0.3392 262.933V260.603C0.309774 257.864 0.310253 255.139 0.3392 252.43V248.111L0.513005 244.39C0.578991 242.356 0.658759 240.331 0.758291 238.315L0.791932 237.4L0.790532 235.438L1.24607 229.213L1.51378 225.467L1.54462 225.03L1.54882 224.984L1.7002 223.462L2.30431 217.72L2.90702 212.753V212.142L3.61906 207.986C3.73422 207.171 3.8525 206.358 3.97368 205.546L4.36053 202.363L4.4222 201.847L4.57218 200.791L5.33186 196.99L6.23736 192.311L6.84145 189.141L7.14423 187.773L8.05109 183.545L8.836 180.014L8.95795 179.451L9.4135 177.64L10.0218 175.511L10.2937 174.54C10.6636 173.127 11.0415 171.719 11.4319 170.32L11.8411 168.688L11.9855 168.104L12.0668 167.783L12.441 166.594L12.8644 165.215L13.0255 164.69L13.0438 164.63L13.1419 164.31L14.1034 161.302L14.4244 160.335C14.6946 159.495 14.9705 158.656 15.2485 157.821L15.7601 156.031L17.1268 152.08L19.2405 146.646L19.5432 145.743L20.4346 143.657C21.2075 141.727 22.0003 139.81 22.8175 137.91L23.3206 136.673L23.622 135.775L23.6556 135.699L24.313 134.511C24.3801 134.361 24.4489 134.211 24.5163 134.062L25.0433 132.747H24.9774L25.3054 132.093L25.4049 131.842L26.3566 129.99L27.2621 128.028L27.8718 126.707H27.7877L28.1872 126.026L29.1585 124.363C29.375 123.937 29.5915 123.51 29.8103 123.085L30.1383 122.428L30.263 122.177L30.4915 121.779C30.7772 121.233 31.0651 120.688 31.3549 120.145L31.7992 119.256L31.9254 119.006L32.5547 117.896L33.1602 116.688L33.1925 116.624L33.243 116.573L33.355 116.458L33.4167 116.362C33.7922 115.689 34.1716 115.019 34.5535 114.351L34.6684 114.122L34.6965 114.073L34.8072 113.905C34.988 113.59 35.1743 113.278 35.3566 112.965L35.7421 112.278L35.8542 112.059L36.0967 111.691L36.1471 111.608H36.042L36.485 110.939L36.7681 110.513L36.8017 110.464L36.9363 110.194L37.0372 109.992V109.795L37.2278 109.793L38.0772 108.283L38.4599 107.597L38.4963 107.535L38.5846 107.378L38.9154 106.843L43.9473 99.2223H43.8464L44.3454 98.5426L44.459 98.3172L44.5136 98.3159L50.2659 90.4966L50.3095 90.4447L50.4536 90.3003L51.0536 89.5433C52.3073 87.9136 53.5836 86.3016 54.8832 84.7095L56.7695 82.3297L58.7177 80.142C60.0723 78.5745 61.4514 77.027 62.8527 75.4985L64.1859 74.0046L65.0591 73.13C66.4145 71.6961 67.7905 70.2781 69.1882 68.8805C69.7005 68.3684 70.2159 67.8591 70.7327 67.353L71.9005 66.139L73.7491 64.4601C75.5391 62.7754 77.3568 61.1234 79.2018 59.5041H79.13L80.0355 58.7517L80.7545 58.1502C83.0468 56.1778 85.3827 54.2573 87.7573 52.3845L88.7036 51.6066L89.8077 50.7865C97.885 44.5742 106.421 38.9281 115.385 33.8489L116.851 32.9897L117.775 32.5136C120.679 30.9143 123.627 29.3731 126.618 27.8911L126.985 27.6948L127.494 27.4594C130.561 25.9555 133.674 24.5139 136.829 23.1331L137.428 22.8542L137.883 22.6704C142.51 20.6718 147.233 18.8078 152.043 17.0727H151.897L154.247 16.2878C158.705 14.7258 163.239 13.2765 167.845 11.9388H167.475L170.502 11.1806C174.362 10.1023 178.271 9.10087 182.23 8.17849L182.812 8.02574L183.482 7.89257Z";
const LOGO = LOGO_D.startsWith("M") ? new Path2D(LOGO_D) : null;
function drawLogo(x, y, size, color, alpha = 1) {
if (!LOGO) return;
ctx.save();
ctx.globalAlpha = alpha; ctx.fillStyle = color;
ctx.translate(x, y); ctx.scale(size / 1000, size / 1000);
ctx.fill(LOGO);
ctx.restore();
}
// ------------------------------ layout ------------------------------
function frameRect() {
const availW = W - PAD * 2, availH = H - PAD * 2;
const s = Math.min(availW / VW(), (availH - CHROME) / VH());
const w = VW() * s, h = VH() * s + CHROME;
return { x: (W - w) / 2, y: (H - h) / 2 - 14, w, h, s }; // lifted: bottom rail breathes
}
const FR = frameRect();
const pageX = x => FR.x + x * FR.s;
const pageY = y => FR.y + CHROME + y * FR.s;
// ------------------------------ cursor ------------------------------
const curTargets = [];
{
let last = C.cursorStart || { x: VW() * 0.55, y: VH() * 0.25 };
for (const b of beats) {
if (b.cursor) last = b.cursor;
else if (b.type) last = { x: b.type.box.x + b.type.box.w * 0.3, y: b.type.box.y + b.type.box.h / 2 };
curTargets.push(last);
}
}
const cursorActive = i => !!(beats[i].cursor || beats[i].click || beats[i].type);
const glideDelay = i => REDUCED_MOTION ? 0 : (beats[i].cursorDelay ?? 0.06);
const glideDuration = i => Math.min(CUR_S, beats[i].dur * 0.42);
const clickTime = i => glideDelay(i) + glideDuration(i) + (REDUCED_MOTION ? 0 : 0.035);
function cursorMotion(i, lt) {
const cur = curTargets[i];
if (i <= 0) return { ...cur, arrive: 0 };
const prev = curTargets[i - 1];
const glide = glideDuration(i);
const k = glide ? easeOutQuint(clamp((lt - glideDelay(i)) / glide, 0, 1)) : 1;
return {
x: lerp(prev.x, cur.x, k),
y: lerp(prev.y, cur.y, k),
arrive: glideDelay(i) + glide,
};
}
function cursor(t) {
const i = beatAt(t);
const lt = t - starts[i];
const motion = cursorMotion(i, lt);
// fade out on idle beats, fade in when working again
let alpha;
if (cursorActive(i)) alpha = (i > 0 && !cursorActive(i - 1)) ? easeOutCubic(clamp(lt / 0.25, 0, 1)) : 1;
else alpha = (i > 0 && cursorActive(i - 1)) ? 1 - easeInOut(clamp(lt / 0.35, 0, 1)) : 0;
return { ...motion, alpha };
}
// ------------------------------ camera ------------------------------
const WIDE = { s: WIDE_SCALE, x: W / 2, y: H / 2, mode: "wide" };
function framedTarget(point, scale, mode = "fixed") {
let px = pageX(point.x), py = pageY(point.y);
const vw2 = W / (2 * scale), vh2 = H / (2 * scale);
px = clamp(px, FR.x + Math.min(vw2, FR.w / 2), FR.x + FR.w - Math.min(vw2, FR.w / 2));
py = clamp(py, FR.y + Math.min(vh2, FR.h / 2), FR.y + FR.h - Math.min(vh2, FR.h / 2));
if (py - vh2 - FR.y < CHROME + 190 * FR.s) py = FR.y + Math.min(vh2, FR.h / 2);
if (px - vw2 - FR.x < 56 * FR.s) px = FR.x + Math.min(vw2, FR.w / 2);
return { s: scale, x: px, y: py, mode };
}
function projectedPoint(point, cam) {
return {
x: W / 2 + STAGE * ((cam.shake || 0) + cam.s * (pageX(point.x) - cam.x)),
y: H / 2 + STAGE * cam.s * (pageY(point.y) - cam.y),
};
}
function clickPointIsSafe(point, cam) {
const p = projectedPoint(point, cam);
return p.x >= CLICK_SAFE.left && p.x <= W - CLICK_SAFE.right &&
p.y >= CLICK_SAFE.top && p.y <= H - CLICK_SAFE.bottom;
}
function nudgePointIntoSafeArea(cam, point) {
const p = projectedPoint(point, cam);
const safeX = clamp(p.x, CLICK_SAFE.left, W - CLICK_SAFE.right);
const safeY = clamp(p.y, CLICK_SAFE.top, H - CLICK_SAFE.bottom);
return {
...cam,
x: cam.x + (p.x - safeX) / (STAGE * cam.s),
y: cam.y + (p.y - safeY) / (STAGE * cam.s),
};
}
function clickSafeTarget(focus, point, scale, mode = "fixed") {
const requested = framedTarget(focus, scale, mode);
if (clickPointIsSafe(point, requested)) return requested;
// Edge actions should pull back before they push the click under captions,
// telemetry, or the video edge. Keep the largest zoom that remains safe.
let low = Math.min(scale, WIDE_SCALE), high = scale;
let best = framedTarget(focus, low, mode);
if (!clickPointIsSafe(point, best)) return nudgePointIntoSafeArea(best, point);
for (let n = 0; n < 14; n++) {
const mid = (low + high) / 2;
const candidate = framedTarget(focus, mid, mode);
if (clickPointIsSafe(point, candidate)) { best = candidate; low = mid; }
else high = mid;
}
return best;
}
const camTargets = [];
for (let i = 0; i < beats.length; i++) {
const b = beats[i];
if (b.card || b.wide) { camTargets.push(WIDE); continue; }
if (b.zoom) {
const scale = b.zoom.scale || AUTO_ZOOM;
camTargets.push(b.click && b.cursor
? clickSafeTarget(b.zoom, b.cursor, scale)
: framedTarget(b.zoom, scale));
continue;
}
if (MOTION.autoFollow !== false && cursorActive(i) && !b.noFollow) {
const scale = b.followScale || AUTO_ZOOM;
camTargets.push(b.click
? clickSafeTarget(curTargets[i], curTargets[i], scale, "follow")
: framedTarget(curTargets[i], scale, "follow"));
continue;
}
if (i === 0 || beats[i - 1].card) camTargets.push(WIDE);
else camTargets.push({ ...camTargets[i - 1], mode: "fixed" });
}
function camera(t) {
const i = beatAt(t), b = beats[i], lt = t - starts[i];
const final = camTargets[i];
const desired = final.mode === "follow"
? (b.click
? clickSafeTarget(cursorMotion(i, lt), cursorMotion(i, lt), final.s, "follow")
: framedTarget(cursorMotion(i, lt), final.s, "follow"))
: final;
let cam;
if (i === 0 || b.card || b.cameraCut) cam = { ...desired };
else {
const prev = beats[i - 1].card ? WIDE : camTargets[i - 1];
const zooming = Math.abs(prev.s - desired.s) > 0.01;
const dur = zooming ? PUNCH_S : Math.min(DRIFT_MAX, b.dur * 0.65);
const k = dur ? easeOutQuint(clamp(lt / dur, 0, 1)) : 1;
cam = {
s: lerp(prev.s, desired.s, k),
x: lerp(prev.x, desired.x, k),
y: lerp(prev.y, desired.y, k),
};
}
if (b.errorMotion && !REDUCED_MOTION) {
const damp = 1 - clamp(lt / 0.42, 0, 1);
cam.shake = Math.sin(lt * 42) * 5 * damp;
} else cam.shake = 0;
return cam;
}
const clickVisibility = beats.flatMap((b, i) => {
if (!b.click || !b.cursor) return [];
const t = starts[i] + Math.min(clickTime(i), Math.max(0, b.dur - 0.001));
const cam = camera(t);
const point = projectedPoint(b.cursor, cam);
const visible = clickPointIsSafe(b.cursor, cam);
if (!visible) preflightErrors.push(`beat ${i + 1} click falls outside the safe viewport`);
const resultTime = b.after
? starts[i] + Math.min(clickTime(i) + REACTION_LAG + REACTION_FADE, b.dur)
: t;
return [{
beat: i + 1, label: b.label, time: t, resultTime,
x: point.x, y: point.y, scale: cam.s, visible,
}];
});
window.clickVisibility = () => clickVisibility.map(item => ({ ...item }));
// ------------------------------ drawing ------------------------------
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d");
const BG = Array.isArray(C.bg) ? C.bg : [C.bg || "#F0EEE6"];
const LIGHT_BG = (() => { // perceived luminance of the backdrop
const c = h => parseInt(h, 16);
const lum = s => { const h = s.replace("#", "");
return 0.299 * c(h.slice(0, 2)) + 0.587 * c(h.slice(2, 4)) + 0.114 * c(h.slice(4, 6)); };
return BG.reduce((a, s) => a + lum(s), 0) / BG.length > 128;
})();
const INK = LIGHT_BG ? "#1f1e1b" : "#ffffff"; // text/logo on the backdrop
function drawBackdrop() {
if (BG.length < 2) { ctx.fillStyle = BG[0]; ctx.fillRect(0, 0, W, H); return; }
const g = ctx.createLinearGradient(0, 0, W, H);
g.addColorStop(0, BG[0]); g.addColorStop(1, BG[1]);
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
// --- realistic Chrome window (macOS, light theme) ---
function afterIsVisible(i, lt) {
const b = beats[i];
if (!b.after) return false;
const at = b.click ? clickTime(i) + REACTION_LAG : Math.min(0.22, b.dur * 0.22);
return lt >= at + REACTION_FADE;
}
function drawChromeWindow(i, lt) {
ctx.save();
ctx.shadowColor = LIGHT_BG ? "rgba(40,35,25,0.3)" : "rgba(0,0,0,0.6)";
ctx.shadowBlur = LIGHT_BG ? 54 : 70; ctx.shadowOffsetY = LIGHT_BG ? 18 : 28;
roundRect(FR.x, FR.y, FR.w, FR.h, RADIUS);
ctx.fillStyle = FRAME_STYLE === "browser" ? "#DEE1E6" : "#EAECF0"; ctx.fill();
ctx.restore();
if (FRAME_STYLE === "raw") return;
const tcy = FR.y + TABSTRIP / 2;
[["#FF5F57"], ["#FEBC2E"], ["#28C840"]].forEach(([c], k) => {
ctx.beginPath(); ctx.arc(FR.x + 22 + k * 20, tcy, 6, 0, 7); ctx.fillStyle = c; ctx.fill();
});
// Native framing keeps the recording honest: no reconstructed tab title or
// fake URL masquerading as captured browser evidence.
if (FRAME_STYLE !== "browser") return;
const tabX = FR.x + 84, tabW = Math.min(240, FR.w * 0.3), tabH = TABSTRIP - 8;
ctx.beginPath();
ctx.moveTo(tabX - 8, FR.y + TABSTRIP);
ctx.quadraticCurveTo(tabX, FR.y + TABSTRIP, tabX, FR.y + TABSTRIP - 8);
ctx.lineTo(tabX, FR.y + 12);
ctx.quadraticCurveTo(tabX, FR.y + 4, tabX + 8, FR.y + 4);
ctx.lineTo(tabX + tabW - 8, FR.y + 4);
ctx.quadraticCurveTo(tabX + tabW, FR.y + 4, tabX + tabW, FR.y + 12);
ctx.lineTo(tabX + tabW, FR.y + TABSTRIP - 8);
ctx.quadraticCurveTo(tabX + tabW, FR.y + TABSTRIP, tabX + tabW + 8, FR.y + TABSTRIP);
ctx.closePath();
ctx.fillStyle = "#fff"; ctx.fill();
const after = afterIsVisible(i, lt);
const route = (after && beats[i].afterRoute) || sticky(i, "route") || sticky(i, "url") || "Browser";
const tab = (after && beats[i].afterTab) || sticky(i, "tab") || route.split("/").pop().trim() || "Browser";
ctx.beginPath(); ctx.arc(tabX + 18, FR.y + 4 + tabH / 2, 6.5, 0, 7);
ctx.fillStyle = "#F19E38"; ctx.fill();
ctx.fillStyle = "#3C4043"; ctx.font = `500 13px ${SANS}`;
ctx.textAlign = "left"; ctx.textBaseline = "middle";
ctx.fillText(fitText(tab, tabW - 64), tabX + 32, FR.y + 5 + tabH / 2);
ctx.fillStyle = "#5F6368"; ctx.font = `400 14px ${SANS}`;
ctx.fillText("×", tabX + tabW - 20, FR.y + 5 + tabH / 2);
ctx.font = `400 18px ${SANS}`;
ctx.fillText("+", tabX + tabW + 18, FR.y + 5 + tabH / 2);
ctx.fillStyle = "#fff";
ctx.fillRect(FR.x, FR.y + TABSTRIP, FR.w, TOOLBAR);
const ty = FR.y + TABSTRIP + TOOLBAR / 2;
arrow(FR.x + 26, ty, -1, "#5F6368");
arrow(FR.x + 58, ty, 1, "#C0C3C7");
reloadIcon(FR.x + 90, ty);
const ox = FR.x + 116, ow = FR.w - 116 - 118, oh = 30;
roundRect(ox, ty - oh / 2, ow, oh, oh / 2);
ctx.fillStyle = "#F1F3F4"; ctx.fill();
lockIcon(ox + 18, ty);
ctx.fillStyle = "#202124"; ctx.font = `400 14.5px ${SANS}`;
ctx.textAlign = "left"; ctx.textBaseline = "middle";
ctx.fillText(fitText(route, ow - 70), ox + 34, ty + 1);
starIcon(ox + ow - 20, ty);
ctx.beginPath(); ctx.arc(FR.x + FR.w - 62, ty, 12, 0, 7);
ctx.fillStyle = "#7C4DFF"; ctx.fill();
ctx.fillStyle = "#fff"; ctx.font = `600 12px ${SANS}`; ctx.textAlign = "center";
ctx.fillText("G", FR.x + FR.w - 62, ty + 1);
ctx.fillStyle = "#5F6368";
for (let k = -1; k <= 1; k++) { ctx.beginPath(); ctx.arc(FR.x + FR.w - 28, ty + k * 5, 1.8, 0, 7); ctx.fill(); }
}
function arrow(x, y, dir, color) {
ctx.strokeStyle = color; ctx.lineWidth = 1.8; ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x + 6 * dir, y - 5); ctx.lineTo(x - 3 * dir, y); ctx.lineTo(x + 6 * dir, y + 5);
ctx.moveTo(x - 3 * dir, y); ctx.lineTo(x + 8 * dir, y);
ctx.stroke();
}
function reloadIcon(x, y) {
ctx.strokeStyle = "#5F6368"; ctx.lineWidth = 1.8;
ctx.beginPath(); ctx.arc(x, y, 7, -0.4, 5.0); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x + 7, y - 5); ctx.lineTo(x + 7, y - 1); ctx.lineTo(x + 3, y - 2);
ctx.fillStyle = "#5F6368"; ctx.fill();
}
function lockIcon(x, y) {
ctx.strokeStyle = "#5F6368"; ctx.lineWidth = 1.6;
ctx.beginPath(); ctx.arc(x, y - 3, 3.5, Math.PI, 0); ctx.stroke();
ctx.fillStyle = "#5F6368";
roundRect(x - 5.5, y - 3, 11, 8, 2); ctx.fill();
}
function starIcon(x, y) {
ctx.strokeStyle = "#9AA0A6"; ctx.lineWidth = 1.4;
ctx.beginPath();
for (let k = 0; k < 10; k++) {
const r = k % 2 ? 3.2 : 7, a = -Math.PI / 2 + k * Math.PI / 5;
ctx[k ? "lineTo" : "moveTo"](x + r * Math.cos(a), y + r * Math.sin(a));
}
ctx.closePath(); ctx.stroke();
}
function fitText(s, maxW) {
if (ctx.measureText(s).width <= maxW) return s;
while (s.length > 1 && ctx.measureText(s + "…").width > maxW) s = s.slice(0, -1);
return s + "…";
}
function sticky(i, key) {
for (let k = i; k >= 0; k--)
if (Object.prototype.hasOwnProperty.call(beats[k], key) && beats[k][key] != null && beats[k][key] !== "")
return beats[k][key];
return null;
}
// Frame shown at the END of beat i (its consequence, if any)
const finalFrame = i => beats[i].after || beats[i].frame;
function fadeDur(i) {
const b = beats[i];
return b.fade != null ? b.fade : 0; // hard cut unless explicitly asked
}
function drawContent(t, i, lt) {
ctx.save();
roundRect(FR.x, FR.y + CHROME, FR.w, FR.h - CHROME, 2);
ctx.clip();
const b = beats[i];
// entry transition from the previous beat's final frame
const prevSrc = i > 0 && !beats[i - 1].card ? finalFrame(i - 1) : null;
const fd = fadeDur(i);
let base = images.get(b.frame);
const shown = [];
if (prevSrc && fd > 0 && lt < fd && prevSrc !== b.frame) {
drawPage(images.get(prevSrc));
shown.push(prevSrc);
ctx.globalAlpha = clamp(lt / fd, 0, 1);
}
if (base) { drawPage(base); shown.push(b.frame); }
ctx.globalAlpha = 1;
// in-beat consequence: swap to `after` once the click lands
if (b.after) {
const at = b.click ? clickTime(i) + REACTION_LAG : Math.min(0.22, b.dur * 0.22);
const k = clamp((lt - at) / REACTION_FADE, 0, 1);
if (k > 0) {
const im = images.get(b.after);
ctx.globalAlpha = k; drawPage(im); ctx.globalAlpha = 1;
if (im) { if (k >= 1) shown.length = 0; shown.push(b.after); }
}
}
if (C.redact)
for (const src of shown)
for (const r of C.redact[src] || []) drawRedact(r);
if (b.type) drawTyping(b, lt);
ctx.restore();
}
function drawPage(im) { if (im) ctx.drawImage(im, FR.x, FR.y + CHROME, FR.w, VH() * FR.s); }
// Opaque masks may match their surrounding surface, but never use alpha: unlike
// blur or pixelation, secrets cannot be reconstructed from them.
function drawRedact(r) {
const pad = r.pad ?? C.privacy?.pad ?? 8;
const mask = C.privacy?.mask || {};
const x = pageX(r.x - pad), y = pageY(r.y - pad);
const w = (r.w + pad * 2) * FR.s, h = (r.h + pad * 2) * FR.s;
if (w < 2 || h < 2) return;
const fill = r.fill ?? mask.fill ?? "#f2f4f7";
const stroke = r.stroke ?? mask.stroke ?? "#e2e7ec";
const radius = Math.min(r.radius ?? mask.radius ?? 7, w / FR.s / 2, h / FR.s / 2) * FR.s;
ctx.save();
ctx.globalAlpha = 1;
roundRect(x, y, w, h, radius);
ctx.fillStyle = fill; ctx.fill();
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = Math.max(1, FR.s);
ctx.stroke();
}
ctx.restore();
}
function drawTyping(b, lt) {
const { box, text } = b.type;
const visibleText = b.type.redact ? "••••••" : text;
const i = beats.indexOf(b);
const mag = camTargets[i].s < 1.25 ? 1.7 : 1; // synthetic pill: enlarge when wide
let x = pageX(box.x), y = pageY(box.y), w = box.w * FR.s * mag, h = box.h * FR.s * mag;
x -= (mag - 1) * box.w * FR.s * 0.15; y -= (mag - 1) * box.h * FR.s * 0.5;
const t0 = 0.35, t1 = Math.max(t0 + 0.25, b.dur - 0.3);
const chars = Math.round(clamp((lt - t0) / (t1 - t0), 0, 1) * visibleText.length);
ctx.save();
roundRect(x, y, w, h, Math.min(8, h / 3));
ctx.fillStyle = "rgba(255,255,255,0.98)"; ctx.fill();
ctx.strokeStyle = ACCENT; ctx.lineWidth = 1.6; ctx.stroke();
ctx.fillStyle = "#111"; ctx.textAlign = "left"; ctx.textBaseline = "middle";
ctx.font = `500 ${Math.max(13, h * 0.46)}px ${SANS}`;
const shown = visibleText.slice(0, chars);
ctx.fillText(shown, x + h * 0.32, y + h / 2 + 1);
if (lt >= t0 && (lt % 0.9) < 0.5) {
const tw = ctx.measureText(shown).width;
ctx.fillStyle = "#333";
ctx.fillRect(x + h * 0.32 + tw + 2, y + h * 0.22, 1.6, h * 0.56);
}
ctx.restore();
}
function drawCursor(t, i, lt, cam) {
const b = beats[i];
if (b.card || b.hideCursor) return;
const c = cursor(t);
if (c.alpha <= 0.01) return;
const x = pageX(c.x), y = pageY(c.y);
const clickAt = clickTime(i);
const pressed = b.click && lt >= clickAt && lt < clickAt + 0.18;
const inv = 1 / Math.max(0.01, cam.s * STAGE);
const cursorPx = cam.s < 1 ? 52 : 38;
ctx.save();
ctx.globalAlpha = c.alpha;
if (b.click && lt >= clickAt) {
const rt = clamp((lt - clickAt) / 0.5, 0, 1);
ctx.strokeStyle = `rgba(110,231,255,${0.5 * (1 - rt)})`; ctx.lineWidth = inv;
ctx.beginPath();
ctx.moveTo(x - 34 * inv, y); ctx.lineTo(x - 12 * inv, y); ctx.moveTo(x + 12 * inv, y); ctx.lineTo(x + 34 * inv, y);
ctx.moveTo(x, y - 34 * inv); ctx.lineTo(x, y - 12 * inv); ctx.moveTo(x, y + 12 * inv); ctx.lineTo(x, y + 34 * inv);
ctx.stroke();
ctx.beginPath(); ctx.arc(x, y, (7 + easeOutCubic(rt) * 34) * inv, 0, 7);
ctx.strokeStyle = `rgba(255,255,255,${0.8 * (1 - rt)})`;
ctx.lineWidth = (3 * (1 - rt) + 1) * inv; ctx.stroke();
}
const k = (cursorPx / 18.1) * inv * (pressed ? 0.85 : 1);
ctx.translate(x, y); ctx.scale(k, k);
ctx.shadowColor = "rgba(0,0,0,0.45)"; ctx.shadowBlur = 5; ctx.shadowOffsetY = 2;
ctx.beginPath();
ctx.moveTo(0, 0); ctx.lineTo(0, 16.5); ctx.lineTo(3.9, 12.8); ctx.lineTo(6.3, 18.1);
ctx.lineTo(9.3, 16.8); ctx.lineTo(6.9, 11.6); ctx.lineTo(12.2, 11.4); ctx.closePath();
ctx.fillStyle = "#fff"; ctx.fill();
ctx.shadowColor = "transparent";
ctx.strokeStyle = "#111"; ctx.lineWidth = 1.1; ctx.stroke();
ctx.restore();
}
// --- telemetry (screen space) ---
function chip(x, y, parts, align = "left", error = false) {
// parts: [[text, color], ...] rendered in one pill
// One palette: ink on paper (light bg) / paper on ink (dark bg). `parts`
// entries are [text, muted?] — no accent colors.
ctx.font = `600 21px ${MONO}`;
const texts = parts.map(p => p[0]);
const logoPad = parts.logo ? 30 : 0;
const w = texts.reduce((a, s) => a + ctx.measureText(s).width, 0) + 30 + (parts.length - 1) * 12 + logoPad;
const bx = align === "right" ? x - w : x;
roundRect(bx, y, w, 48, 24);
if (error) {
ctx.fillStyle = "rgba(160,40,40,0.95)"; ctx.fill();
} else if (LIGHT_BG) {
ctx.fillStyle = "rgba(255,253,247,0.9)"; ctx.fill();
ctx.strokeStyle = "rgba(31,30,27,0.22)"; ctx.lineWidth = 1; ctx.stroke();
} else {
ctx.fillStyle = "rgba(9,12,20,0.72)"; ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,0.13)"; ctx.lineWidth = 1; ctx.stroke();
}
const ink = error ? "#fff" : (LIGHT_BG ? "#1f1e1b" : "#f4f2ec");
ctx.textAlign = "left"; ctx.textBaseline = "middle";
let cx = bx + 15;
if (parts.logo) { drawLogo(cx - 2, y + 14, 20, ink, 0.9); cx += logoPad; }
for (const [text, muted] of parts) {
ctx.globalAlpha = muted ? 0.52 : 1;
ctx.fillStyle = ink;
ctx.fillText(text, cx, y + 25);
ctx.globalAlpha = 1;
cx += ctx.measureText(text).width + 12;
}
}
function drawPlan(i) {
const chapter = sticky(i, "chapter") ?? 0;
const detour = !!beats[i].detour;
const gap = 10, h = 58, maxW = 420;
ctx.font = `600 19px ${SANS}`;
const widths = C.plan.map((step, n) => Math.min(maxW, Math.max(240, ctx.measureText(`${n + 1}. ${step}`).width + 62)));
const total = widths.reduce((sum, width) => sum + width, 0) + gap * (widths.length - 1);
let x = (W - total) / 2;
for (let n = 0; n < C.plan.length; n++) {
const active = n === chapter;
roundRect(x, 26, widths[n], h, 18);
ctx.fillStyle = active ? "rgba(10,12,18,0.93)" : "rgba(10,12,18,0.58)"; ctx.fill();
ctx.strokeStyle = active ? "rgba(110,231,255,0.7)" : "rgba(255,255,255,0.12)";
ctx.lineWidth = active ? 2 : 1; ctx.stroke();
ctx.fillStyle = active ? "#fff" : "rgba(255,255,255,0.68)";
ctx.textAlign = "left"; ctx.textBaseline = "middle";
ctx.fillText(`${n + 1}. ${C.plan[n]}`, x + 24, 56);
x += widths[n] + gap;
}
if (detour) {
ctx.font = `700 15px ${MONO}`;
const label = "DETOUR";
const w = ctx.measureText(label).width + 30;
roundRect(W / 2 - w / 2, 92, w, 32, 16);
ctx.fillStyle = "rgba(128,63,22,0.94)"; ctx.fill();
ctx.fillStyle = "#fff"; ctx.textAlign = "center";
ctx.fillText(label, W / 2, 108);
}
}
function drawTelemetry(t, i, lt) {
const b = beats[i];
if (b.card) return;
drawPlan(i);
const cy = H - 74;
if (b.label || b.error) {
const parts = [];
if (b.label) {
parts.push([`${String(stepOf[i]).padStart(2, "0")}/${String(stepCount).padStart(2, "0")}`, true]);
parts.push([b.label, false]);
}
if (b.error) parts.push(["⚠ ERROR", false]);
chip(28, cy, parts, "left", b.error);
}
const bt = sticky(i, "t");
if (bt != null) {
let next = null;
for (let k = i + 1; k < beats.length; k++) if (beats[k].t != null) { next = beats[k].t; break; }
const cur = beats[i].t != null ? beats[i].t : bt;
const shown = next != null ? lerp(cur, next, clamp(lt / b.dur, 0, 1)) : cur;
const m = Math.floor(shown / 60), s = Math.floor(shown % 60);
const parts = [[`${m}:${String(s).padStart(2, "0")}`, false]];
parts.logo = true;
chip(W - 28, cy, parts, "right");
}
ctx.save();
const px0 = 28, pw = W - 56, py = H - 12;
ctx.fillStyle = LIGHT_BG ? "rgba(31,30,27,0.12)" : "rgba(255,255,255,0.1)";
ctx.fillRect(px0, py, pw, 2);
ctx.fillStyle = LIGHT_BG ? "rgba(31,30,27,0.5)" : "rgba(255,255,255,0.55)";
ctx.fillRect(px0, py, pw * (t / DURATION), 2);
ctx.restore();
}
function narrationState(i) {
for (let k = i; k >= 0; k--) {
if (beats[k].card) return null;
if (Object.prototype.hasOwnProperty.call(beats[k], "narration")) {
if (!beats[k].narration) return null;
return {
text: String(beats[k].narration),
index: k,
error: beats[k].narrationError ?? beats[k].error ?? false,
};
}
}
return null;
}
function captionState(i) {
const b = beats[i];
if (b.card) return null;
if (b.caption) return { text: b.caption, index: i, error: !!b.error };
return narrationState(i);
}
function drawCaptionPill(state, alpha, rise = 0) {
if (!state || alpha <= 0) return;
ctx.save();
ctx.globalAlpha = alpha;
let fs = 56;
ctx.font = `600 ${fs}px ${SANS}`;
while (ctx.measureText(state.text).width > W * 0.64 && fs > 44) {
fs -= 2; ctx.font = `600 ${fs}px ${SANS}`;
}
ctx.textAlign = "center"; ctx.textBaseline = "middle";
const ph = fs * 1.9;
const y = RAIL + rise;
const w = ctx.measureText(state.text).width;
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.4)"; ctx.shadowBlur = 24; ctx.shadowOffsetY = 6;
roundRect(W / 2 - w / 2 - 26, y - ph / 2, w + 52, ph, ph / 2);
ctx.fillStyle = state.error ? "rgba(52,10,14,0.9)" : "rgba(10,12,18,0.82)"; ctx.fill();
ctx.restore();
ctx.strokeStyle = state.error ? "rgba(255,90,90,0.55)" : "rgba(255,255,255,0.14)"; ctx.lineWidth = 1;
roundRect(W / 2 - w / 2 - 26, y - ph / 2, w + 52, ph, ph / 2); ctx.stroke();
ctx.fillStyle = state.error ? "#ffd7d7" : "#fff";
ctx.fillText(state.text, W / 2, y + 1);
ctx.restore();
}
function drawCaption(t, i) {
const state = captionState(i);
if (!state) return;
const elapsed = t - starts[state.index];
const k = REDUCED_MOTION ? 1 : easeOutQuart(clamp((elapsed - 0.04) / 0.22, 0, 1));
if (state.index === i && i > 0 && k < 1) {
const previous = captionState(i - 1);
if (previous && previous.text !== state.text)
drawCaptionPill(previous, 1 - k, -8 * k);
}
drawCaptionPill(state, k, 12 * (1 - k));
}
function drawCard(i, lt) {
const b = beats[i];
const last = i === beats.length - 1;
const aIn = easeOutCubic(clamp(lt / 0.35, 0, 1));
const aOut = last ? 1 : 1 - easeInOut(clamp((lt - (b.dur - 0.3)) / 0.3, 0, 1));
const a = Math.min(aIn, aOut);
ctx.save();
ctx.globalAlpha = a;
// Center each card's complete visual stack, not just its title. The intro is
// shorter than explanation/outcome cards, so it needs the larger offset.
const centerShift = b.kind === "intro" ? 154
: b.kind === "explanation" ? 106
: b.kind === "outcome" ? 116
: 0;
const rise = centerShift + (1 - aIn) * 16;
drawLogo(W / 2 - 40, 130 + rise, 80, INK, 1);
ctx.fillStyle = INK; ctx.textAlign = "center"; ctx.textBaseline = "middle";
let titleSize = 72;
ctx.font = `600 ${titleSize}px ${SANS}`;
while (ctx.measureText(b.title || "").width > W * 0.8 && titleSize > 52) {
titleSize -= 2; ctx.font = `600 ${titleSize}px ${SANS}`;
}
ctx.fillText(b.title || "", W / 2, 300 + rise);
if (b.sub) {
ctx.font = `500 25px ${MONO}`;
ctx.globalAlpha = a * 0.6; ctx.fillStyle = INK;
ctx.fillText(b.sub, W / 2, 386 + rise);
ctx.globalAlpha = a;
}
if (b.kind === "intro" && C.plan) {
const gap = 18, w = 430, h = 118;
const total = C.plan.length * w + (C.plan.length - 1) * gap;
let x = (W - total) / 2;
for (let n = 0; n < C.plan.length; n++) {
roundRect(x, 525 + rise, w, h, 20);
ctx.fillStyle = LIGHT_BG ? "rgba(255,255,255,0.62)" : "rgba(255,255,255,0.08)"; ctx.fill();
ctx.strokeStyle = LIGHT_BG ? "rgba(31,30,27,0.18)" : "rgba(255,255,255,0.14)"; ctx.stroke();
ctx.font = `700 20px ${MONO}`; ctx.textAlign = "left"; ctx.fillStyle = INK;
ctx.fillText(String(n + 1).padStart(2, "0"), x + 28, 559 + rise);
ctx.font = `600 26px ${SANS}`; ctx.fillText(C.plan[n], x + 28, 607 + rise);
x += w + gap;
}
}
if (b.kind === "explanation" && b.points) {
const firstReveal = 1.1;
const finalHold = 3.0;
const revealSpan = Math.max(0, b.dur - firstReveal - finalHold);
const revealGap = b.points.length > 1 ? revealSpan / (b.points.length - 1) : 0;
const current = Math.min(
b.points.length - 1,
Math.max(-1, Math.floor((lt - firstReveal) / Math.max(0.001, revealGap))),
);
let y = 505 + rise;
for (let n = 0; n < b.points.length; n++) {
const point = b.points[n];
const revealAt = firstReveal + n * revealGap;
const k = lt < revealAt ? 0 : REDUCED_MOTION ? 1 : easeOutQuart(clamp((lt - revealAt) / 0.28, 0, 1));
if (k <= 0) { y += 106; continue; }
const active = n === current;
const rowAlpha = active ? 1 : 0.56;
const slide = REDUCED_MOTION ? 0 : 14 * (1 - k);
ctx.textAlign = "left";
ctx.font = `700 18px ${MONO}`; ctx.globalAlpha = a * k * rowAlpha; ctx.fillStyle = INK;
const label = `${String(n + 1).padStart(2, "0")} ${String(point.label || "").toUpperCase()}`;
ctx.fillText(label, 360 + slide, y);
ctx.font = `${active ? 650 : 560} 31px ${SANS}`;
ctx.fillText(point.text || "", 590 + slide, y);
y += 106;
}
ctx.globalAlpha = a;
}
if (b.kind === "outcome" && b.outcomes) {
let y = 515 + rise;
for (const outcome of b.outcomes) {
ctx.beginPath(); ctx.arc(520, y, 18, 0, 7); ctx.fillStyle = "#157f63"; ctx.fill();
ctx.fillStyle = "#fff"; ctx.font = `700 19px ${SANS}`; ctx.textAlign = "center"; ctx.fillText("✓", 520, y + 1);
ctx.textAlign = "left"; ctx.fillStyle = INK; ctx.font = `600 32px ${SANS}`;
ctx.fillText(outcome, 570, y);
y += 92;
}
}
ctx.restore();
}
function draw() {
if (!ready) return;
const t = clamp(playhead, 0, DURATION - 0.001);
const i = beatAt(t), lt = t - starts[i];
drawBackdrop();
if (beats[i].card) { drawCard(i, lt); drawHud(t, i); return; }
const cam = camera(t);
ctx.save();
// Stage inset: the camera's output maps to a slightly smaller rect so a
// margin of backdrop stays visible on every side at ANY zoom (Screen Studio
// keeps this breathing room; full-bleed content reads as a raw screencap).
ctx.translate(W / 2, H / 2); ctx.scale(STAGE, STAGE); ctx.translate(-W / 2, -H / 2);
ctx.translate(W / 2 + (cam.shake || 0), H / 2); ctx.scale(cam.s, cam.s); ctx.translate(-cam.x, -cam.y);
// Screen-Studio window entrance after a card
if (i === firstWindowBeat) {
const k = easeOutCubic(clamp(lt / 0.45, 0, 1));
const ws = 0.94 + 0.06 * k;
ctx.translate(W / 2, H / 2); ctx.scale(ws, ws); ctx.translate(-W / 2, -H / 2);
ctx.globalAlpha = 0.25 + 0.75 * k;
}
drawChromeWindow(i, lt);
drawContent(t, i, lt);
drawCursor(t, i, lt, cam);
ctx.restore();
ctx.globalAlpha = 1;
if (beats[i].errorMotion) {
const a = 0.12 * easeOutCubic(clamp(lt / 0.3, 0, 1));
const g = ctx.createRadialGradient(W/2, H/2, H * 0.42, W/2, H/2, H * 0.85);
g.addColorStop(0, "rgba(255,60,60,0)"); g.addColorStop(1, `rgba(255,50,50,${a})`);
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
}
drawTelemetry(t, i, lt);
drawCaption(t, i, lt);
drawHud(t, i);
}
function drawHud(t, i) {
hud.textContent = `${t.toFixed(2)}s / ${DURATION.toFixed(2)}s beat ${i + 1}/${beats.length}` +
(recorder ? " ● EXPORTING" : playing ? " ▶" : " ⏸");
}
// ------------------------------ playback ------------------------------
const hud = document.getElementById("hud");
let playhead = 0, playing = false, rafT0 = 0, recorder = null;
function tick(ts) {
if (playing) {
playhead = (ts - rafT0) / 1000;
if (playhead >= DURATION) {
playhead = DURATION; playing = false;
if (recorder) recorder.stop();
}
}
draw();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
window.seek = t => { playing = false; playhead = clamp(t, 0, DURATION); draw(); };
window.play = (from = playhead >= DURATION ? 0 : playhead) => {
playhead = from; rafT0 = performance.now() - from * 1000; playing = true;
};
window.pause = () => { playing = false; };
window.duration = () => DURATION;
// Realtime export: plays once from 0 while MediaRecorder captures the canvas.
// Keep this tab focused — rAF throttles in background tabs and stalls capture.
window.exportVideo = (filename = "video.webm") => new Promise((resolve, reject) => {
if (preflightErrors.length) {
const message = `Export blocked: ${preflightErrors.join("; ")}`;
window.__exportError = message;
reject(new Error(message));
return;
}
const stream = canvas.captureStream(60);
const mime = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"]
.find(m => MediaRecorder.isTypeSupported(m));
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 14_000_000 });
const chunks = [];
recorder.ondataavailable = e => e.data.size && chunks.push(e.data);
recorder.onstop = () => {
recorder = null;
const blob = new Blob(chunks, { type: "video/webm" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.download = filename; a.click();
window.__exported = filename;
resolve(filename);
};
recorder.start();
window.play(0);
});
document.addEventListener("keydown", e => {
if (e.key === " ") { e.preventDefault(); playing ? pause() : play(); }
if (e.key === "ArrowRight") seek(playhead + (e.shiftKey ? 1 : 0.1));
if (e.key === "ArrowLeft") seek(playhead - (e.shiftKey ? 1 : 0.1));
if (e.key === "e") exportVideo();
});
</script>
</body>
</html>