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.
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +152 -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 +69 -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/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +659 -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 +59 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
- package/skills/explainer-video-from-coursework/SERIES.md +232 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +858 -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/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/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,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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
+
})();
|