explainer-video 0.0.0-stage → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +154 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +35 -0
- package/package.json +56 -4
- package/plugin.json +13 -0
- package/skills/coursework-author/.synced-from +5 -0
- package/skills/coursework-author/SKILL.md +209 -0
- package/skills/craft-codeflow-flows/SKILL.md +116 -0
- package/skills/craft-codeflow-mine/SKILL.md +207 -0
- package/skills/craft-codeflow-plan/SKILL.md +65 -0
- package/skills/craft-codeflow-scene/SKILL.md +168 -0
- package/skills/craft-video-align/SKILL.md +109 -0
- package/skills/craft-video-assemble/SKILL.md +70 -0
- package/skills/craft-video-audio/SKILL.md +59 -0
- package/skills/craft-video-enrich/SKILL.md +46 -0
- package/skills/craft-video-plan/SKILL.md +57 -0
- package/skills/craft-video-publish/SKILL.md +79 -0
- package/skills/craft-video-render/SKILL.md +68 -0
- package/skills/craft-video-scene/SKILL.md +112 -0
- package/skills/craft-video-script/SKILL.md +80 -0
- package/skills/craft-video-thumbnail/SKILL.md +168 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +698 -0
- package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
- package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
- package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
- package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
- package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
- package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
- package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
- package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
- package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
- package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
- package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
- package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
- package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
- package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
- package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
- package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
- package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
- package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
- package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
- package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
- package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
- package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
- package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
- package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
- package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
- package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
- package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
- package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
- package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
- package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
- package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
- package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
- package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
- package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
- package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
- package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
- package/skills/explainer-video-from-coursework/ARCHITECTURE.md +61 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +369 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +727 -0
- package/skills/explainer-video-from-coursework/SERIES.md +243 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +998 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
- package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
- package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
- package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
- package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
- package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
- package/skills/explainer-video-from-coursework/src/thumbnail.html +119 -0
- package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
- package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
- package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
- package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
- package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
- package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
- package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
- package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
- package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
- package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
- package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
- package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
- package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
- package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
- package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
- package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
- package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
- package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
- package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
- package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
- package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
- package/skills/explainer-video-from-coursework/tools/thumbnail.mjs +224 -0
- package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
- package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
- package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
- package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
- package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Diagram scene host</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body {
|
|
8
|
+
margin: 0; padding: 0; width: 1920px; height: 1080px;
|
|
9
|
+
overflow: hidden; background: #080e1a;
|
|
10
|
+
}
|
|
11
|
+
svg#stage { display: block; width: 1920px; height: 1080px; }
|
|
12
|
+
text { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<!--
|
|
17
|
+
Host for the DIAGRAM renderer. The scene config supplies the content and the
|
|
18
|
+
beat timings (relative to a narration segment); src/timeline.js supplies the
|
|
19
|
+
MEASURED segment windows produced by the audio stage.
|
|
20
|
+
-->
|
|
21
|
+
<svg id="stage" xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080"></svg>
|
|
22
|
+
|
|
23
|
+
<script src="scene-core.js"></script>
|
|
24
|
+
<script>
|
|
25
|
+
// ?scene=<name> names the config; ?work=<dir> scopes it to a video's own dir so
|
|
26
|
+
// multiple videos don't share a timeline or scene namespace (ROADMAP §1, "Work-dir layout").
|
|
27
|
+
var Q = new URLSearchParams(location.search);
|
|
28
|
+
var NAME = Q.get('scene') || 'srp-split';
|
|
29
|
+
var WORK = Q.get('work') || '';
|
|
30
|
+
var tlEl = document.createElement('script');
|
|
31
|
+
tlEl.src = WORK ? WORK + '/timeline.js' : 'timeline.js';
|
|
32
|
+
tlEl.onload = function () {
|
|
33
|
+
var s = document.createElement('script');
|
|
34
|
+
s.src = WORK ? WORK + '/scenes/' + NAME + '.js' : '../scenes/' + NAME + '.js';
|
|
35
|
+
s.onload = function () {
|
|
36
|
+
var TL = window.TIMELINE;
|
|
37
|
+
var scene = window.SceneCore.createScene({
|
|
38
|
+
svg: document.getElementById('stage'),
|
|
39
|
+
config: window.SCENE_CONFIG,
|
|
40
|
+
timeline: TL,
|
|
41
|
+
});
|
|
42
|
+
window.renderAt = function (t) { scene.renderAt(t); return true; };
|
|
43
|
+
window.VIDEO_META = { width: TL.width, height: TL.height, fps: TL.fps, total: TL.total };
|
|
44
|
+
};
|
|
45
|
+
document.head.appendChild(s);
|
|
46
|
+
};
|
|
47
|
+
document.head.appendChild(tlEl);
|
|
48
|
+
</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>SVG scene host — the open lane</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body { margin:0; padding:0; width:1920px; height:1080px; overflow:hidden; background:#080e1a; }
|
|
8
|
+
svg#stage { display:block; width:1920px; height:1080px; }
|
|
9
|
+
/* must match the font textW() measures with, or primitive layout drifts */
|
|
10
|
+
text { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<!--
|
|
15
|
+
Open lane for stage 5. The author's scene script defines:
|
|
16
|
+
|
|
17
|
+
window.SCENE_CONFIG = { renderAt(t, svg, timeline) }
|
|
18
|
+
|
|
19
|
+
renderAt is a PURE function of time: it draws the frame for t and returns, so
|
|
20
|
+
frames can be rendered in any order and re-rendered after an audio change. The
|
|
21
|
+
author owns the SVG completely — no node kinds, no fixed vocabulary.
|
|
22
|
+
-->
|
|
23
|
+
<svg id="stage" xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080"></svg>
|
|
24
|
+
|
|
25
|
+
<script src="scene-core.js"></script>
|
|
26
|
+
<script>
|
|
27
|
+
var Q = new URLSearchParams(location.search);
|
|
28
|
+
var NAME = Q.get('scene') || 'svg';
|
|
29
|
+
var WORK = Q.get('work') || '';
|
|
30
|
+
var tlEl = document.createElement('script');
|
|
31
|
+
tlEl.src = WORK ? WORK + '/timeline.js' : 'timeline.js';
|
|
32
|
+
tlEl.onload = function () {
|
|
33
|
+
var s = document.createElement('script');
|
|
34
|
+
s.src = WORK ? WORK + '/scenes/' + NAME + '.js' : '../scenes/' + NAME + '.js';
|
|
35
|
+
s.onload = function () {
|
|
36
|
+
var TL = window.TIMELINE;
|
|
37
|
+
var cfg = window.SCENE_CONFIG;
|
|
38
|
+
if (!cfg || typeof cfg.renderAt !== 'function') {
|
|
39
|
+
throw new Error(NAME + ': SCENE_CONFIG must expose renderAt(t, svg, timeline)');
|
|
40
|
+
}
|
|
41
|
+
var stage = document.getElementById('stage');
|
|
42
|
+
window.renderAt = function (t) { cfg.renderAt(t, stage, TL); return true; };
|
|
43
|
+
window.VIDEO_META = { width: TL.width, height: TL.height, fps: TL.fps, total: TL.total };
|
|
44
|
+
};
|
|
45
|
+
document.head.appendChild(s);
|
|
46
|
+
};
|
|
47
|
+
document.head.appendChild(tlEl);
|
|
48
|
+
</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Thumbnail host — stage 6b</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body { margin:0; padding:0; width:1280px; height:720px; overflow:hidden; background:#0b0b12; }
|
|
8
|
+
#frame { position:relative; width:1280px; height:720px; overflow:hidden; }
|
|
9
|
+
/* the still: a frame of the video's own scene, cropped to its strongest part and pushed louder */
|
|
10
|
+
#still { position:absolute; inset:0; width:1280px; height:720px; object-fit:cover;
|
|
11
|
+
filter:saturate(1.35) contrast(1.12) brightness(0.9); }
|
|
12
|
+
/* a dark wedge behind the hook, so white type reads at any size */
|
|
13
|
+
#shade { position:absolute; inset:0;
|
|
14
|
+
background:linear-gradient(100deg, rgba(0,0,0,.86) 0%, rgba(0,0,0,.62) 42%, rgba(0,0,0,0) 74%),
|
|
15
|
+
linear-gradient(0deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,0) 32%); }
|
|
16
|
+
#label { position:absolute; left:0; right:0; top:0; padding:14px 40px 16px; box-sizing:border-box;
|
|
17
|
+
color:#111; font:800 40px/1.12 "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
18
|
+
letter-spacing:-0.01em; box-shadow:0 6px 0 rgba(0,0,0,.55); }
|
|
19
|
+
#hook { position:absolute; left:52px; width:780px; display:flex; flex-direction:column; justify-content:center;
|
|
20
|
+
transform:rotate(-2.5deg); transform-origin:left center; }
|
|
21
|
+
#hook .t { margin:0; color:#fff; line-height:0.98; letter-spacing:0.01em;
|
|
22
|
+
font-family:Impact, Haettenschweiler, "Arial Narrow Bold", "Arial Black", sans-serif; font-weight:900;
|
|
23
|
+
-webkit-text-stroke:0.085em #000; paint-order:stroke fill; text-shadow:0.06em 0.07em 0 #000;
|
|
24
|
+
overflow-wrap:normal; word-break:keep-all; }
|
|
25
|
+
#badge { position:absolute; right:46px; padding:14px 26px; transform:rotate(7deg);
|
|
26
|
+
color:#111; font:900 44px/1 Impact, Haettenschweiler, "Arial Black", sans-serif;
|
|
27
|
+
border:6px solid #fff; border-radius:18px; box-shadow:8px 9px 0 #000; }
|
|
28
|
+
#bar { position:absolute; left:0; right:0; bottom:0; height:16px; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<!--
|
|
33
|
+
Stage 6b's thumbnail host. tools/thumbnail.mjs passes the resolved config as JSON in `?c=`:
|
|
34
|
+
|
|
35
|
+
{ hook, marked[] (word positions in the hook), label|null, still (file URL), focus {x, y, zoom}, accent, badge|null }
|
|
36
|
+
|
|
37
|
+
and photographs the page at 1280x720 with the headless Chromium. Everything is laid out
|
|
38
|
+
synchronously before the load event, so the screenshot never races the layout.
|
|
39
|
+
-->
|
|
40
|
+
<div id="frame">
|
|
41
|
+
<img id="still" alt="">
|
|
42
|
+
<div id="shade"></div>
|
|
43
|
+
<div id="hook"><p class="t"></p></div>
|
|
44
|
+
<div id="bar"></div>
|
|
45
|
+
</div>
|
|
46
|
+
<script>
|
|
47
|
+
var C = JSON.parse(new URLSearchParams(location.search).get('c') || '{}');
|
|
48
|
+
var frame = document.getElementById('frame');
|
|
49
|
+
var accent = C.accent || '#FFD400';
|
|
50
|
+
|
|
51
|
+
var still = document.getElementById('still');
|
|
52
|
+
var f = C.focus || {};
|
|
53
|
+
var zoom = f.zoom || 1;
|
|
54
|
+
still.style.objectPosition = (100 * (f.x == null ? 0.5 : f.x)) + '% ' + (100 * (f.y == null ? 0.5 : f.y)) + '%';
|
|
55
|
+
still.style.transformOrigin = still.style.objectPosition;
|
|
56
|
+
still.style.transform = 'scale(' + zoom + ')';
|
|
57
|
+
if (C.still) still.src = C.still;
|
|
58
|
+
|
|
59
|
+
document.getElementById('bar').style.background = accent;
|
|
60
|
+
|
|
61
|
+
// shrink a box's font until nothing overflows its width and its content fits h; returns the size it settled on.
|
|
62
|
+
// With `text` (an inline child holding the words), the width test is the text's own width against the box's
|
|
63
|
+
// content width, so the box's side padding stays clear; scrollWidth alone cannot see into the right padding.
|
|
64
|
+
function fit(el, max, min, h, text) {
|
|
65
|
+
var lo = min, hi = max;
|
|
66
|
+
var cs = getComputedStyle(el);
|
|
67
|
+
var room = el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
|
68
|
+
while (hi - lo > 1) {
|
|
69
|
+
var mid = (lo + hi) >> 1;
|
|
70
|
+
el.style.fontSize = mid + 'px';
|
|
71
|
+
var wide = text ? text.getBoundingClientRect().width > room : el.scrollWidth > el.clientWidth;
|
|
72
|
+
if (!wide && el.scrollHeight <= h) lo = mid; else hi = mid;
|
|
73
|
+
}
|
|
74
|
+
el.style.fontSize = lo + 'px';
|
|
75
|
+
return lo;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var y0 = 40; // where the hook may start: below the label when there is one (never `top`, which is window.top)
|
|
79
|
+
if (C.label) {
|
|
80
|
+
var label = document.createElement('div');
|
|
81
|
+
label.id = 'label';
|
|
82
|
+
label.style.background = accent;
|
|
83
|
+
var words = document.createElement('span');
|
|
84
|
+
words.textContent = C.label;
|
|
85
|
+
label.appendChild(words);
|
|
86
|
+
frame.appendChild(label);
|
|
87
|
+
// one line when it fits at a readable size, else two
|
|
88
|
+
label.style.whiteSpace = 'nowrap';
|
|
89
|
+
if (fit(label, 40, 18, 200, words) < 30) { label.style.whiteSpace = "normal"; fit(label, 36, 18, 112, words); }
|
|
90
|
+
y0 = label.offsetHeight + 34;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (C.badge) {
|
|
94
|
+
var badge = document.createElement('div');
|
|
95
|
+
badge.id = 'badge';
|
|
96
|
+
badge.style.background = accent;
|
|
97
|
+
badge.style.top = (y0 + 6) + 'px';
|
|
98
|
+
badge.textContent = C.badge;
|
|
99
|
+
frame.appendChild(badge);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var hook = document.getElementById('hook');
|
|
103
|
+
var text = hook.querySelector('.t');
|
|
104
|
+
var marked = C.marked || [];
|
|
105
|
+
String(C.hook || '').split(' ').forEach(function (word, i) {
|
|
106
|
+
if (i) text.appendChild(document.createTextNode(' '));
|
|
107
|
+
var span = document.createElement('span');
|
|
108
|
+
span.textContent = word;
|
|
109
|
+
if (marked.indexOf(i) !== -1) span.style.color = accent;
|
|
110
|
+
text.appendChild(span);
|
|
111
|
+
});
|
|
112
|
+
var hookH = 720 - y0 - 64;
|
|
113
|
+
hook.style.top = y0 + 'px';
|
|
114
|
+
hook.style.height = hookH + 'px';
|
|
115
|
+
fit(text, 230, 48, hookH);
|
|
116
|
+
document.body.setAttribute('data-ready', '1');
|
|
117
|
+
</script>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
window.TIMELINE = {
|
|
2
|
+
"fps": 30,
|
|
3
|
+
"width": 1920,
|
|
4
|
+
"height": 1080,
|
|
5
|
+
"total": 392.220317,
|
|
6
|
+
"scenes": {
|
|
7
|
+
"hook": {
|
|
8
|
+
"start": 0.6,
|
|
9
|
+
"end": 23.88,
|
|
10
|
+
"dur": 23.28
|
|
11
|
+
},
|
|
12
|
+
"why": {
|
|
13
|
+
"start": 24.63,
|
|
14
|
+
"end": 55.67,
|
|
15
|
+
"dur": 31.04
|
|
16
|
+
},
|
|
17
|
+
"why-analogy": {
|
|
18
|
+
"start": 56.42,
|
|
19
|
+
"end": 75.74,
|
|
20
|
+
"dur": 19.32
|
|
21
|
+
},
|
|
22
|
+
"l4-vs-l7": {
|
|
23
|
+
"start": 76.49,
|
|
24
|
+
"end": 114.01,
|
|
25
|
+
"dur": 37.52
|
|
26
|
+
},
|
|
27
|
+
"l4l7-analogy": {
|
|
28
|
+
"start": 114.76,
|
|
29
|
+
"end": 135.52,
|
|
30
|
+
"dur": 20.76
|
|
31
|
+
},
|
|
32
|
+
"round-robin": {
|
|
33
|
+
"start": 136.27,
|
|
34
|
+
"end": 163.91,
|
|
35
|
+
"dur": 27.64
|
|
36
|
+
},
|
|
37
|
+
"weighted-round-robin": {
|
|
38
|
+
"start": 164.66,
|
|
39
|
+
"end": 183.38,
|
|
40
|
+
"dur": 18.72
|
|
41
|
+
},
|
|
42
|
+
"least-connections": {
|
|
43
|
+
"start": 184.13,
|
|
44
|
+
"end": 206.25,
|
|
45
|
+
"dur": 22.12
|
|
46
|
+
},
|
|
47
|
+
"ip-hash": {
|
|
48
|
+
"start": 207,
|
|
49
|
+
"end": 231.6,
|
|
50
|
+
"dur": 24.6
|
|
51
|
+
},
|
|
52
|
+
"consistent-hashing": {
|
|
53
|
+
"start": 232.35,
|
|
54
|
+
"end": 262.43,
|
|
55
|
+
"dur": 30.08
|
|
56
|
+
},
|
|
57
|
+
"least-response-time": {
|
|
58
|
+
"start": 263.18,
|
|
59
|
+
"end": 284.94,
|
|
60
|
+
"dur": 21.76
|
|
61
|
+
},
|
|
62
|
+
"health-checks": {
|
|
63
|
+
"start": 285.69,
|
|
64
|
+
"end": 313.57,
|
|
65
|
+
"dur": 27.88
|
|
66
|
+
},
|
|
67
|
+
"graceful-draining": {
|
|
68
|
+
"start": 314.32,
|
|
69
|
+
"end": 337.96,
|
|
70
|
+
"dur": 23.64
|
|
71
|
+
},
|
|
72
|
+
"code-demos": {
|
|
73
|
+
"start": 338.71,
|
|
74
|
+
"end": 366.55,
|
|
75
|
+
"dur": 27.84
|
|
76
|
+
},
|
|
77
|
+
"outro": {
|
|
78
|
+
"start": 367.3,
|
|
79
|
+
"end": 390.62,
|
|
80
|
+
"dur": 23.32
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"windows": {
|
|
84
|
+
"hook": {
|
|
85
|
+
"start": 0,
|
|
86
|
+
"end": 24.03,
|
|
87
|
+
"dur": 24.03
|
|
88
|
+
},
|
|
89
|
+
"why": {
|
|
90
|
+
"start": 24.03,
|
|
91
|
+
"end": 55.82,
|
|
92
|
+
"dur": 31.79
|
|
93
|
+
},
|
|
94
|
+
"why-analogy": {
|
|
95
|
+
"start": 55.82,
|
|
96
|
+
"end": 75.89,
|
|
97
|
+
"dur": 20.07
|
|
98
|
+
},
|
|
99
|
+
"l4-vs-l7": {
|
|
100
|
+
"start": 75.89,
|
|
101
|
+
"end": 114.16,
|
|
102
|
+
"dur": 38.27
|
|
103
|
+
},
|
|
104
|
+
"l4l7-analogy": {
|
|
105
|
+
"start": 114.16,
|
|
106
|
+
"end": 135.67,
|
|
107
|
+
"dur": 21.51
|
|
108
|
+
},
|
|
109
|
+
"round-robin": {
|
|
110
|
+
"start": 135.67,
|
|
111
|
+
"end": 164.06,
|
|
112
|
+
"dur": 28.39
|
|
113
|
+
},
|
|
114
|
+
"weighted-round-robin": {
|
|
115
|
+
"start": 164.06,
|
|
116
|
+
"end": 183.53,
|
|
117
|
+
"dur": 19.47
|
|
118
|
+
},
|
|
119
|
+
"least-connections": {
|
|
120
|
+
"start": 183.53,
|
|
121
|
+
"end": 206.4,
|
|
122
|
+
"dur": 22.87
|
|
123
|
+
},
|
|
124
|
+
"ip-hash": {
|
|
125
|
+
"start": 206.4,
|
|
126
|
+
"end": 231.75,
|
|
127
|
+
"dur": 25.35
|
|
128
|
+
},
|
|
129
|
+
"consistent-hashing": {
|
|
130
|
+
"start": 231.75,
|
|
131
|
+
"end": 262.58,
|
|
132
|
+
"dur": 30.83
|
|
133
|
+
},
|
|
134
|
+
"least-response-time": {
|
|
135
|
+
"start": 262.58,
|
|
136
|
+
"end": 285.09,
|
|
137
|
+
"dur": 22.51
|
|
138
|
+
},
|
|
139
|
+
"health-checks": {
|
|
140
|
+
"start": 285.09,
|
|
141
|
+
"end": 313.72,
|
|
142
|
+
"dur": 28.63
|
|
143
|
+
},
|
|
144
|
+
"graceful-draining": {
|
|
145
|
+
"start": 313.72,
|
|
146
|
+
"end": 338.11,
|
|
147
|
+
"dur": 24.39
|
|
148
|
+
},
|
|
149
|
+
"code-demos": {
|
|
150
|
+
"start": 338.11,
|
|
151
|
+
"end": 366.7,
|
|
152
|
+
"dur": 28.59
|
|
153
|
+
},
|
|
154
|
+
"outro": {
|
|
155
|
+
"start": 366.7,
|
|
156
|
+
"end": 392.22,
|
|
157
|
+
"dur": 25.52
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"segments": [
|
|
161
|
+
{
|
|
162
|
+
"key": "hook",
|
|
163
|
+
"start": 0.6,
|
|
164
|
+
"end": 23.88,
|
|
165
|
+
"duration": 23.28,
|
|
166
|
+
"pad": 0,
|
|
167
|
+
"words": 65,
|
|
168
|
+
"cached": true
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"key": "why",
|
|
172
|
+
"start": 24.63,
|
|
173
|
+
"end": 55.67,
|
|
174
|
+
"duration": 31.04,
|
|
175
|
+
"pad": 0,
|
|
176
|
+
"words": 70,
|
|
177
|
+
"cached": true
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"key": "why-analogy",
|
|
181
|
+
"start": 56.42,
|
|
182
|
+
"end": 75.74,
|
|
183
|
+
"duration": 19.32,
|
|
184
|
+
"pad": 0,
|
|
185
|
+
"words": 58,
|
|
186
|
+
"cached": true
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"key": "l4-vs-l7",
|
|
190
|
+
"start": 76.49,
|
|
191
|
+
"end": 114.01,
|
|
192
|
+
"duration": 37.52,
|
|
193
|
+
"pad": 0,
|
|
194
|
+
"words": 86,
|
|
195
|
+
"cached": true
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"key": "l4l7-analogy",
|
|
199
|
+
"start": 114.76,
|
|
200
|
+
"end": 135.52,
|
|
201
|
+
"duration": 20.76,
|
|
202
|
+
"pad": 0,
|
|
203
|
+
"words": 55,
|
|
204
|
+
"cached": true
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"key": "round-robin",
|
|
208
|
+
"start": 136.27,
|
|
209
|
+
"end": 163.91,
|
|
210
|
+
"duration": 27.64,
|
|
211
|
+
"pad": 0,
|
|
212
|
+
"words": 66,
|
|
213
|
+
"cached": true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"key": "weighted-round-robin",
|
|
217
|
+
"start": 164.66,
|
|
218
|
+
"end": 183.38,
|
|
219
|
+
"duration": 18.72,
|
|
220
|
+
"pad": 0,
|
|
221
|
+
"words": 49,
|
|
222
|
+
"cached": true
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"key": "least-connections",
|
|
226
|
+
"start": 184.13,
|
|
227
|
+
"end": 206.25,
|
|
228
|
+
"duration": 22.12,
|
|
229
|
+
"pad": 0,
|
|
230
|
+
"words": 52,
|
|
231
|
+
"cached": true
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"key": "ip-hash",
|
|
235
|
+
"start": 207,
|
|
236
|
+
"end": 231.6,
|
|
237
|
+
"duration": 24.6,
|
|
238
|
+
"pad": 0,
|
|
239
|
+
"words": 64,
|
|
240
|
+
"cached": true
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"key": "consistent-hashing",
|
|
244
|
+
"start": 232.35,
|
|
245
|
+
"end": 262.43,
|
|
246
|
+
"duration": 30.08,
|
|
247
|
+
"pad": 0,
|
|
248
|
+
"words": 76,
|
|
249
|
+
"cached": true
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"key": "least-response-time",
|
|
253
|
+
"start": 263.18,
|
|
254
|
+
"end": 284.94,
|
|
255
|
+
"duration": 21.76,
|
|
256
|
+
"pad": 0,
|
|
257
|
+
"words": 49,
|
|
258
|
+
"cached": true
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"key": "health-checks",
|
|
262
|
+
"start": 285.69,
|
|
263
|
+
"end": 313.57,
|
|
264
|
+
"duration": 27.88,
|
|
265
|
+
"pad": 0,
|
|
266
|
+
"words": 67,
|
|
267
|
+
"cached": true
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"key": "graceful-draining",
|
|
271
|
+
"start": 314.32,
|
|
272
|
+
"end": 337.96,
|
|
273
|
+
"duration": 23.64,
|
|
274
|
+
"pad": 0,
|
|
275
|
+
"words": 60,
|
|
276
|
+
"cached": true
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"key": "code-demos",
|
|
280
|
+
"start": 338.71,
|
|
281
|
+
"end": 366.55,
|
|
282
|
+
"duration": 27.84,
|
|
283
|
+
"pad": 0,
|
|
284
|
+
"words": 78,
|
|
285
|
+
"cached": true
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"key": "outro",
|
|
289
|
+
"start": 367.3,
|
|
290
|
+
"end": 390.62,
|
|
291
|
+
"duration": 23.32,
|
|
292
|
+
"pad": 0,
|
|
293
|
+
"words": 68,
|
|
294
|
+
"cached": true
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
timing-chart.js — browser side of the timing audit.
|
|
3
|
+
|
|
4
|
+
`tools/timing-report.mjs` sets window.TIMING_DATA and inlines this file (plus
|
|
5
|
+
Plotly) into timing.html, so the page is self-contained and opens with no
|
|
6
|
+
network. Shape of the data:
|
|
7
|
+
|
|
8
|
+
{ mode: 'flat' | 'grouped',
|
|
9
|
+
stages: [['3', 'script'], …],
|
|
10
|
+
modules: [{ name, display, videos: [{ index, title, stages: { '<stage>': secs } }] }] }
|
|
11
|
+
|
|
12
|
+
mode 'flat' — every video on one stacked-bar chart. Used when the series
|
|
13
|
+
spans a single module, where grouping would be a pointless
|
|
14
|
+
extra click.
|
|
15
|
+
mode 'grouped' — one aggregate bar per module (summed across its videos), click
|
|
16
|
+
a bar to break that module down by video. Used as soon as a
|
|
17
|
+
series spans more than one module, which is the case that made
|
|
18
|
+
a single 85-row chart unusable.
|
|
19
|
+
|
|
20
|
+
Layout notes, all learned the hard way:
|
|
21
|
+
· the <h1>/<h2> titles are HTML, not Plotly titles — a Plotly title anchors to
|
|
22
|
+
the container while the legend anchors to the plot, so on short charts they
|
|
23
|
+
collide;
|
|
24
|
+
· the legend is placed with yanchor:'bottom' at a pixel offset converted into
|
|
25
|
+
plot fractions, after reserving room for half a tick label;
|
|
26
|
+
· rows are 34px so a two-line wrapped label does not collide with its
|
|
27
|
+
neighbour, and wrapping is capped at two lines;
|
|
28
|
+
· labels are pushed off the axis with `ticks:'outside'` + `ticklen`, since
|
|
29
|
+
cartesian axes have no tick-label padding property.
|
|
30
|
+
========================================================================== */
|
|
31
|
+
(function () {
|
|
32
|
+
const D = window.TIMING_DATA;
|
|
33
|
+
if (!D || !window.Plotly) return;
|
|
34
|
+
|
|
35
|
+
const STAGES = D.stages || [];
|
|
36
|
+
const COLORS = ['#4f46e5', '#0ea5e9', '#14b8a6', '#f59e0b', '#ef4444', '#8b5cf6',
|
|
37
|
+
'#ec4899', '#10b981', '#f97316', '#6366f1', '#a855f7', '#22c55e'];
|
|
38
|
+
const stageName = {}, colorOf = {};
|
|
39
|
+
STAGES.forEach(([k, n], i) => { stageName[k] = n; colorOf[k] = COLORS[i % COLORS.length]; });
|
|
40
|
+
const label = (k) => k + ' · ' + (stageName[k] || '');
|
|
41
|
+
|
|
42
|
+
const escHtml = (s) => String(s ?? '').replace(/&/g, '&')
|
|
43
|
+
.replace(/</g, '<').replace(/>/g, '>');
|
|
44
|
+
const rev = (a) => a.slice().reverse();
|
|
45
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
46
|
+
|
|
47
|
+
function wrap(t, max) {
|
|
48
|
+
const words = escHtml(t).split(/\s+/);
|
|
49
|
+
const lines = []; let cur = '';
|
|
50
|
+
for (const w of words) {
|
|
51
|
+
if (!cur) cur = w;
|
|
52
|
+
else if ((cur + ' ' + w).length <= max) cur += ' ' + w;
|
|
53
|
+
else { lines.push(cur); cur = w; }
|
|
54
|
+
}
|
|
55
|
+
if (cur) lines.push(cur);
|
|
56
|
+
if (lines.length > 2) { // cap at two lines so rows stay predictable
|
|
57
|
+
const rest = lines.slice(1).join(' ');
|
|
58
|
+
lines.length = 1;
|
|
59
|
+
lines.push(rest.length > max ? rest.slice(0, max - 1) + '…' : rest);
|
|
60
|
+
}
|
|
61
|
+
return lines.join('<br>');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tracesFor(videos) {
|
|
65
|
+
return STAGES.map(([k]) => ({
|
|
66
|
+
type: 'bar', orientation: 'h', name: label(k),
|
|
67
|
+
y: videos.map((v) => v.title),
|
|
68
|
+
x: videos.map((v) => round1(v.stages[k] || 0)),
|
|
69
|
+
marker: { color: colorOf[k] },
|
|
70
|
+
hovertemplate: '%{y}<br>' + label(k) + ': %{x:.1f}s<extra></extra>',
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function aggTraces(rows) {
|
|
75
|
+
return STAGES.map(([k]) => ({
|
|
76
|
+
type: 'bar', orientation: 'h', name: label(k),
|
|
77
|
+
y: rows.map((r) => r.title),
|
|
78
|
+
x: rows.map((r) => round1(r.stages[k] || 0)),
|
|
79
|
+
customdata: rows.map((r) => r.count),
|
|
80
|
+
marker: { color: colorOf[k] },
|
|
81
|
+
hovertemplate: '%{y}<br>' + label(k) + ': %{x:.1f}s<br>%{customdata} video(s)<extra></extra>',
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function layoutFor(rows, wrapMax) {
|
|
86
|
+
const legendH = 22, labelHalf = 8, gap = 12;
|
|
87
|
+
const topPad = labelHalf + gap + legendH;
|
|
88
|
+
const b = 40, rowH = 34;
|
|
89
|
+
const h = Math.max(200, rows.length * rowH + topPad + b);
|
|
90
|
+
const plotH = Math.max(48, h - topPad - b);
|
|
91
|
+
return {
|
|
92
|
+
barmode: 'stack', hovermode: 'closest',
|
|
93
|
+
xaxis: { title: { text: 'seconds', font: { size: 11 } }, gridcolor: '#eceff3', zeroline: true },
|
|
94
|
+
yaxis: {
|
|
95
|
+
automargin: true, tickmode: 'array',
|
|
96
|
+
ticks: 'outside', ticklen: 16, tickcolor: '#cbd5e1',
|
|
97
|
+
tickvals: rows.map((r) => r.title),
|
|
98
|
+
ticktext: rows.map((r) => wrap(r.title, wrapMax || 42)),
|
|
99
|
+
tickfont: { size: 11 },
|
|
100
|
+
},
|
|
101
|
+
height: h,
|
|
102
|
+
margin: { l: 0, r: 24, t: topPad, b },
|
|
103
|
+
showlegend: true,
|
|
104
|
+
legend: {
|
|
105
|
+
orientation: 'h', yanchor: 'bottom',
|
|
106
|
+
y: 1 + (labelHalf + gap) / plotH,
|
|
107
|
+
x: 0, xanchor: 'left', traceorder: 'normal', font: { size: 11 },
|
|
108
|
+
},
|
|
109
|
+
hoverlabel: { namelength: -1 },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const CONFIG = { displaylogo: false, responsive: true };
|
|
114
|
+
|
|
115
|
+
function renderFlat() {
|
|
116
|
+
const all = [];
|
|
117
|
+
for (const m of (D.modules || [])) for (const v of (m.videos || [])) all.push(v);
|
|
118
|
+
all.sort((a, b) => a.index - b.index);
|
|
119
|
+
const vs = rev(all); // plan order reads top-down
|
|
120
|
+
Plotly.newPlot('timing-chart', tracesFor(vs), layoutFor(vs, 42), CONFIG);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderGrouped() {
|
|
124
|
+
const rows = rev((D.modules || []).map((m) => {
|
|
125
|
+
const stages = {};
|
|
126
|
+
for (const [k] of STAGES) stages[k] = m.videos.reduce((a, v) => a + (v.stages[k] || 0), 0);
|
|
127
|
+
return { title: m.display, stages, count: m.videos.length, videos: m.videos };
|
|
128
|
+
}));
|
|
129
|
+
const el = document.getElementById('timing-chart');
|
|
130
|
+
Plotly.newPlot(el, aggTraces(rows), layoutFor(rows, 30), CONFIG);
|
|
131
|
+
el.on('plotly_click', (e) => {
|
|
132
|
+
const name = e.points[0].y;
|
|
133
|
+
const m = (D.modules || []).find((x) => x.display === name);
|
|
134
|
+
if (m) showDetail(m);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function showDetail(m) {
|
|
139
|
+
const wrapEl = document.getElementById('timing-detail-wrap');
|
|
140
|
+
if (!wrapEl) return;
|
|
141
|
+
document.getElementById('timing-detail-title').textContent =
|
|
142
|
+
m.display + ' — ' + m.videos.length + ' video' + (m.videos.length === 1 ? '' : 's');
|
|
143
|
+
const vs = rev(m.videos.slice().sort((a, b) => a.index - b.index));
|
|
144
|
+
Plotly.react('timing-detail', tracesFor(vs), layoutFor(vs, 46), CONFIG);
|
|
145
|
+
wrapEl.hidden = false;
|
|
146
|
+
wrapEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const reset = document.getElementById('all-modules');
|
|
150
|
+
if (reset) {
|
|
151
|
+
reset.onclick = (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
const w = document.getElementById('timing-detail-wrap');
|
|
154
|
+
if (w) w.hidden = true;
|
|
155
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (D.mode === 'grouped') renderGrouped(); else renderFlat();
|
|
160
|
+
})();
|