disaster-broadcast-studio 0.1.1 → 0.3.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 (69) hide show
  1. package/README.md +19 -7
  2. package/assets/README.md +1 -1
  3. package/assets/audio/README.md +16 -4
  4. package/assets/audio/alert-critical.mjs +4 -0
  5. package/assets/audio/alert-critical.wav +0 -0
  6. package/assets/audio/alert-critical.wav.json +15 -0
  7. package/assets/audio/alert-urgent.mjs +4 -0
  8. package/assets/audio/alert-urgent.wav +0 -0
  9. package/assets/audio/alert-urgent.wav.json +15 -0
  10. package/assets/audio/bulletin.mjs +9 -0
  11. package/assets/audio/bulletin.wav +0 -0
  12. package/assets/audio/bulletin.wav.json +15 -0
  13. package/assets/audio/news.mjs +4 -0
  14. package/assets/audio/news.wav +0 -0
  15. package/assets/audio/news.wav.json +15 -0
  16. package/assets/audio/notification.mjs +24 -9
  17. package/assets/audio/notification.wav +0 -0
  18. package/assets/audio/notification.wav.json +15 -0
  19. package/assets/audio/status.mjs +5 -0
  20. package/assets/audio/status.wav +0 -0
  21. package/assets/audio/status.wav.json +15 -0
  22. package/assets/audio/tones.mjs +17 -0
  23. package/assets/workspace/README.md +3 -0
  24. package/dist/cli.js +20 -1
  25. package/dist/credits.js +2 -2
  26. package/dist/load.js +8 -7
  27. package/dist/notifications.d.ts +40 -3
  28. package/dist/notifications.js +34 -6
  29. package/dist/preview/browser.d.ts +1 -0
  30. package/dist/preview/browser.js +87 -0
  31. package/dist/preview/http.d.ts +3 -0
  32. package/dist/preview/http.js +78 -0
  33. package/dist/preview/page.d.ts +1 -0
  34. package/dist/preview/page.js +30 -0
  35. package/dist/preview/project.d.ts +22 -0
  36. package/dist/preview/project.js +81 -0
  37. package/dist/preview/server.d.ts +6 -0
  38. package/dist/preview/server.js +57 -0
  39. package/dist/render.js +4 -40
  40. package/dist/scenario.d.ts +32 -4
  41. package/dist/scenario.js +59 -50
  42. package/dist/timeline.d.ts +68 -5
  43. package/dist/timeline.js +29 -9
  44. package/dist/video/Alerts.js +49 -49
  45. package/dist/video/Broadcast.js +82 -96
  46. package/dist/video/Bulletin.d.ts +2 -2
  47. package/dist/video/Bulletin.js +13 -18
  48. package/dist/video/Chat.d.ts +9 -0
  49. package/dist/video/Chat.js +34 -0
  50. package/dist/video/Information.d.ts +10 -0
  51. package/dist/video/Information.js +100 -0
  52. package/dist/video/Map.js +28 -12
  53. package/dist/video/TimeJump.d.ts +8 -0
  54. package/dist/video/TimeJump.js +42 -0
  55. package/dist/video/layout.d.ts +5 -0
  56. package/dist/video/layout.js +2 -0
  57. package/dist/video/prepare.d.ts +4 -0
  58. package/dist/video/prepare.js +47 -0
  59. package/dist/video/transitions.d.ts +609 -0
  60. package/dist/video/transitions.js +22 -0
  61. package/dist/video/useBundledFonts.js +16 -7
  62. package/package.json +2 -1
  63. package/skills/scenario-writing/SKILL.md +12 -5
  64. package/skills/scenario-writing/references/README.md +4 -4
  65. package/skills/scenario-writing/references/audio.md +7 -1
  66. package/skills/scenario-writing/references/direction.md +31 -7
  67. package/skills/scenario-writing/references/format.md +63 -12
  68. package/skills/scenario-writing/references/news-writing.md +10 -0
  69. package/skills/scenario-writing/references/status-systems.md +4 -2
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { clock, stateAt } from '../timeline.js';
3
+ import { numberFont } from './fonts.js';
4
+ import { changeProgress } from './transitions.js';
5
+ export const chatHeight = 260;
6
+ export function ChatPanel({ timeline, chat, frame, }) {
7
+ const progress = changeProgress(frame, chat.panel.start);
8
+ return (_jsxs("section", { "aria-label": chat.panel.title, style: {
9
+ height: chatHeight,
10
+ boxSizing: 'border-box',
11
+ flexShrink: 0,
12
+ display: 'flex',
13
+ flexDirection: 'column',
14
+ borderTop: '1px solid #495159',
15
+ background: '#0b0f12',
16
+ opacity: progress,
17
+ transform: `translateY(${12 * (1 - progress)}px)`,
18
+ }, children: [_jsxs("div", { style: {
19
+ height: 42,
20
+ flexShrink: 0,
21
+ display: 'flex',
22
+ alignItems: 'center',
23
+ gap: 12,
24
+ padding: '0 24px',
25
+ borderBottom: '1px solid #293238',
26
+ }, children: [_jsx("span", { style: { width: 6, height: 6, borderRadius: '50%', background: '#b46e68' } }), _jsx("span", { style: { fontSize: chat.panel.title.length > 20 ? 20 : 28, fontWeight: 600 }, children: chat.panel.title }), _jsx("span", { style: { marginLeft: 'auto', fontSize: 22, color: '#b2bdc5', letterSpacing: 1 }, children: "LIVE" })] }), _jsxs("div", { style: { position: 'relative', flex: 1, minHeight: 0, overflow: 'hidden' }, children: [chat.messages.length === 0 && (_jsx("div", { style: { padding: '16px 24px', color: '#b2bdc5', fontSize: 28 }, children: "\uC544\uC9C1 \uCC44\uD305\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })), _jsx("div", { style: { position: 'absolute', bottom: 12, left: 24, right: 24 }, children: chat.messages.slice(-12).map((message) => {
27
+ const arrival = changeProgress(frame, message.start, 18);
28
+ return (_jsx("div", { style: { display: 'grid', gridTemplateRows: `${arrival}fr` }, children: _jsx("div", { style: { minHeight: 0, overflow: 'hidden' }, children: _jsxs("article", { style: {
29
+ paddingTop: 8,
30
+ opacity: arrival,
31
+ transform: `translateY(${8 * (1 - arrival)}px)`,
32
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 3 }, children: [_jsx("span", { style: { fontSize: 24, color: message.color, overflowWrap: 'anywhere' }, children: message.author }), _jsx("time", { style: { fontSize: 22, color: '#abb8c2', fontFamily: numberFont, flexShrink: 0 }, children: clock(stateAt(timeline, message.start).epoch, timeline.scenario.timezone) })] }), _jsx("div", { style: { fontSize: 28, lineHeight: 1.35, overflowWrap: 'anywhere' }, children: message.text })] }) }) }, message.id));
33
+ }) })] })] }));
34
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { type Timeline } from '../scenario.js';
3
+ export declare function News({ timeline, frame }: {
4
+ timeline: Timeline;
5
+ frame: number;
6
+ }): React.JSX.Element;
7
+ export declare function Status({ timeline, frame }: {
8
+ timeline: Timeline;
9
+ frame: number;
10
+ }): React.JSX.Element;
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FPS } from '../scenario.js';
3
+ import { clock, notificationPulse, stateAt } from '../timeline.js';
4
+ import { numberFont } from './fonts.js';
5
+ import { changeFrames, changeProgress, transitionAt } from './transitions.js';
6
+ import { layout } from './layout.js';
7
+ export function News({ timeline, frame }) {
8
+ const { current, previous, progress } = transitionAt(timeline.events, 'news', frame);
9
+ return (_jsxs("div", { style: {
10
+ position: 'relative',
11
+ flex: 1,
12
+ alignSelf: 'stretch',
13
+ overflow: 'hidden',
14
+ }, children: [[previous, current].map((news, i) => {
15
+ if (!news)
16
+ return null;
17
+ return (_jsxs("div", { "aria-hidden": i === 0 || undefined, style: {
18
+ position: 'absolute',
19
+ inset: 0,
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ gap: 24,
23
+ opacity: i === 0 ? 1 - progress : progress,
24
+ transform: `translateY(${i === 0 ? -layout.footer * progress : layout.footer * (1 - progress)}px)`,
25
+ }, children: [_jsxs("div", { style: { width: 280, flexShrink: 0, overflowWrap: 'anywhere' }, children: [_jsx("div", { style: { fontSize: 28, color: '#c6d0d7', fontWeight: 600, lineHeight: 1.3 }, children: timeline.scenario.agencies[news.source] }), _jsx("div", { style: {
26
+ fontSize: 26,
27
+ color: '#b0bec8',
28
+ marginTop: 5,
29
+ fontFamily: numberFont,
30
+ fontVariantNumeric: 'tabular-nums',
31
+ }, children: clock(news.publishedAt
32
+ ? Date.parse(news.publishedAt)
33
+ : stateAt(timeline, news.start).epoch, timeline.scenario.timezone) })] }), _jsx("div", { style: {
34
+ fontSize: 40,
35
+ lineHeight: 1.3,
36
+ wordBreak: 'normal',
37
+ whiteSpace: 'pre-wrap',
38
+ minWidth: 0,
39
+ flex: 1,
40
+ overflowWrap: 'anywhere',
41
+ }, children: news.headline })] }, news.id));
42
+ }), _jsx("div", { style: {
43
+ position: 'absolute',
44
+ top: 0,
45
+ left: 0,
46
+ right: 0,
47
+ height: 2,
48
+ background: '#b3c0c9',
49
+ opacity: current ? (1 - changeProgress(frame, current.start, 60)) * 0.7 : 0,
50
+ } })] }));
51
+ }
52
+ export function Status({ timeline, frame }) {
53
+ const { current, previous, progress, start } = transitionAt(timeline.events, 'status', frame);
54
+ const pulse = notificationPulse(frame, start + changeFrames, start + changeFrames + 1.8 * FPS);
55
+ // Compare authored values, so unchanged badges do not animate on event replacement.
56
+ const before = timeline.events.find((e) => e.type === 'status' && e.end === current?.start);
57
+ const oldItems = before?.type === 'status' ? before.items : [];
58
+ const same = (a, b) => a.text === b.text && a.color === b.color;
59
+ const removed = previous?.items.filter((item) => !current?.items.some((next) => same(item, next))) ?? [];
60
+ const count = Math.max(current?.items.length ?? 0, previous?.items.length ?? 0);
61
+ return (_jsx("div", { style: { display: 'flex', alignItems: 'center', overflow: 'hidden', minWidth: 0 }, children: Array.from({ length: count }, (_, i) => {
62
+ const item = current?.items[i];
63
+ const old = previous?.items[i];
64
+ const outgoing = old && removed.includes(old) ? old : undefined;
65
+ const changed = item && !oldItems.some((old) => same(item, old));
66
+ return (_jsx("div", { style: {
67
+ display: 'grid',
68
+ flexShrink: 0,
69
+ gridTemplateColumns: `${item ? 1 : 1 - progress}fr`,
70
+ }, children: _jsx("div", { style: { display: 'grid', minWidth: 0, overflow: 'hidden' }, children: [outgoing, item].map((badge, layer) => badge && (_jsxs("span", { "aria-hidden": layer === 0 || undefined, style: {
71
+ gridArea: '1 / 1',
72
+ position: 'relative',
73
+ display: 'block',
74
+ marginRight: 14,
75
+ opacity: layer === 0 ? 1 - progress : changed ? progress * (1 - 0.6 * pulse) : 1,
76
+ transform: `translateY(${layer === 0 ? -24 * progress : changed ? 24 * (1 - progress) : 0}px)`,
77
+ border: `1px solid ${badge.color}`,
78
+ color: badge.color,
79
+ padding: '3px 10px',
80
+ fontSize: 28,
81
+ lineHeight: 1.3,
82
+ whiteSpace: 'nowrap',
83
+ background: `${badge.color}${Math.round((layer === 1 && changed
84
+ ? Math.max(1 - changeProgress(frame, start, 66), pulse)
85
+ : 0) * 96)
86
+ .toString(16)
87
+ .padStart(2, '0')}`,
88
+ }, children: [badge.text, layer === 1 && changed && (_jsx("span", { style: {
89
+ position: 'absolute',
90
+ left: 0,
91
+ right: 0,
92
+ bottom: 0,
93
+ height: 2,
94
+ background: badge.color,
95
+ transformOrigin: 'left',
96
+ transform: `scaleX(${progress})`,
97
+ opacity: Math.max(1 - changeProgress(frame, start, 66), pulse),
98
+ } }))] }, layer))) }) }, i));
99
+ }) }));
100
+ }
package/dist/video/Map.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import * as maplibregl from 'maplibre-gl';
4
- import { useDelayRender, cancelRender, staticFile } from 'remotion';
4
+ import { useBufferState, useDelayRender, useRemotionEnvironment, staticFile } from 'remotion';
5
5
  import { FPS } from '../scenario.js';
6
6
  import { regionOpacity } from '../timeline.js';
7
7
  const style = (countries, regions, provinces) => ({
@@ -89,27 +89,32 @@ export function BroadcastMap({ camera, countries, regions, provinces, events, fr
89
89
  const container = useRef(null), map = useRef(null);
90
90
  const snapshot = useRef(null), markers = useRef(null);
91
91
  const paths = useRef(new Map()), capturedCamera = useRef(''), completed = useRef('');
92
- const { delayRender, continueRender } = useDelayRender();
92
+ const { delayRender, continueRender, cancelRender } = useDelayRender();
93
+ const buffer = useBufferState();
94
+ const { isRendering } = useRemotionEnvironment();
93
95
  const pending = useRef(null);
94
96
  const [revision, setRevision] = useState(0);
95
97
  useLayoutEffect(() => { pending.current?.(); pending.current = null; }, [revision]);
96
98
  useEffect(() => {
97
99
  const handle = delayRender('Initializing offline map');
100
+ const playback = buffer.delayPlayback();
98
101
  maplibregl.setWorkerUrl(staticFile('maplibre-gl-worker.mjs'));
99
102
  let m;
100
103
  try {
101
104
  m = new maplibregl.Map({ container: container.current, style: style(countries, regions, provinces),
102
105
  ...camera, interactive: false, attributionControl: false, fadeDuration: 0,
103
- canvasContextAttributes: { alpha: true, preserveDrawingBuffer: true, antialias: true } });
106
+ canvasContextAttributes: { alpha: true, preserveDrawingBuffer: isRendering, antialias: true } });
104
107
  }
105
108
  catch (e) {
109
+ playback.unblock();
110
+ continueRender(handle);
106
111
  cancelRender(e);
107
112
  return;
108
113
  }
109
114
  map.current = m;
110
115
  m.on('error', e => cancelRender(new Error(`MapLibre: ${e.error.message}`)));
111
- m.once('load', () => { m.resize(); continueRender(handle); });
112
- return () => { map.current = null; m.remove(); };
116
+ m.once('load', () => { m.resize(); continueRender(handle); playback.unblock(); });
117
+ return () => { playback.unblock(); continueRender(handle); map.current = null; m.remove(); };
113
118
  }, []);
114
119
  const cameraKey = JSON.stringify(camera);
115
120
  const regionEvents = events.filter(e => e.type === 'region');
@@ -121,19 +126,23 @@ export function BroadcastMap({ camera, countries, regions, provinces, events, fr
121
126
  return;
122
127
  const handle = delayRender(`Projecting map frame ${frame}`);
123
128
  let settled = false, cancelled = false;
129
+ const playback = isRendering ? buffer.delayPlayback() : null;
124
130
  const finish = () => { if (!settled) {
125
131
  settled = true;
126
132
  continueRender(handle);
133
+ playback?.unblock();
127
134
  } };
128
135
  const project = () => {
129
136
  if (cancelled)
130
137
  return;
131
138
  try {
132
139
  if (capturedCamera.current !== cameraKey) {
133
- const canvas = snapshot.current;
134
- canvas.width = m.getCanvas().width;
135
- canvas.height = m.getCanvas().height;
136
- canvas.getContext('2d').drawImage(m.getCanvas(), 0, 0);
140
+ if (isRendering) {
141
+ const canvas = snapshot.current;
142
+ canvas.width = m.getCanvas().width;
143
+ canvas.height = m.getCanvas().height;
144
+ canvas.getContext('2d').drawImage(m.getCanvas(), 0, 0);
145
+ }
137
146
  capturedCamera.current = cameraKey;
138
147
  paths.current.clear();
139
148
  }
@@ -168,6 +177,13 @@ export function BroadcastMap({ camera, countries, regions, provinces, events, fr
168
177
  }
169
178
  };
170
179
  const update = () => {
180
+ // Playback lets MapLibre paint continuously. Waiting for idle and reading
181
+ // the WebGL canvas on every moving frame is only needed for file output.
182
+ if (!isRendering) {
183
+ m.jumpTo(camera);
184
+ project();
185
+ return;
186
+ }
171
187
  if (capturedCamera.current === cameraKey) {
172
188
  project();
173
189
  return;
@@ -182,7 +198,7 @@ export function BroadcastMap({ camera, countries, regions, provinces, events, fr
182
198
  m.once('load', update);
183
199
  return () => { cancelled = true; m.off('load', update); m.off('idle', project); finish(); };
184
200
  }, [requestKey]);
185
- return _jsxs("div", { style: { position: 'absolute', inset: 0, overflow: 'hidden' }, children: [_jsx("style", { children: '.maplibregl-canvas {position:absolute;left:0;top:0;}' }), _jsx("div", { ref: container, style: { position: 'absolute', inset: 0, opacity: 0, pointerEvents: 'none' } }), _jsx("canvas", { ref: snapshot, "aria-hidden": "true", style: { position: 'absolute', inset: 0, width: '100%', height: '100%' } }), _jsxs("svg", { width: "100%", height: "100%", style: { position: 'absolute', inset: 0, pointerEvents: 'none' }, children: [_jsx("g", { children: regionEvents.map(e => e.regions.map(id => _jsx("path", { d: paths.current.get(id), fill: e.color, fillRule: "evenodd", opacity: e.fillOpacity * regionOpacity(e, frame) }, `${e.id}-${id}`))) }), _jsx("g", { fill: "none", strokeLinejoin: "round", children: regionEvents.map(e => e.regions.map(id => _jsx("path", { d: paths.current.get(id), stroke: e.strokeColor ?? e.color, strokeWidth: e.strokeWidth, opacity: e.strokeOpacity * regionOpacity(e, frame, false) }, `${e.id}-${id}`))) })] }), _jsxs("div", { ref: markers, style: { position: 'absolute', inset: 0, pointerEvents: 'none' }, children: [_jsx("svg", { width: "100%", height: "100%", style: { position: 'absolute', inset: 0 }, children: events.map((e) => {
201
+ return _jsxs("div", { style: { position: 'absolute', inset: 0, overflow: 'hidden' }, children: [_jsx("style", { children: '.maplibregl-canvas {position:absolute;left:0;top:0;}' }), _jsx("div", { ref: container, style: { position: 'absolute', inset: 0, opacity: isRendering ? 0 : 1, pointerEvents: 'none' } }), isRendering && _jsx("canvas", { ref: snapshot, "aria-hidden": "true", style: { position: 'absolute', inset: 0, width: '100%', height: '100%' } }), _jsxs("svg", { width: "100%", height: "100%", style: { position: 'absolute', inset: 0, pointerEvents: 'none' }, children: [_jsx("g", { children: regionEvents.map(e => e.regions.map(id => _jsx("path", { d: paths.current.get(id), fill: e.color, fillRule: "evenodd", opacity: e.fillOpacity * regionOpacity(e, frame) }, `${e.id}-${id}`))) }), _jsx("g", { fill: "none", strokeLinejoin: "round", children: regionEvents.map(e => e.regions.map(id => _jsx("path", { d: paths.current.get(id), stroke: e.strokeColor ?? e.color, strokeWidth: e.strokeWidth, opacity: e.strokeOpacity * regionOpacity(e, frame, false) }, `${e.id}-${id}`))) })] }), _jsxs("div", { ref: markers, style: { position: 'absolute', inset: 0, pointerEvents: 'none' }, children: [_jsx("svg", { width: "100%", height: "100%", style: { position: 'absolute', inset: 0 }, children: events.map((e) => {
186
202
  if (e.type === 'zone')
187
203
  return (_jsx("polygon", { "data-geometry": JSON.stringify(e.points), fill: e.color, fillOpacity: e.opacity, stroke: e.color, strokeWidth: 1.5 }, e.id));
188
204
  if (e.type !== 'line')
@@ -204,7 +220,7 @@ export function BroadcastMap({ camera, countries, regions, provinces, events, fr
204
220
  background: '#0b0e10f2',
205
221
  padding: '6px 12px',
206
222
  borderLeft: `3px solid ${e.color}`,
207
- fontSize: 18,
223
+ fontSize: 28,
208
224
  color: e.color,
209
225
  whiteSpace: 'nowrap',
210
226
  }, children: e.label }, e.id));
@@ -226,7 +242,7 @@ export function BroadcastMap({ camera, countries, regions, provinces, events, fr
226
242
  top: '50%',
227
243
  left: 'calc(100% + 10px)',
228
244
  transform: 'translateY(-50%)',
229
- fontSize: 22,
245
+ fontSize: 32,
230
246
  fontWeight: 600,
231
247
  color: e.color,
232
248
  whiteSpace: 'nowrap',
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { stateAt } from '../timeline.js';
3
+ type Jump = ReturnType<typeof stateAt>['timeJump'];
4
+ export declare function timeJumpStrength(jump: Jump): number;
5
+ export declare function TimeJump({ jump }: {
6
+ jump: Jump;
7
+ }): React.JSX.Element | null;
8
+ export {};
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { layout } from './layout.js';
3
+ export function timeJumpStrength(jump) {
4
+ if (!jump)
5
+ return 0;
6
+ const ramp = Math.max(0, Math.min(1, jump.progress / 0.15, (1 - jump.progress) / 0.25));
7
+ return ramp * ramp * (3 - 2 * ramp);
8
+ }
9
+ // All motion comes from the requested frame, including direct seeks and stills.
10
+ export function TimeJump({ jump }) {
11
+ if (!jump)
12
+ return null;
13
+ const strength = timeJumpStrength(jump);
14
+ return (_jsxs(_Fragment, { children: [_jsx("svg", { width: 0, height: 0, "aria-hidden": "true", style: { position: 'absolute' }, children: _jsx("defs", { children: _jsx("filter", { id: "dbs-time-jump", x: "-5%", y: "-5%", width: "110%", height: "110%", children: _jsx("feGaussianBlur", { stdDeviation: `${5 * strength} ${0.3 * strength}` }) }) }) }), _jsxs("div", { "aria-hidden": "true", style: {
15
+ position: 'absolute',
16
+ inset: `${layout.header + 1}px 0 0`,
17
+ overflow: 'hidden',
18
+ pointerEvents: 'none',
19
+ opacity: strength,
20
+ }, children: [_jsx("div", { style: {
21
+ position: 'absolute',
22
+ inset: 0,
23
+ opacity: 0.22,
24
+ background: 'repeating-linear-gradient(to bottom, transparent 0px, transparent 3px, #c6d6df55 3px, #c6d6df55 4px, #0005 4px, #0005 6px)',
25
+ backgroundPositionY: (jump.elapsedFrames * 1.4) % 6,
26
+ } }), [0, 60].map((offset) => (_jsx("div", { style: {
27
+ position: 'absolute',
28
+ left: 0,
29
+ right: 0,
30
+ top: `${((jump.elapsedFrames * 2.8 + offset) % 120) - 10}%`,
31
+ height: '5%',
32
+ background: 'linear-gradient(to bottom, transparent, #dce8ed20 45%, #dce8ed66 50%, #0005 54%, transparent)',
33
+ } }, offset)))] }), _jsx("svg", { viewBox: "0 0 240 140", width: 240, height: 140, "aria-hidden": "true", style: {
34
+ position: 'absolute',
35
+ left: '50%',
36
+ top: '50%',
37
+ transform: 'translate(-50%, -50%)',
38
+ pointerEvents: 'none',
39
+ opacity: strength * 0.27,
40
+ filter: 'blur(2.5px)',
41
+ }, children: _jsx("path", { d: "M20 20 L104 70 L20 120 Z M122 20 L206 70 L122 120 Z", fill: "#e0e8ed" }) })] }));
42
+ }
@@ -0,0 +1,5 @@
1
+ export declare const layout: {
2
+ readonly header: 64;
3
+ readonly footer: 128;
4
+ readonly sidebar: 768;
5
+ };
@@ -0,0 +1,2 @@
1
+ // Composition pixels. At 854px landscape width, 40px text is approximately 18px.
2
+ export const layout = { header: 64, footer: 128, sidebar: 768 };
@@ -0,0 +1,4 @@
1
+ import type { Loaded } from '../load.js';
2
+ import type { BroadcastProps } from './Broadcast.js';
3
+ export declare function prepareStaticAssets(publicDir: string): Promise<void>;
4
+ export declare function prepareBroadcast(loaded: Loaded, publicDir: string, audioPrefix?: string): Promise<BroadcastProps>;
@@ -0,0 +1,47 @@
1
+ import { mkdir, copyFile, readFile, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { readBundledJson } from '../assets.js';
5
+ import { fonts } from './fonts.js';
6
+ const require = createRequire(import.meta.url);
7
+ // PNG, MP4 and browser playback share the same assets and composition props.
8
+ export async function prepareStaticAssets(publicDir) {
9
+ await mkdir(path.join(publicDir, 'fonts', 'files'), { recursive: true });
10
+ for (const name of ['maplibre-gl-worker.mjs', 'maplibre-gl-shared.mjs'])
11
+ await copyFile(require.resolve(`maplibre-gl/dist/${name}`), path.join(publicDir, name));
12
+ for (const font of fonts) {
13
+ const fontRoot = path.dirname(require.resolve(`${font.package}/package.json`));
14
+ for (const weight of font.weights) {
15
+ const css = await readFile(path.join(fontRoot, `${weight}.css`), 'utf8');
16
+ await writeFile(path.join(publicDir, 'fonts', `${font.id}-${weight}.css`), css);
17
+ for (const match of css.matchAll(/url\(([^)]+)\)/g)) {
18
+ const relative = match[1].replace(/['"]/g, '');
19
+ await copyFile(path.join(fontRoot, relative), path.join(publicDir, 'fonts', relative));
20
+ }
21
+ }
22
+ }
23
+ }
24
+ export async function prepareBroadcast(loaded, publicDir, audioPrefix = 'audio') {
25
+ const audio = {};
26
+ for (const [id, file] of Object.entries(loaded.assetPaths)) {
27
+ const name = `${audioPrefix}/${id}${path.extname(file)}`;
28
+ await mkdir(path.join(publicDir, audioPrefix), { recursive: true });
29
+ await copyFile(file, path.join(publicDir, name));
30
+ audio[id] = name;
31
+ }
32
+ const countries = await readBundledJson('countries.geojson.gz');
33
+ const defaultRegions = !loaded.timeline.scenario.map.regions;
34
+ // The full SGIS polygons supply the Korean coastline and administrative fills.
35
+ if (defaultRegions)
36
+ countries.features = countries.features.filter((f) => f.properties?.code !== 'KOR');
37
+ const provinces = defaultRegions
38
+ ? await readBundledJson('provinces.geojson.gz')
39
+ : { type: 'FeatureCollection', features: [] };
40
+ return {
41
+ timeline: loaded.timeline,
42
+ regions: loaded.regions,
43
+ countries,
44
+ provinces,
45
+ audio,
46
+ };
47
+ }