@lalalic/markcut 2.9.0 → 3.0.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/B] +2 -0
- package/package.json +1 -1
- package/skills/markcut/docs/components.md +45 -2
- package/skills/markcut/docs/markdown-descriptive.md +5 -2
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +64 -29
- package/src/descriptive/markdown.ts +18 -0
- package/src/player/bundle/player.js +303 -44
- package/src/player/pipeline.mjs +66 -19
- package/src/schema/index.ts +2 -1
- package/src/types/Component.tsx +27 -1
- package/src/types/Effect.tsx +13 -6
- package/src/types/Folder.tsx +1 -1
- package/src/utils/index.ts +14 -2
- package/tests/fixtures/md/animate-diagrams.md +40 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/segments/test-clip_0to3_seg_1100to3000.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_0to3.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_audio.mp3 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/metadata.json +9 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.vtt +5 -0
package/src/player/pipeline.mjs
CHANGED
|
@@ -33,7 +33,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
|
|
34
34
|
// src/utils/index.ts
|
|
35
35
|
function uid() {
|
|
36
|
-
|
|
36
|
+
const raw = Math.random().toString(36).slice(2, 10);
|
|
37
|
+
const first = raw[0];
|
|
38
|
+
if (/^[0-9]/.test(first)) {
|
|
39
|
+
const letter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
40
|
+
return letter + raw;
|
|
41
|
+
}
|
|
42
|
+
return raw;
|
|
37
43
|
}
|
|
38
44
|
function walkDown(node2, visit, parent = null, depth = 0) {
|
|
39
45
|
const keep = visit(node2, parent, depth);
|
|
@@ -207,7 +213,8 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
207
213
|
const absStart = innerStream.start ?? 0;
|
|
208
214
|
const absEnd = innerStream.end ?? result.duration;
|
|
209
215
|
const duration = absEnd - absStart;
|
|
210
|
-
const
|
|
216
|
+
const isBgNoEnd = innerStream.isBackground && innerStream.end == null;
|
|
217
|
+
const resetStream = isBgNoEnd ? { ...innerStream } : {
|
|
211
218
|
...innerStream,
|
|
212
219
|
start: 0,
|
|
213
220
|
end: duration,
|
|
@@ -224,28 +231,33 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
224
231
|
id: uid(),
|
|
225
232
|
type: "effect",
|
|
226
233
|
animation: spec.animation,
|
|
234
|
+
animationDurationSeconds: spec.duration,
|
|
227
235
|
durationInSeconds: spec.duration,
|
|
228
236
|
animationTimingFunction: spec.animationTimingFunction,
|
|
229
237
|
animationIterationCount: spec.animationIterationCount ?? 1,
|
|
230
238
|
customKeyframes: spec.customKeyframes,
|
|
231
239
|
children: [currentStream],
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
// For background inner nodes: propagate start/end as-is so parent
|
|
241
|
+
// back-propagation fills the correct scene duration. The effect's
|
|
242
|
+
// durationInSeconds (animation spec) controls animation timing.
|
|
243
|
+
start: isOutermost && isBgNoEnd ? innerStream.start : effStart,
|
|
244
|
+
end: isOutermost && isBgNoEnd ? void 0 : effEnd,
|
|
245
|
+
visible: innerStream.visible ?? true,
|
|
235
246
|
...pickOn(node2)
|
|
236
247
|
};
|
|
237
248
|
}
|
|
238
249
|
return { stream: currentStream, duration: result.duration };
|
|
239
250
|
}
|
|
240
251
|
function compileLeaf(node2, ctx, parentKind) {
|
|
241
|
-
const id = node2.id
|
|
252
|
+
const id = node2.id;
|
|
253
|
+
const hasExplicitId = node2.id != null;
|
|
242
254
|
const hasOwnDuration = typeof node2.duration === "number" || typeof node2.endAt === "number";
|
|
243
255
|
const isBgNoOwnTiming = node2.isBackground && !hasOwnDuration;
|
|
244
256
|
const start = isBgNoOwnTiming ? typeof node2.start === "number" ? node2.start : void 0 : parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
245
257
|
const duration = isBgNoOwnTiming ? void 0 : deriveLeafDuration(node2, ctx);
|
|
246
258
|
const end = duration != null ? start + duration : void 0;
|
|
247
259
|
const base = {
|
|
248
|
-
id,
|
|
260
|
+
...id ? { id } : {},
|
|
249
261
|
style: node2.style,
|
|
250
262
|
visible: node2.visible ?? true,
|
|
251
263
|
isBackground: node2.isBackground,
|
|
@@ -305,8 +317,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
305
317
|
const bindings = {};
|
|
306
318
|
for (const key of Object.keys(node2)) {
|
|
307
319
|
if (!KNOWN_COMPONENT_KEYS.has(key)) {
|
|
308
|
-
|
|
309
|
-
if (typeof val === "string") bindings[key] = val;
|
|
320
|
+
bindings[key] = node2[key];
|
|
310
321
|
}
|
|
311
322
|
}
|
|
312
323
|
const stream = {
|
|
@@ -407,13 +418,23 @@ function compileScene(node2, ctx, parentKind) {
|
|
|
407
418
|
];
|
|
408
419
|
const sceneContentDuration = sceneKind === "parallel" ? aggregateDuration(compiledChildren, "parallel") : aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
409
420
|
const localDuration = Math.max(node2.duration ?? 0, sceneContentDuration);
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
function backpropagate(stream2, dur) {
|
|
422
|
+
if (stream2.end == null) {
|
|
423
|
+
stream2.end = dur;
|
|
424
|
+
if (stream2.durationInSeconds == null) {
|
|
425
|
+
stream2.durationInSeconds = dur;
|
|
426
|
+
}
|
|
427
|
+
if (stream2.start == null) stream2.start = 0;
|
|
428
|
+
}
|
|
429
|
+
if (stream2.type === "effect" && Array.isArray(stream2.children)) {
|
|
430
|
+
for (const child of stream2.children) {
|
|
431
|
+
backpropagate(child, dur);
|
|
432
|
+
}
|
|
415
433
|
}
|
|
416
434
|
}
|
|
435
|
+
for (const c of compiledChildren) {
|
|
436
|
+
backpropagate(c.stream, localDuration);
|
|
437
|
+
}
|
|
417
438
|
const start = parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
418
439
|
const end = start + localDuration;
|
|
419
440
|
const stream = {
|
|
@@ -507,13 +528,23 @@ function compileContainer(node2, ctx, parentKind) {
|
|
|
507
528
|
const resolved = node2.type === "transitionSeries" ? resolveTransition(node2.transition, node2.transitionTime) : { name: "fade", time: 0.5 };
|
|
508
529
|
const children = compileChildren(node2.children, ctx, node2.type);
|
|
509
530
|
const duration = aggregateDuration(children, node2.type, resolved.time);
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
531
|
+
function backpropagate(stream2, dur) {
|
|
532
|
+
if (stream2.end == null) {
|
|
533
|
+
stream2.end = dur;
|
|
534
|
+
if (stream2.durationInSeconds == null) {
|
|
535
|
+
stream2.durationInSeconds = dur;
|
|
536
|
+
}
|
|
537
|
+
if (stream2.start == null) stream2.start = 0;
|
|
538
|
+
}
|
|
539
|
+
if (stream2.type === "effect" && Array.isArray(stream2.children)) {
|
|
540
|
+
for (const child of stream2.children) {
|
|
541
|
+
backpropagate(child, dur);
|
|
542
|
+
}
|
|
515
543
|
}
|
|
516
544
|
}
|
|
545
|
+
for (const c of children) {
|
|
546
|
+
backpropagate(c.stream, duration);
|
|
547
|
+
}
|
|
517
548
|
const stream = {
|
|
518
549
|
id,
|
|
519
550
|
type: "folder",
|
|
@@ -10122,6 +10153,7 @@ var TYPE_TOKENS = {
|
|
|
10122
10153
|
video: "video",
|
|
10123
10154
|
audio: "audio",
|
|
10124
10155
|
component: "component",
|
|
10156
|
+
event: "event",
|
|
10125
10157
|
rhythm: "rhythm",
|
|
10126
10158
|
include: "include",
|
|
10127
10159
|
map: "map",
|
|
@@ -10341,6 +10373,21 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10341
10373
|
preserveVariantAttrs(node2, attrs);
|
|
10342
10374
|
return node2;
|
|
10343
10375
|
}
|
|
10376
|
+
case "event": {
|
|
10377
|
+
const node2 = {
|
|
10378
|
+
type: "component",
|
|
10379
|
+
id: attrs.id,
|
|
10380
|
+
jsx: "",
|
|
10381
|
+
duration: attrs.duration,
|
|
10382
|
+
start: attrs.start,
|
|
10383
|
+
instruction: attrs.instruction,
|
|
10384
|
+
style: attrs.style,
|
|
10385
|
+
effects: attrs.effects,
|
|
10386
|
+
on: attrs.on
|
|
10387
|
+
};
|
|
10388
|
+
preserveVariantAttrs(node2, attrs);
|
|
10389
|
+
return node2;
|
|
10390
|
+
}
|
|
10344
10391
|
case "rhythm": {
|
|
10345
10392
|
const src = firstPositional ?? attrs.src;
|
|
10346
10393
|
if (!src) throw new DslError("rhythm requires src", ctx);
|
package/src/schema/index.ts
CHANGED
|
@@ -133,7 +133,7 @@ export type Image = z.infer<typeof image>;
|
|
|
133
133
|
export const component = base.extend({
|
|
134
134
|
type: z.literal("component").default("component"),
|
|
135
135
|
jsx: z.string().describe("usage JSX expression compiled at runtime; tag names resolved from imports"),
|
|
136
|
-
data: z.record(z.string(), z.
|
|
136
|
+
data: z.record(z.string(), z.unknown()).optional().describe("extra variables (e.g. from ~~~md source code fences) available in JSX scope"),
|
|
137
137
|
});
|
|
138
138
|
export type Component = z.infer<typeof component>;
|
|
139
139
|
|
|
@@ -143,6 +143,7 @@ export type Component = z.infer<typeof component>;
|
|
|
143
143
|
export const effect = base.extend({
|
|
144
144
|
type: z.literal("effect").default("effect"),
|
|
145
145
|
animation: z.string().optional().describe("builtin keyframe name or 'custom'"),
|
|
146
|
+
animationDurationSeconds: z.number().optional().describe("animation duration (separate from wrapper durationInSeconds which getDurationInSeconds may overwrite)"),
|
|
146
147
|
animationTimingFunction: z
|
|
147
148
|
.enum(["linear", "ease", "ease-in", "ease-out", "ease-in-out"])
|
|
148
149
|
.optional(),
|
package/src/types/Component.tsx
CHANGED
|
@@ -72,7 +72,30 @@ export function ComponentLeaf({ stream }: { stream: Component }) {
|
|
|
72
72
|
[stream.data, components, eventState],
|
|
73
73
|
);
|
|
74
74
|
|
|
75
|
-
if (!stream.jsx)
|
|
75
|
+
if (!stream.jsx) {
|
|
76
|
+
// Event-only stub: no JSX to render, but may fire events on `on`.
|
|
77
|
+
// Still needs EventAwareComponent for useFrameEvents to register
|
|
78
|
+
// and fire at the right frame.
|
|
79
|
+
const start = stream.start ?? 0;
|
|
80
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
81
|
+
const durFrames = Math.max(1, Math.floor(fps * (end - start)));
|
|
82
|
+
return (
|
|
83
|
+
<Sequence
|
|
84
|
+
durationInFrames={durFrames}
|
|
85
|
+
from={Math.floor(fps * start)}
|
|
86
|
+
layout="none"
|
|
87
|
+
>
|
|
88
|
+
<EventAwareComponent
|
|
89
|
+
jsx=""
|
|
90
|
+
components={components}
|
|
91
|
+
data={bindings}
|
|
92
|
+
action={{ start, end }}
|
|
93
|
+
durFrames={durFrames}
|
|
94
|
+
on={stream.on}
|
|
95
|
+
/>
|
|
96
|
+
</Sequence>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
76
99
|
|
|
77
100
|
const start = stream.start ?? 0;
|
|
78
101
|
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
@@ -118,6 +141,9 @@ function EventAwareComponent({
|
|
|
118
141
|
// Fire events at the right frame for this node's timeline
|
|
119
142
|
useFrameEvents(on, durFrames);
|
|
120
143
|
|
|
144
|
+
// If no JSX, this is an event-only stub — nothing to render
|
|
145
|
+
if (!jsx) return null;
|
|
146
|
+
|
|
121
147
|
return (
|
|
122
148
|
<TweenedJsxParser
|
|
123
149
|
jsx={jsx}
|
package/src/types/Effect.tsx
CHANGED
|
@@ -34,17 +34,24 @@ export function EffectWrapper({
|
|
|
34
34
|
const durationInFrames = end - start;
|
|
35
35
|
if (durationInFrames <= 0) return [] as Record<string, string>[];
|
|
36
36
|
|
|
37
|
+
// Use animationDurationSeconds for animation timing when available
|
|
38
|
+
// (set by wrapWithEffects for background nodes where end is set to parent
|
|
39
|
+
// duration but the animation spec duration is preserved separately).
|
|
40
|
+
const animDurationSec = stream.animationDurationSeconds ?? stream.durationInSeconds ?? (durationInFrames / fps);
|
|
41
|
+
const animDurationFrames = Math.ceil(animDurationSec * fps);
|
|
42
|
+
|
|
37
43
|
const animation = stream.animation;
|
|
38
44
|
const timingFn = stream.animationTimingFunction;
|
|
39
45
|
const iterCount = stream.animationIterationCount ?? 1;
|
|
40
46
|
const style = (cssJS(stream.style) ?? {}) as Record<string, string>;
|
|
41
47
|
|
|
42
|
-
// Handle iteration count: loop the animation within the span
|
|
48
|
+
// Handle iteration count: loop the animation within the span.
|
|
49
|
+
// The animation period is animDurationFrames (not the full span duration).
|
|
43
50
|
let currentFrame = frame;
|
|
44
|
-
if (iterCount > 0 &&
|
|
45
|
-
const iteration = Math.floor((frame - start) /
|
|
51
|
+
if (iterCount > 0 && animDurationFrames > 0) {
|
|
52
|
+
const iteration = Math.floor((frame - start) / animDurationFrames);
|
|
46
53
|
if (iteration < iterCount) {
|
|
47
|
-
currentFrame = start + ((frame - start) %
|
|
54
|
+
currentFrame = start + ((frame - start) % animDurationFrames);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -56,7 +63,7 @@ export function EffectWrapper({
|
|
|
56
63
|
if (config) {
|
|
57
64
|
const animStyle = interpolateKeyframes(config, actionFrame, {
|
|
58
65
|
fps,
|
|
59
|
-
durationInSeconds:
|
|
66
|
+
durationInSeconds: animDurationSec,
|
|
60
67
|
timingFunction: timingFn,
|
|
61
68
|
});
|
|
62
69
|
if (animStyle) Object.assign(style, animStyle);
|
|
@@ -65,7 +72,7 @@ export function EffectWrapper({
|
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
return Object.keys(style).length > 0 ? [style] : [];
|
|
68
|
-
}, [frame, fps, startSec, endSec, stream.animation, stream.animationTimingFunction, stream.animationIterationCount, stream.customKeyframes, stream.style]);
|
|
75
|
+
}, [frame, fps, startSec, endSec, stream.animation, stream.animationTimingFunction, stream.animationIterationCount, stream.customKeyframes, stream.style, stream.animationDurationSeconds, stream.durationInSeconds]);
|
|
69
76
|
|
|
70
77
|
if (styles.length === 0) return <>{children}</>;
|
|
71
78
|
|
package/src/types/Folder.tsx
CHANGED
|
@@ -62,7 +62,7 @@ export function FolderLeaf({ stream }: { stream: FolderStream }) {
|
|
|
62
62
|
// Background children are rendered outside the series (parallel overlays),
|
|
63
63
|
// so TransitionSeries doesn't reject the <Loop> wrapper.
|
|
64
64
|
const bgChildren = visibleChildren.filter((c) => c.isBackground);
|
|
65
|
-
const seriesChildren =
|
|
65
|
+
const seriesChildren = visibleChildren.filter((c) => !c.isBackground);
|
|
66
66
|
|
|
67
67
|
// When all non-background series children are audio, skip transitions to
|
|
68
68
|
// avoid audio overlap (both audio tracks play simultaneously during a fade).
|
package/src/utils/index.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export function uid(): string {
|
|
7
|
-
|
|
7
|
+
// Ensure the first character is a letter (valid JS identifier start)
|
|
8
|
+
const raw = Math.random().toString(36).slice(2, 10);
|
|
9
|
+
const first = raw[0]!;
|
|
10
|
+
// If first char is a digit, prepend a random letter
|
|
11
|
+
if (/^[0-9]/.test(first)) {
|
|
12
|
+
const letter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
13
|
+
return letter + raw;
|
|
14
|
+
}
|
|
15
|
+
return raw;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
const KEBAB = /[^a-zA-Z0-9_-]+/g;
|
|
@@ -135,7 +143,11 @@ export function getDurationInSeconds(stream: DurationStream, update = true): num
|
|
|
135
143
|
getDurationInSeconds(child, update);
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
// Effect wrappers encompass all children visually; background children
|
|
147
|
+
// still contribute to the effect's visible duration.
|
|
148
|
+
const visible = stream.type === "effect"
|
|
149
|
+
? stream.children
|
|
150
|
+
: stream.children.filter((c) => !c.isBackground);
|
|
139
151
|
if (stream.isSeries) {
|
|
140
152
|
const overlap = stream.transition ? (stream.transitionTime ?? 0.5) : 0;
|
|
141
153
|
for (let i = 0; i < visible.length; i++) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# video
|
|
2
|
+
seed:2660286133
|
|
3
|
+
width:640 height:480 fps:30 layout:series
|
|
4
|
+
|
|
5
|
+
## Title
|
|
6
|
+
layout:parallel
|
|
7
|
+
- component duration:3
|
|
8
|
+
~~~jsx jsx
|
|
9
|
+
<div style={{position:'absolute',top:0,left:0,width:640,height:480,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',background:'linear-gradient(135deg,#1a1a2e,#16213e)'}}>
|
|
10
|
+
<h1 style={{color:'#00d4ff',fontSize:28,margin:0}}>Animated Diagrams</h1>
|
|
11
|
+
<p style={{color:'#aaa',fontSize:16,marginTop:10}}>Mermaid Diagrams with Dynamic Highlight</p>
|
|
12
|
+
</div>
|
|
13
|
+
~~~
|
|
14
|
+
|
|
15
|
+
## FlowChart
|
|
16
|
+
layout:parallel
|
|
17
|
+
- component id:flowChart duration:12
|
|
18
|
+
~~~jsx
|
|
19
|
+
<div style={{position:'absolute',top:0,left:0,width:640,height:480,background:'#1a1a2e',padding:10,fontFamily:'monospace',boxSizing:'border-box',display:'flex',flexDirection:'column'}}>
|
|
20
|
+
<p style={{color:'#00d4ff',fontSize:12,textAlign:'center',margin:'0 0 4px 0',flexShrink:0}}>Flow — {highlight}</p>
|
|
21
|
+
<div style={{flex:1,display:'flex',justifyContent:'center',alignItems:'center',overflow:'hidden'}}>
|
|
22
|
+
<Mermaid highlight={highlight} animateEdges={animateEdges} theme='dark' source={mermaid}/>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
~~~
|
|
26
|
+
~~~mermaid
|
|
27
|
+
graph TD
|
|
28
|
+
A["Receive Request"] --> B["Validate Input"]
|
|
29
|
+
B --> C{"Valid?"}
|
|
30
|
+
C -->|Yes| D["Process Data"]
|
|
31
|
+
C -->|No| E["Return Error"]
|
|
32
|
+
D --> F["Format Response"]
|
|
33
|
+
F --> G["Send Response"]
|
|
34
|
+
classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px,color:#000
|
|
35
|
+
~~~
|
|
36
|
+
highlight:"A"
|
|
37
|
+
animateEdges:true
|
|
38
|
+
- event duration:3 start:3 on:(start, flowChart.highlight="B";flowChart.animateEdges=["B->C"])
|
|
39
|
+
- event duration:3 start:6 on:(start, flowChart.highlight="C")
|
|
40
|
+
- event duration:3 start:9 on:(start, flowChart.highlight=["D","G"])
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# video
|
|
2
|
+
seed:1793769080
|
|
3
|
+
width:640 height:480 fps:30 layout:series
|
|
4
|
+
|
|
5
|
+
## Title
|
|
6
|
+
layout:parallel
|
|
7
|
+
- component duration:3
|
|
8
|
+
~~~jsx jsx
|
|
9
|
+
<div style={{width:'100%',height:'100%',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',background:'linear-gradient(135deg,#0c0c1d,#1a1a3e)'}}>
|
|
10
|
+
<h1 style={{color:'#ffd700',fontSize:36,margin:0}}>Global Electricity Generation</h1>
|
|
11
|
+
<p style={{color:'#88ccff',fontSize:20,marginTop:10}}>1985 → 2025</p>
|
|
12
|
+
<p style={{color:'#aaa',fontSize:14,marginTop:20}}>Data: Ember / Energy Institute (Our World in Data)</p>
|
|
13
|
+
</div>
|
|
14
|
+
~~~
|
|
15
|
+
|
|
16
|
+
## ChinaSurge
|
|
17
|
+
layout:parallel
|
|
18
|
+
- component duration:6
|
|
19
|
+
~~~jsx jsx
|
|
20
|
+
<div style={{width:'100%',height:'100%',background:'#0c0c1d',padding:20,fontFamily:'Arial,sans-serif'}}>
|
|
21
|
+
<h2 style={{color:'#ffd700',fontSize:18,textAlign:'center',margin:'0 0 10px 0'}}>China's Electricity Generation (TWh)</h2>
|
|
22
|
+
<svg viewBox='0 0 580 380' width='100%' height='85%'>
|
|
23
|
+
{[1985,1990,1995,2000,2005,2010,2015,2020,2025].map((year,i)=>(
|
|
24
|
+
<g key={year}>
|
|
25
|
+
<rect x={20+i*62} y={350-tween(0,{1985:42,1990:63,1995:97,2000:137,2005:253,2010:425,2015:625,2020:786,2025:1069}[year])} width={42} height={tween(0,{1985:42,1990:63,1995:97,2000:137,2005:253,2010:425,2015:625,2020:786,2025:1069}[year])} fill='#ff4444' rx={3} />
|
|
26
|
+
<text x={20+i*62+21} y={365} textAnchor='middle' fill='#aaa' fontSize={9}>{year}</text>
|
|
27
|
+
<text x={20+i*62+21} y={345-tween(0,{1985:42,1990:63,1995:97,2000:137,2005:253,2010:425,2015:625,2020:786,2025:1069}[year])} textAnchor='middle' fill='#ff8888' fontSize={9}>{Math.round({1985:411,1990:621,1995:928,2000:1356,2005:2500,2010:4207,2015:6186,2020:7779,2025:10583}[year])}</text>
|
|
28
|
+
</g>
|
|
29
|
+
))}
|
|
30
|
+
<text x={10} y={20} fill='#666' fontSize={10}>TWh</text>
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
~~~
|
|
34
|
+
|
|
35
|
+
## World1985
|
|
36
|
+
layout:parallel
|
|
37
|
+
- component duration:5
|
|
38
|
+
~~~jsx jsx
|
|
39
|
+
<div style={{width:'100%',height:'100%',background:'#0c0c1d',padding:20,fontFamily:'Arial,sans-serif'}}>
|
|
40
|
+
<h2 style={{color:'#ffd700',fontSize:18,textAlign:'center',margin:'0 0 10px 0'}}>Electricity Generation in 1985 (TWh)</h2>
|
|
41
|
+
<svg viewBox='0 0 580 380' width='100%' height='85%'>
|
|
42
|
+
{[
|
|
43
|
+
{name:'USA',val:2657,color:'#4477ff',scale:266},
|
|
44
|
+
{name:'Russia',val:962,color:'#cc4444',scale:96},
|
|
45
|
+
{name:'Japan',val:672,color:'#44cc44',scale:67},
|
|
46
|
+
{name:'Germany',val:523,color:'#ffaa00',scale:52},
|
|
47
|
+
{name:'Canada',val:459,color:'#ff66aa',scale:46},
|
|
48
|
+
{name:'France',val:344,color:'#aa66ff',scale:34},
|
|
49
|
+
{name:'UK',val:298,color:'#66cccc',scale:30},
|
|
50
|
+
{name:'Brazil',val:194,color:'#66ff66',scale:19},
|
|
51
|
+
{name:'India',val:186,color:'#ff8844',scale:19},
|
|
52
|
+
{name:'China',val:411,color:'#ff4444',scale:41},
|
|
53
|
+
].map((c,i)=>(
|
|
54
|
+
<g key={c.name}>
|
|
55
|
+
<rect x={20+i*54} y={350-tween(0,c.scale)} width={40} height={tween(0,c.scale)} fill={c.color} rx={3}>
|
|
56
|
+
<animate attributeName='opacity' values='0;1' dur='2s' fill='freeze'/>
|
|
57
|
+
</rect>
|
|
58
|
+
<text x={20+i*54+20} y={365} textAnchor='middle' fill='#aaa' fontSize={9}>{c.name}</text>
|
|
59
|
+
<text x={20+i*54+20} y={345-tween(0,c.scale)} textAnchor='middle' fill='#fff' fontSize={9}>{c.val}</text>
|
|
60
|
+
</g>
|
|
61
|
+
))}
|
|
62
|
+
</svg>
|
|
63
|
+
</div>
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
## World2025
|
|
67
|
+
layout:parallel
|
|
68
|
+
- component duration:5
|
|
69
|
+
~~~jsx jsx
|
|
70
|
+
<div style={{width:'100%',height:'100%',background:'#0c0c1d',padding:20,fontFamily:'Arial,sans-serif'}}>
|
|
71
|
+
<h2 style={{color:'#ffd700',fontSize:18,textAlign:'center',margin:'0 0 10px 0'}}>Electricity Generation in 2025 (TWh)</h2>
|
|
72
|
+
<svg viewBox='0 0 580 380' width='100%' height='85%'>
|
|
73
|
+
{[
|
|
74
|
+
{name:'China',val:10583,color:'#ff4444',scale:280},
|
|
75
|
+
{name:'USA',val:4520,color:'#4477ff',scale:120},
|
|
76
|
+
{name:'India',val:2082,color:'#ff8844',scale:55},
|
|
77
|
+
{name:'Russia',val:1193,color:'#cc4444',scale:32},
|
|
78
|
+
{name:'Japan',val:1030,color:'#44cc44',scale:27},
|
|
79
|
+
{name:'Brazil',val:751,color:'#66ff66',scale:20},
|
|
80
|
+
{name:'Canada',val:652,color:'#ff66aa',scale:17},
|
|
81
|
+
{name:'S.Korea',val:625,color:'#66cccc',scale:17},
|
|
82
|
+
{name:'Germany',val:500,color:'#ffaa00',scale:13},
|
|
83
|
+
{name:'France',val:570,color:'#aa66ff',scale:15},
|
|
84
|
+
].map((c,i)=>(
|
|
85
|
+
<g key={c.name}>
|
|
86
|
+
<rect x={20+i*54} y={350-tween(0,c.scale)} width={40} height={tween(0,c.scale)} fill={c.color} rx={3}/>
|
|
87
|
+
<text x={20+i*54+20} y={365} textAnchor='middle' fill='#aaa' fontSize={9}>{c.name}</text>
|
|
88
|
+
<text x={20+i*54+20} y={345-tween(0,c.scale)} textAnchor='middle' fill='#fff' fontSize={9}>{c.val}</text>
|
|
89
|
+
</g>
|
|
90
|
+
))}
|
|
91
|
+
</svg>
|
|
92
|
+
</div>
|
|
93
|
+
~~~
|
|
94
|
+
|
|
95
|
+
## GrowthComparison
|
|
96
|
+
layout:parallel
|
|
97
|
+
- component duration:6
|
|
98
|
+
~~~jsx jsx
|
|
99
|
+
<div style={{width:'100%',height:'100%',background:'#0c0c1d',padding:20,fontFamily:'Arial,sans-serif'}}>
|
|
100
|
+
<h2 style={{color:'#ffd700',fontSize:18,textAlign:'center',margin:'0 0 10px 0'}}>Growth: 1985 → 2025 (TWh)</h2>
|
|
101
|
+
<svg viewBox='0 0 580 380' width='100%' height='85%'>
|
|
102
|
+
{[
|
|
103
|
+
{name:'China',v1985:411,v2025:10583,growth:'+10172',color:'#ff4444',s1985:11,s2025:280},
|
|
104
|
+
{name:'USA',v1985:2657,v2025:4520,growth:'+1863',color:'#4477ff',s1985:70,s2025:120},
|
|
105
|
+
{name:'India',v1985:186,v2025:2082,growth:'+1896',color:'#ff8844',s1985:5,s2025:55},
|
|
106
|
+
{name:'Brazil',v1985:194,v2025:751,growth:'+557',color:'#66ff66',s1985:5,s2025:20},
|
|
107
|
+
].map((c,i)=>(
|
|
108
|
+
<g key={c.name}>
|
|
109
|
+
<text x={20} y={50+i*80} fill='#aaa' fontSize={14}>{c.name}</text>
|
|
110
|
+
<rect x={20} y={58+i*80} width={tween(0,c.s1985)} height={14} fill={c.color} opacity={0.6} rx={2}/>
|
|
111
|
+
<rect x={20+tween(0,c.s1985)} y={58+i*80} width={tween(0,c.s2025-c.s1985)} height={14} fill={c.color} rx={2}/>
|
|
112
|
+
<text x={20+tween(0,c.s2025)+5} y={70+i*80} fill='#fff' fontSize={11}>{c.v1985} → {c.v2025}</text>
|
|
113
|
+
<text x={20} y={86+i*80} fill='#ffd700' fontSize={10}>Growth: {c.growth} TWh</text>
|
|
114
|
+
</g>
|
|
115
|
+
))}
|
|
116
|
+
<text x={20} y={370} fill='#666' fontSize={9}>1985 bar (dim) → 2025 bar (bright)</text>
|
|
117
|
+
</svg>
|
|
118
|
+
</div>
|
|
119
|
+
~~~
|
|
120
|
+
|
|
121
|
+
## Closing
|
|
122
|
+
layout:parallel
|
|
123
|
+
- component duration:3
|
|
124
|
+
~~~jsx jsx
|
|
125
|
+
<div style={{width:'100%',height:'100%',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',background:'linear-gradient(135deg,#0c0c1d,#1a1a3e)'}}>
|
|
126
|
+
<h2 style={{color:'#ffd700',fontSize:24,margin:0}}>China leads at 10,583 TWh</h2>
|
|
127
|
+
<p style={{color:'#88ccff',fontSize:16,marginTop:10}}>nearly 25x growth since 1985</p>
|
|
128
|
+
<p style={{color:'#aaa',fontSize:12,marginTop:30}}>Data: Our World in Data · Ember (2026) / Energy Institute (2025)</p>
|
|
129
|
+
</div>
|
|
130
|
+
~~~
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|