explainer-video 0.0.0-stage → 0.4.0

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.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +152 -2
  4. package/cordis.patch.yml +7 -0
  5. package/lib/index.js +35 -0
  6. package/package.json +56 -4
  7. package/plugin.json +13 -0
  8. package/skills/coursework-author/.synced-from +5 -0
  9. package/skills/coursework-author/SKILL.md +209 -0
  10. package/skills/craft-codeflow-flows/SKILL.md +116 -0
  11. package/skills/craft-codeflow-mine/SKILL.md +207 -0
  12. package/skills/craft-codeflow-plan/SKILL.md +65 -0
  13. package/skills/craft-codeflow-scene/SKILL.md +168 -0
  14. package/skills/craft-video-align/SKILL.md +109 -0
  15. package/skills/craft-video-assemble/SKILL.md +69 -0
  16. package/skills/craft-video-audio/SKILL.md +59 -0
  17. package/skills/craft-video-enrich/SKILL.md +46 -0
  18. package/skills/craft-video-plan/SKILL.md +57 -0
  19. package/skills/craft-video-publish/SKILL.md +79 -0
  20. package/skills/craft-video-render/SKILL.md +68 -0
  21. package/skills/craft-video-scene/SKILL.md +112 -0
  22. package/skills/craft-video-script/SKILL.md +80 -0
  23. package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
  24. package/skills/explainer-video-from-codebase/SKILL.md +659 -0
  25. package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
  26. package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
  27. package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
  28. package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
  29. package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
  30. package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
  31. package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
  32. package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
  33. package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
  34. package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
  35. package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
  36. package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
  37. package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
  38. package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
  39. package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
  40. package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
  41. package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
  42. package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
  43. package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
  44. package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
  45. package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
  46. package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
  47. package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
  48. package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
  49. package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
  50. package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
  51. package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
  52. package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
  53. package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
  54. package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
  55. package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
  56. package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
  57. package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
  58. package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
  59. package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
  60. package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
  61. package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
  62. package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
  63. package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
  64. package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
  65. package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
  66. package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
  67. package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
  68. package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
  69. package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
  70. package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
  71. package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
  72. package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
  73. package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
  74. package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
  75. package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
  76. package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
  77. package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
  78. package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
  79. package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
  80. package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
  81. package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
  82. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
  83. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
  84. package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
  85. package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
  86. package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
  87. package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
  88. package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
  89. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
  90. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
  91. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
  92. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
  93. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
  94. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
  95. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
  96. package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
  97. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
  98. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
  99. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
  100. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  101. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  102. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  103. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  104. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
  105. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
  106. package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
  107. package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
  108. package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
  109. package/skills/explainer-video-from-coursework/SERIES.md +232 -0
  110. package/skills/explainer-video-from-coursework/SETUP.md +105 -0
  111. package/skills/explainer-video-from-coursework/SKILL.md +858 -0
  112. package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
  113. package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
  114. package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
  115. package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
  116. package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
  117. package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
  118. package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
  119. package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
  120. package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
  121. package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
  122. package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
  123. package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
  124. package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
  125. package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
  126. package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
  127. package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
  128. package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
  129. package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
  130. package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
  131. package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
  132. package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
  133. package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
  134. package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
  135. package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
  136. package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
  137. package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
  138. package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
  139. package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
  140. package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
  141. package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
  142. package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
  143. package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
  144. package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
  145. package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
  146. package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
  147. package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
  148. package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
  149. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
  150. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
@@ -0,0 +1,12 @@
1
+ window.SCENE_CONFIG = { renderAt: function (t, svg) {
2
+ SceneCore.primitives.chart(svg, { type: 'pie', x: 520, y: 560, radius: 210, at: 0, dur: 1.2,
3
+ title: 'Read share',
4
+ data: [ { label: 'Linearizable', value: 3, color: 'ok' },
5
+ { label: 'Causal', value: 3, color: 'client' },
6
+ { label: 'Eventual', value: 2, color: 'hub' } ] }, t);
7
+ SceneCore.primitives.chart(svg, { type: 'bar', x: 1080, y: 400, w: 700, h: 340, at: 0, dur: 1.0,
8
+ title: 'Write latency (ms)',
9
+ data: [ { label: 'LWW', value: 5, color: 'client' },
10
+ { label: 'Vector', value: 22, color: 'hub' },
11
+ { label: 'CRDT', value: 9, color: 'ok' } ] }, t);
12
+ } };
@@ -0,0 +1,10 @@
1
+ window.SCENE_CONFIG = { renderAt: function (t, svg) {
2
+ SceneCore.primitives.equation(svg, { tex: 'P(k) = \\frac{(\\lambda t)^k e^{-\\lambda t}}{k!}',
3
+ y: 300, size: 52, at: 0 }, t);
4
+ SceneCore.primitives.equation(svg, { tex: 'a \\to b \\iff V[a] < V[b]',
5
+ y: 470, size: 48, at: 0.2 }, t);
6
+ SceneCore.primitives.equation(svg, { tex: 'V = \\sqrt{x^2 + y_{i}^{2}} \\ge \\lceil z \\rceil',
7
+ y: 640, size: 48, at: 0.4 }, t);
8
+ SceneCore.primitives.equation(svg, { tex: 'merged[i] = \\max(A[i], B[i])',
9
+ y: 800, size: 44, at: 0.6 }, t);
10
+ } };
@@ -0,0 +1,7 @@
1
+ window.SCENE_CONFIG = { renderAt: function (t, svg) {
2
+ SceneCore.primitives.hubSpoke(svg, { x: 960, y: 540, radius: 330, at: 0, stagger: 0.3,
3
+ label: 'Event Bus', sub: 'one broker, many consumers',
4
+ spokes: [ { label: 'CQRS', color: 'client' }, { label: 'Event Sourcing', color: 'ok' },
5
+ { label: 'CDC', color: 'hub' }, { label: 'Sagas', color: 'client' },
6
+ { label: 'Streams', color: 'ok' } ] }, t);
7
+ } };
@@ -0,0 +1,7 @@
1
+ window.SCENE_CONFIG = { renderAt: function (t, svg) {
2
+ SceneCore.primitives.loopFlow(svg, { at: 0, lap: 6, packetCount: 4, bow: 0.22,
3
+ nodes: [ { x: 960, y: 300, label: 'Command' },
4
+ { x: 1400, y: 540, label: 'Event Store' },
5
+ { x: 960, y: 780, label: 'Projection' },
6
+ { x: 520, y: 540, label: 'Query' } ] }, t);
7
+ } };
@@ -0,0 +1,12 @@
1
+ window.SCENE_CONFIG = { renderAt: function (t, svg) {
2
+ SceneCore.primitives.sequence(svg, { x: 220, y: 200, w: 1480, h: 720, at: 0, stagger: 0.5,
3
+ participants: [ { id: 'client', label: 'POST /orders', sub: 'HTTP request' },
4
+ { id: 'api', label: 'api.orders' },
5
+ { id: 'queue', label: 'worker.tasks' },
6
+ { id: 'db', label: 'store.orders' } ],
7
+ messages: [ { from: 'client', to: 'api', label: 'create_order', kind: 'http' },
8
+ { from: 'api', to: 'api', label: 'validate' },
9
+ { from: 'api', to: 'queue', label: 'charge_card', kind: 'queue' },
10
+ { from: 'queue', to: 'db', label: 'save' },
11
+ { from: 'api', to: 'client', label: '202 Accepted', kind: 'return' } ] }, t);
12
+ } };
@@ -0,0 +1,11 @@
1
+ window.SCENE_CONFIG = { renderAt: function (t, svg) {
2
+ SceneCore.primitives.timeline(svg, { x: 420, y: 330, w: 1180, laneH: 150, at: 0,
3
+ axisLabel: 'real time',
4
+ tracks: [ { label: 'R1', color: 'client' }, { label: 'R2', color: 'ok' } ],
5
+ events: [ { track: 0, at: 0.10, label: 'write(1)', color: 'client' },
6
+ { track: 1, at: 0.35, label: 'read()', color: 'bad' },
7
+ { track: 0, at: 0.60, label: 'write(2)', color: 'client' },
8
+ { track: 1, at: 0.85, label: 'read()', color: 'ok' } ],
9
+ arrows: [ { from: 0, to: 1, label: 'happens-before' },
10
+ { from: 2, to: 3 } ] }, t);
11
+ } };
@@ -0,0 +1,127 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Code / Refactor scene host</title>
6
+ <style>
7
+ html, body {
8
+ margin: 0; padding: 0; width: 1920px; height: 1080px;
9
+ overflow: hidden; background: #080e1a;
10
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
11
+ }
12
+ #stage { position: relative; width: 1920px; height: 1080px; }
13
+ .panel-wrap, .note-wrap { position: absolute; inset: 0; }
14
+ .note-wrap { pointer-events: none; }
15
+
16
+ /* ---- chrome ---- */
17
+ .chrome { position: absolute; inset: 0; pointer-events: none; }
18
+ .hdr {
19
+ position: absolute; left: 74px; top: 48px; display: flex; align-items: center; gap: 14px;
20
+ color: #e8eefc; font-size: 25px; font-weight: 700;
21
+ }
22
+ .hdr-dot { width: 18px; height: 18px; border-radius: 50%; background: #fbbf24; display: inline-block; }
23
+ .stage-badge {
24
+ position: absolute; right: 74px; top: 38px; padding: 13px 34px; border-radius: 25px;
25
+ background: #0f1c31; border: 1.5px solid #22334f; color: #fbbf24;
26
+ font-size: 22px; font-weight: 700; letter-spacing: 0.8px; opacity: 0;
27
+ }
28
+ .prog { position: absolute; left: 0; bottom: 0; width: 1920px; height: 6px; background: #0d1728; }
29
+ .prog-fill { height: 6px; background: #fbbf24; opacity: .85; }
30
+
31
+ /* ---- code panel ---- */
32
+ .panel { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform, opacity; }
33
+ .card {
34
+ background: #0d1728; border: 2px solid #22334f; border-radius: 16px;
35
+ overflow: hidden; box-shadow: 0 24px 70px rgba(0,0,0,.55);
36
+ }
37
+ .accent-bad .card { border-color: rgba(251,113,133,.55); }
38
+ .accent-ok .card { border-color: rgba(52,211,153,.55); }
39
+
40
+ .tab {
41
+ display: flex; align-items: center; gap: 12px;
42
+ padding: 14px 24px; background: #0f1c31; border-bottom: 1px solid #22334f;
43
+ }
44
+ .dot { width: 11px; height: 11px; border-radius: 50%; background: #34d399; flex: none; }
45
+ .accent-bad .dot { background: #fb7185; }
46
+ .accent-ok .dot { background: #34d399; }
47
+ .fname { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 20px; font-weight: 500; color: #93a7c8; }
48
+ .badge {
49
+ margin-left: auto; font-size: 15px; font-weight: 800; letter-spacing: 1.2px;
50
+ padding: 5px 14px; border-radius: 14px;
51
+ }
52
+ .accent-bad .badge { color: #fb7185; background: rgba(251,113,133,.12); border: 1px solid rgba(251,113,133,.4); }
53
+ .accent-ok .badge { color: #34d399; background: rgba(52,211,153,.12); border: 1px solid rgba(52,211,153,.4); }
54
+
55
+ .code {
56
+ padding: 20px 26px 24px 18px;
57
+ font-family: "SF Mono", Menlo, Consolas, monospace;
58
+ font-size: 22px; line-height: 34px;
59
+ }
60
+ .ln { display: flex; gap: 18px; white-space: pre; border-radius: 5px; }
61
+ .ln .num { width: 38px; flex: none; text-align: right; color: #3d4c66; user-select: none; }
62
+ .ln .txt { color: #d6deeb; }
63
+ .ln.focused { background: rgba(251,113,133,.10); box-shadow: -3px 0 0 rgba(251,113,133,.85); }
64
+ .accent-ok .ln.focused { background: rgba(52,211,153,.10); box-shadow: -3px 0 0 rgba(52,211,153,.85); }
65
+ /* an elision marker stands for unnarrated lines of a repo excerpt */
66
+ .ln.elided .txt { color: #55688a; font-style: italic; letter-spacing: 1px; }
67
+
68
+ /* ---- pause badge: only on a segment whose narration asks the viewer to pause ---- */
69
+ .pause-badge {
70
+ position: absolute; left: 50%; top: 40px; transform: translateX(-50%); opacity: 0;
71
+ display: flex; align-items: center; gap: 14px; padding: 12px 30px; border-radius: 25px;
72
+ background: #2a1e07; border: 1.5px solid rgba(251,191,36,.6); color: #fbbf24;
73
+ font-size: 22px; font-weight: 800; letter-spacing: 1.2px; white-space: nowrap; pointer-events: none;
74
+ }
75
+ .pause-icon { display: inline-flex; gap: 5px; }
76
+ .pause-icon i { display: block; width: 6px; height: 20px; border-radius: 2px; background: #fbbf24; }
77
+
78
+ /* ---- tokens ---- */
79
+ .t-k { color: #c792ea; } /* keyword */
80
+ .t-t { color: #82aaff; } /* type */
81
+ .t-f { color: #82aaff; } /* function */
82
+ .t-s { color: #c3e88d; } /* string */
83
+ .t-c { color: #55688a; font-style: italic; }
84
+ .t-n { color: #f78c6c; } /* number */
85
+ .t-u { color: #89ddff; } /* punct */
86
+ .t-p { color: #d6deeb; }
87
+
88
+ /* ---- annotation notes ---- */
89
+ .note {
90
+ position: absolute; transform: translate(-50%, 0); opacity: 0;
91
+ padding: 14px 26px; border-radius: 14px; font-size: 24px; font-weight: 600;
92
+ background: #101d33; border: 1.5px solid #2b3d5c; color: #e8eefc; white-space: nowrap;
93
+ box-shadow: 0 12px 34px rgba(0,0,0,.45);
94
+ }
95
+ .note-bad { border-color: rgba(251,113,133,.5); color: #ffd9df; }
96
+ .note-ok { border-color: rgba(52,211,153,.5); color: #d6fff0; }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <div id="stage"></div>
101
+ <script src="scene-code.js"></script>
102
+ <script>
103
+ // config name comes from ?scene=<name>, ?work=<dir> scopes the scene path per-video
104
+ var Q = new URLSearchParams(location.search);
105
+ var NAME = Q.get('scene') || 'ocp-refactor';
106
+ var WORK = Q.get('work') || '';
107
+ // the measured timeline, when there is one: a scene naming its `segment` is timed from that window
108
+ function loadScene() {
109
+ var s = document.createElement('script');
110
+ s.src = WORK ? WORK + '/scenes/' + NAME + '.js' : '../scenes/' + NAME + '.js';
111
+ s.onload = function () {
112
+ var cfg = window.SCENE_CONFIG;
113
+ var clock = window.CodeScenes.clockOf(cfg, window.TIMELINE);
114
+ var scene = window.CodeScenes.create({ root: document.getElementById('stage'), config: cfg });
115
+ window.renderAt = function (t) { scene.renderAt(t - clock.offset); return true; };
116
+ window.VIDEO_META = { width: 1920, height: 1080, fps: cfg.fps || 30, total: clock.total };
117
+ };
118
+ document.head.appendChild(s);
119
+ }
120
+ var tlEl = document.createElement('script');
121
+ tlEl.src = WORK ? WORK + '/timeline.js' : 'timeline.js';
122
+ tlEl.onload = loadScene;
123
+ tlEl.onerror = loadScene;
124
+ document.head.appendChild(tlEl);
125
+ </script>
126
+ </body>
127
+ </html>
@@ -0,0 +1,379 @@
1
+ /* =============================================================================
2
+ scene-code.js — code + refactor renderers.
3
+
4
+ Two renderers, one core:
5
+ • CODE a single file on screen, revealed line by line, with line focus
6
+ • REFACTOR the same panel splitting into BEFORE / AFTER, so a transformation
7
+ is shown rather than described
8
+
9
+ They share everything except the number of panels, which is why they live in
10
+ one module: the "refactor" view is a code view that happens to have two panels
11
+ and a layout that moves them apart.
12
+
13
+ Like the diagram renderer, renderAt(t) is a PURE FUNCTION of time.
14
+
15
+ Code excerpted from a repo (explainer-video-from-codebase's excerpt.mjs) adds,
16
+ all optional, defaults unchanged:
17
+ • numbers[] the real line number of each line (null on an elision marker)
18
+ • elided[] the indices of elision-marker lines, styled apart from code
19
+ • several `focus` acts per panel, one per narrated sentence
20
+ • a `pause` act: an on-screen badge while the narration asks the viewer to
21
+ pause and read a whole block
22
+ • `segment: <key>`: the acts are timed from that segment's window (scene time
23
+ = line time + lead, as lines.json and excerpt-check measure it), so the host
24
+ subtracts the window's start from the video time render passes; such a scene
25
+ draws no progress bar
26
+
27
+ Contract: window.CodeScenes.create({ root, config }) -> { renderAt(t) }
28
+ ========================================================================== */
29
+ (function () {
30
+ 'use strict';
31
+
32
+ /* ---------------- math ---------------- */
33
+ const clamp01 = v => (v < 0 ? 0 : v > 1 ? 1 : v);
34
+ const lerp = (a, b, p) => a + (b - a) * p;
35
+ const seg = (t, a, b) => clamp01((t - a) / (b - a || 1e-6));
36
+ const easeOut = p => 1 - Math.pow(1 - p, 3);
37
+ const easeIO = p => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);
38
+ const fadeIn = (t, a, b) => easeOut(seg(t, a, b));
39
+ const fadeOut = (t, a, b) => 1 - easeIO(seg(t, a, b));
40
+ const inWin = (t, a, b) => t >= a && t < (b == null ? Infinity : b);
41
+
42
+ /* ---------------- syntax highlighting ----------------
43
+ A deliberately small Java/C-like tokenizer. No dependency, and it only has
44
+ to handle the curated snippets we author — not arbitrary source. A language
45
+ may override the string rule (Go raw strings, JS template literals) and mark
46
+ preprocessor directives (C, C++). */
47
+ const C_KW = ['auto','break','case','const','continue','default','do','else','enum','extern','for','goto','if','inline','register','restrict','return','sizeof','static','struct','switch','typedef','union','volatile','while','NULL','true','false'];
48
+ const C_TYPES = ['int','char','short','long','float','double','void','unsigned','signed','bool','size_t','ssize_t','int8_t','int16_t','int32_t','int64_t','uint8_t','uint16_t','uint32_t','uint64_t'];
49
+ const TS_KW = ['abstract','as','async','await','break','case','catch','class','const','continue','debugger','declare','default','delete','do','else','enum','export','extends','false','finally','for','from','function','if','implements','import','in','instanceof','interface','keyof','let','namespace','new','null','of','private','protected','public','readonly','return','static','super','switch','this','throw','true','try','type','typeof','undefined','var','void','while','yield'];
50
+ const TS_TYPES = ['string','number','boolean','any','unknown','never','object','bigint','symbol'];
51
+ const C_COMMENT = /^\/\/.*|^\/\*[\s\S]*?\*\//;
52
+ const LANGS = {
53
+ java: {
54
+ keywords: new Set(['class','interface','implements','extends','public','private','protected','static','final','void','new','if','else','return','this','abstract','package','import','throws','throw','enum','super']),
55
+ types: new Set(['String','double','int','boolean','long','float','Object','List','Map']),
56
+ comment: /^\/\/.*|^\/\*[\s\S]*?\*\//,
57
+ },
58
+ python: {
59
+ keywords: new Set(['def','class','return','if','elif','else','for','while','import','from','as','with','try','except','raise','lambda','None','True','False','self','yield','pass','in','not','and','or']),
60
+ types: new Set(['int','str','float','bool','list','dict','set','tuple']),
61
+ comment: /^#.*/,
62
+ },
63
+ go: {
64
+ keywords: new Set(['break','case','chan','const','continue','default','defer','else','fallthrough','for','func','go','goto','if','import','interface','map','package','range','return','select','struct','switch','type','var','nil','true','false','iota']),
65
+ types: new Set(['string','int','int8','int16','int32','int64','uint','uint8','uint16','uint32','uint64','uintptr','float32','float64','complex64','complex128','bool','byte','rune','error','any']),
66
+ comment: C_COMMENT,
67
+ string: /^"(?:[^"\\]|\\.)*"|^'(?:[^'\\]|\\.)*'|^`[^`]*`/,
68
+ },
69
+ ts: {
70
+ keywords: new Set(TS_KW),
71
+ types: new Set(TS_TYPES),
72
+ comment: C_COMMENT,
73
+ string: /^"(?:[^"\\]|\\.)*"|^'(?:[^'\\]|\\.)*'|^`(?:[^`\\]|\\.)*`/,
74
+ },
75
+ c: {
76
+ keywords: new Set(C_KW),
77
+ types: new Set(C_TYPES),
78
+ comment: C_COMMENT,
79
+ directive: /^#\s*[a-z]+/,
80
+ },
81
+ cpp: {
82
+ keywords: new Set(C_KW.concat(['class','namespace','template','typename','public','private','protected','virtual','override','final','new','delete','this','nullptr','using','try','catch','throw','operator','friend','explicit','constexpr','noexcept','static_cast','dynamic_cast','reinterpret_cast','const_cast','decltype','mutable'])),
83
+ types: new Set(C_TYPES.concat(['std','string','vector','wchar_t','char16_t','char32_t'])),
84
+ comment: C_COMMENT,
85
+ directive: /^#\s*[a-z]+/,
86
+ },
87
+ };
88
+ LANGS.js = LANGS.typescript = LANGS.tsx = LANGS.javascript = LANGS.ts;
89
+ const STRING = /^"(?:[^"\\]|\\.)*"|^'(?:[^'\\]|\\.)*'/;
90
+
91
+ function tokenize(line, lang) {
92
+ const L = LANGS[lang] || LANGS.java;
93
+ const out = [];
94
+ let i = 0;
95
+ while (i < line.length) {
96
+ const rest = line.slice(i);
97
+ let m;
98
+ if ((m = L.comment.exec(rest))) { out.push(['c', m[0]]); i += m[0].length; continue; }
99
+ if (L.directive && (m = L.directive.exec(rest))) { out.push(['k', m[0]]); i += m[0].length; continue; }
100
+ if ((m = (L.string || STRING).exec(rest))) { out.push(['s', m[0]]); i += m[0].length; continue; }
101
+ if ((m = /^[A-Za-z_$][\w$]*/.exec(rest))) {
102
+ const w = m[0];
103
+ const kind = L.keywords.has(w) ? 'k' : L.types.has(w) ? 't' : (/^[A-Z]/.test(w) ? 't' : (rest[w.length] === '(' ? 'f' : 'p'));
104
+ out.push([kind, w]); i += w.length; continue;
105
+ }
106
+ if ((m = /^\d+(?:\.\d+)?/.exec(rest))) { out.push(['n', m[0]]); i += m[0].length; continue; }
107
+ if ((m = /^[{}()[\];,.]/.exec(rest))) { out.push(['u', m[0]]); i += 1; continue; }
108
+ out.push(['p', rest[0]]); i += 1;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ const esc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
114
+
115
+ function highlight(line, lang) {
116
+ return tokenize(line, lang).map(([k, txt]) => `<span class="t-${k}">${esc(txt)}</span>`).join('');
117
+ }
118
+
119
+ /* One panel's lines as markup. The number column shows numbers[i] when the panel
120
+ carries real line numbers (blank on an elision marker), else counts from 1. */
121
+ function panelLinesHtml(p) {
122
+ const elided = new Set((p.elided || []).map(e => (typeof e === 'number' ? e : e.at)));
123
+ return p.lines.map((ln, i) => {
124
+ const num = p.numbers ? p.numbers[i] : i + 1;
125
+ return elided.has(i)
126
+ ? `<div class="ln elided" data-i="${i}"><span class="num">${num == null ? '' : num}</span><span class="txt">${esc(ln)}</span></div>`
127
+ : `<div class="ln" data-i="${i}"><span class="num">${num == null ? '' : num}</span><span class="txt">${highlight(ln, p.lang || 'java')}</span></div>`;
128
+ }).join('');
129
+ }
130
+
131
+ /* The focus window in force at t, and how far its dim has faded in. A panel with
132
+ real line numbers (an excerpt) has one window per narrated sentence; a window
133
+ that starts as the previous one ends keeps the dim instead of flashing back.
134
+ A coursework panel (no numbers[]) keeps its one, first focus act. */
135
+ function focusState(acts, panel, numbered, t) {
136
+ const all = acts.filter(a => a.op === 'focus' && a.panel === panel);
137
+ const wins = numbered ? all : all.slice(0, 1);
138
+ if (!wins.length) return null;
139
+ const live = wins.filter(f => inWin(t, f.from, f.to));
140
+ const foc = live.length ? live[live.length - 1] : wins[0];
141
+ const active = live.length > 0;
142
+ const chained = wins.some(w => w !== foc && w.to != null && Math.abs(w.to - foc.from) < 0.05);
143
+ const k = active ? (chained ? 1 : fadeIn(t, foc.from, foc.from + 0.5)) : 0;
144
+ return { foc, active, k };
145
+ }
146
+
147
+ /* The clock a scene plays on. A coursework scene is timed on the video clock and
148
+ spans its own `total`; a scene naming its `segment` is timed from that window's
149
+ start, read from the measured timeline, and spans the whole timeline like the
150
+ diagram and svg hosts. A named segment with no window is an error, never t + 0. */
151
+ function clockOf(cfg, timeline) {
152
+ if (cfg.segment == null) return { offset: 0, total: cfg.total };
153
+ const w = timeline && timeline.windows && timeline.windows[cfg.segment];
154
+ if (!w) throw new Error(`scene names segment "${cfg.segment}" but the timeline has no window for it (run build-audio, and open the host with ?work=)`);
155
+ return { offset: w.start, total: timeline.total };
156
+ }
157
+
158
+ /* ---------------- factory ---------------- */
159
+ function create(opts) {
160
+ const root = opts.root;
161
+ const cfg = opts.config;
162
+ const W = (cfg.meta && cfg.meta.width) || 1920;
163
+ const H = (cfg.meta && cfg.meta.height) || 1080;
164
+ const TOTAL = cfg.total;
165
+ const PROGRESS = cfg.segment == null;
166
+
167
+ root.innerHTML = '';
168
+
169
+ /* --- background grid --- */
170
+ const NS = 'http://www.w3.org/2000/svg';
171
+ const svg = document.createElementNS(NS, 'svg');
172
+ svg.setAttribute('width', W); svg.setAttribute('height', H);
173
+ svg.style.position = 'absolute'; svg.style.inset = '0';
174
+ const grid = document.createElementNS(NS, 'g');
175
+ grid.setAttribute('opacity', '0.5');
176
+ for (let x = 0; x <= W; x += 60) {
177
+ const l = document.createElementNS(NS, 'line');
178
+ l.setAttribute('x1', x); l.setAttribute('y1', 0); l.setAttribute('x2', x); l.setAttribute('y2', H);
179
+ l.setAttribute('stroke', x % 300 === 0 ? '#182742' : '#111d33'); l.setAttribute('stroke-width', 1);
180
+ grid.appendChild(l);
181
+ }
182
+ for (let y = 0; y <= H; y += 60) {
183
+ const l = document.createElementNS(NS, 'line');
184
+ l.setAttribute('x1', 0); l.setAttribute('y1', y); l.setAttribute('x2', W); l.setAttribute('y2', y);
185
+ l.setAttribute('stroke', y % 300 === 0 ? '#182742' : '#111d33'); l.setAttribute('stroke-width', 1);
186
+ grid.appendChild(l);
187
+ }
188
+ svg.appendChild(grid);
189
+ root.appendChild(svg);
190
+
191
+ /* --- chrome --- */
192
+ const chrome = document.createElement('div');
193
+ chrome.className = 'chrome';
194
+ chrome.innerHTML =
195
+ `<div class="hdr"><span class="hdr-dot"></span><span>${esc((cfg.chrome && cfg.chrome.header) || '')}</span></div>` +
196
+ `<div class="stage-badge" id="stageBadge"></div>` +
197
+ (PROGRESS ? `<div class="prog"><div class="prog-fill" id="progFill"></div></div>` : '');
198
+ root.appendChild(chrome);
199
+
200
+ /* --- panels --- */
201
+ const panelWrap = document.createElement('div');
202
+ panelWrap.className = 'panel-wrap';
203
+ root.appendChild(panelWrap);
204
+
205
+ const panels = {};
206
+ (cfg.panels || []).forEach(p => {
207
+ const el = document.createElement('div');
208
+ el.className = `panel accent-${p.accent || 'ok'}`;
209
+ const lines = panelLinesHtml(p);
210
+ el.innerHTML =
211
+ `<div class="card">` +
212
+ `<div class="tab"><span class="dot"></span><span class="fname">${esc(p.file || '')}</span>` +
213
+ (p.badge ? `<span class="badge">${esc(p.badge)}</span>` : '') + `</div>` +
214
+ `<div class="code">${lines}</div></div>`;
215
+ panelWrap.appendChild(el);
216
+ panels[p.id] = { spec: p, el, lineEls: Array.from(el.querySelectorAll('.ln')), w: 0, h: 0 };
217
+ });
218
+
219
+ // natural size, measured once so layouts can centre panels exactly
220
+ Object.values(panels).forEach(p => {
221
+ p.el.style.transform = 'none';
222
+ const r = p.el.getBoundingClientRect();
223
+ p.w = r.width; p.h = r.height;
224
+ });
225
+
226
+ /* --- annotation notes --- */
227
+ const noteWrap = document.createElement('div');
228
+ noteWrap.className = 'note-wrap';
229
+ root.appendChild(noteWrap);
230
+ const notes = {};
231
+ (cfg.notes || []).forEach(n => {
232
+ const el = document.createElement('div');
233
+ el.className = `note note-${n.accent || 'hub'}`;
234
+ el.innerHTML = esc(n.text);
235
+ el.style.left = n.at[0] + 'px';
236
+ el.style.top = n.at[1] + 'px';
237
+ noteWrap.appendChild(el);
238
+ notes[n.id] = { spec: n, el };
239
+ });
240
+
241
+ // line text lengths, measured once — used for weighted reveal timing
242
+ Object.values(panels).forEach(p => {
243
+ p.lineLens = p.lineEls.map(el => {
244
+ const t = el.querySelector('.txt');
245
+ return t ? t.textContent.length : 0;
246
+ });
247
+ });
248
+
249
+ /* --- acts, normalised --- */
250
+ const acts = cfg.acts || [];
251
+
252
+ /* --- pause badge: built only when a `pause` act asks for it --- */
253
+ const pauses = acts.filter(a => a.op === 'pause');
254
+ let pauseEl = null;
255
+ if (pauses.length) {
256
+ pauseEl = document.createElement('div');
257
+ pauseEl.className = 'pause-badge';
258
+ pauseEl.innerHTML = '<span class="pause-icon"><i></i><i></i></span><span class="pause-text"></span>';
259
+ root.appendChild(pauseEl);
260
+ }
261
+ const actOf = (op, panel) => acts.filter(a => a.op === op && (!panel || a.panel === panel));
262
+
263
+ /**
264
+ * Per-line reveal offsets.
265
+ *
266
+ * A uniform stagger gives a blank line the same screen time as a 60-character
267
+ * statement, which reads as a race. Real dwell is proportional to what is on
268
+ * the line: a base cost for the eye to land, plus a cost per character.
269
+ */
270
+ function revealOffsets(p, rev) {
271
+ if (rev.stagger != null) {
272
+ return { starts: p.lineEls.map((_, i) => i * rev.stagger), natural: (p.lineEls.length - 1) * rev.stagger };
273
+ }
274
+ const base = rev.dwell && rev.dwell.base != null ? rev.dwell.base : 0.28;
275
+ const perChar = rev.dwell && rev.dwell.perChar != null ? rev.dwell.perChar : 0.038;
276
+ const speed = rev.speed || 1;
277
+ const starts = [];
278
+ let acc = 0;
279
+ p.lineLens.forEach(len => { starts.push(acc); acc += (base + len * perChar) / speed; });
280
+ return { starts, natural: acc };
281
+ }
282
+
283
+ const initOf = id => (cfg.initLayout && cfg.initLayout[id]) || { cx: W / 2, cy: H / 2, scale: 1 };
284
+
285
+ /** Pure: interpolate through the ordered layout acts. No mutation between frames. */
286
+ const layoutAt = (panelId, t) => {
287
+ const ls = acts.filter(a => a.op === 'layout' && a.panel === panelId);
288
+ const init = initOf(panelId);
289
+ let cur = init;
290
+ for (let i = 0; i < ls.length; i++) {
291
+ const a = ls[i];
292
+ if (t < a.from) break;
293
+ const from = i === 0 ? init : ls[i - 1].to;
294
+ const k = easeIO(seg(t, a.from, a.to));
295
+ cur = {
296
+ cx: lerp(from.cx, a.to.cx, k),
297
+ cy: lerp(from.cy, a.to.cy, k),
298
+ scale: lerp(from.scale, a.to.scale, k),
299
+ };
300
+ }
301
+ return cur;
302
+ };
303
+
304
+ /* ---------------- render(t) ---------------- */
305
+ let prevStage = null;
306
+
307
+ function render(t) {
308
+ /* panels */
309
+ Object.entries(panels).forEach(([id, p]) => {
310
+ const show = acts.find(a => a.op === 'show' && a.panel === id);
311
+ const hide = acts.find(a => a.op === 'hide' && a.panel === id);
312
+ let op = 1;
313
+ if (show) op = fadeIn(t, show.from, show.to);
314
+ if (hide) op *= fadeOut(t, hide.from, hide.to);
315
+
316
+ const lay = layoutAt(id, t);
317
+ const x = lay.cx - (p.w * lay.scale) / 2;
318
+ const y = lay.cy - (p.h * lay.scale) / 2;
319
+ p.el.style.opacity = op.toFixed(3);
320
+ p.el.style.transform = `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px) scale(${lay.scale.toFixed(4)})`;
321
+
322
+ /* line reveal + focus */
323
+ const rev = acts.find(a => a.op === 'reveal' && a.panel === id);
324
+ const fs = focusState(acts, id, !!p.spec.numbers, t);
325
+ const off = rev ? revealOffsets(p, rev) : null;
326
+
327
+ p.lineEls.forEach((ln, i) => {
328
+ let lineOp = 1;
329
+ if (rev) lineOp = fadeIn(t, rev.from + off.starts[i], rev.from + off.starts[i] + 0.30);
330
+
331
+ let dim = 1;
332
+ if (fs) {
333
+ const inFocus = fs.foc.lines.includes(i + 1);
334
+ dim = inFocus ? 1 : lerp(1, 0.28, fs.k);
335
+ }
336
+ ln.style.opacity = (lineOp * dim).toFixed(3);
337
+ ln.classList.toggle('focused', !!fs && fs.foc.lines.includes(i + 1) && fs.active);
338
+ });
339
+ });
340
+
341
+ /* notes */
342
+ Object.values(notes).forEach(n => {
343
+ const o = fadeIn(t, n.spec.from, n.spec.from + 0.5) * fadeOut(t, n.spec.to - 0.5, n.spec.to);
344
+ n.el.style.opacity = o.toFixed(3);
345
+ n.el.style.transform = `translate(-50%, ${((1 - fadeIn(t, n.spec.from, n.spec.from + 0.5)) * 14).toFixed(1)}px)`;
346
+ });
347
+
348
+ /* stage badge driven by a `phase` list */
349
+ const phases = cfg.phases || [];
350
+ let cur = null;
351
+ for (const ph of phases) if (t >= ph.from) cur = ph;
352
+ const badge = root.querySelector('#stageBadge');
353
+ if (cur) {
354
+ if (cur.name !== prevStage) { badge.textContent = cur.name; prevStage = cur.name; }
355
+ const local = t - cur.from;
356
+ const dur = (cur.to == null ? TOTAL : cur.to) - cur.from;
357
+ badge.style.opacity = (clamp01(local / 0.3) * clamp01((dur - local) / 0.3) * 0.98).toFixed(3);
358
+ } else {
359
+ badge.style.opacity = '0';
360
+ }
361
+
362
+ /* pause badge: up while the narration asks the viewer to pause and read */
363
+ if (pauseEl) {
364
+ const live = pauses.filter(a => inWin(t, a.from, a.to));
365
+ const a = live.length ? live[live.length - 1] : null;
366
+ pauseEl.style.opacity = a ? (fadeIn(t, a.from, a.from + 0.3) * fadeOut(t, a.to - 0.3, a.to)).toFixed(3) : '0';
367
+ if (a) pauseEl.querySelector('.pause-text').textContent = a.text || 'PAUSE · READ THE WHOLE BLOCK';
368
+ }
369
+
370
+ /* progress */
371
+ if (PROGRESS) root.querySelector('#progFill').style.width = (W * clamp01(t / TOTAL)).toFixed(1) + 'px';
372
+ }
373
+
374
+ render(0);
375
+ return { renderAt: render };
376
+ }
377
+
378
+ window.CodeScenes = { create, clockOf, highlight, panelLinesHtml, focusState };
379
+ })();