disaster-broadcast-studio 0.1.1 → 0.2.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/README.md +10 -5
- package/assets/README.md +1 -1
- package/assets/audio/README.md +16 -4
- package/assets/audio/alert-critical.mjs +4 -0
- package/assets/audio/alert-critical.wav +0 -0
- package/assets/audio/alert-critical.wav.json +15 -0
- package/assets/audio/alert-urgent.mjs +4 -0
- package/assets/audio/alert-urgent.wav +0 -0
- package/assets/audio/alert-urgent.wav.json +15 -0
- package/assets/audio/bulletin.mjs +9 -0
- package/assets/audio/bulletin.wav +0 -0
- package/assets/audio/bulletin.wav.json +15 -0
- package/assets/audio/news.mjs +4 -0
- package/assets/audio/news.wav +0 -0
- package/assets/audio/news.wav.json +15 -0
- package/assets/audio/notification.mjs +24 -9
- package/assets/audio/notification.wav +0 -0
- package/assets/audio/notification.wav.json +15 -0
- package/assets/audio/status.mjs +5 -0
- package/assets/audio/status.wav +0 -0
- package/assets/audio/status.wav.json +15 -0
- package/assets/audio/tones.mjs +17 -0
- package/assets/workspace/README.md +3 -0
- package/dist/cli.js +19 -1
- package/dist/credits.js +2 -2
- package/dist/load.js +5 -3
- package/dist/notifications.d.ts +40 -3
- package/dist/notifications.js +34 -6
- package/dist/preview/browser.d.ts +1 -0
- package/dist/preview/browser.js +87 -0
- package/dist/preview/http.d.ts +3 -0
- package/dist/preview/http.js +78 -0
- package/dist/preview/page.d.ts +1 -0
- package/dist/preview/page.js +30 -0
- package/dist/preview/project.d.ts +22 -0
- package/dist/preview/project.js +81 -0
- package/dist/preview/server.d.ts +6 -0
- package/dist/preview/server.js +57 -0
- package/dist/render.js +4 -40
- package/dist/scenario.d.ts +15 -0
- package/dist/scenario.js +19 -15
- package/dist/timeline.d.ts +6 -5
- package/dist/timeline.js +11 -8
- package/dist/video/Alerts.js +63 -47
- package/dist/video/Broadcast.js +14 -38
- package/dist/video/Bulletin.d.ts +2 -1
- package/dist/video/Bulletin.js +8 -4
- package/dist/video/Information.d.ts +16 -0
- package/dist/video/Information.js +104 -0
- package/dist/video/Map.js +26 -10
- package/dist/video/prepare.d.ts +4 -0
- package/dist/video/prepare.js +47 -0
- package/dist/video/transitions.d.ts +562 -0
- package/dist/video/transitions.js +31 -0
- package/dist/video/useBundledFonts.js +15 -6
- package/package.json +2 -1
- package/skills/scenario-writing/SKILL.md +2 -2
- package/skills/scenario-writing/references/audio.md +1 -1
- package/skills/scenario-writing/references/format.md +16 -7
package/dist/video/Alerts.js
CHANGED
|
@@ -1,62 +1,78 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { pages, alertText } from '../scenario.js';
|
|
3
3
|
import { stateAt, clock } from '../timeline.js';
|
|
4
4
|
import { numberFont } from './fonts.js';
|
|
5
|
+
import { PagedText } from './Information.js';
|
|
6
|
+
import { changeProgress, pageAt } from './transitions.js';
|
|
5
7
|
const colors = {
|
|
6
8
|
안전안내: '#a0adb5',
|
|
7
9
|
긴급재난: '#c2746e',
|
|
8
10
|
위급재난: '#dd655f',
|
|
9
11
|
};
|
|
10
|
-
function AlertCard({ event,
|
|
12
|
+
function AlertCard({ event, emphasis, frame, timeline, }) {
|
|
11
13
|
if (event.type !== 'alert')
|
|
12
14
|
return null;
|
|
13
|
-
const chunks = pages(alertText(timeline.scenario.agencies[event.sender], event.text))
|
|
15
|
+
const chunks = pages(alertText(timeline.scenario.agencies[event.sender], event.text));
|
|
16
|
+
const { page } = pageAt(frame, event.start, event.pageSeconds, chunks.length);
|
|
17
|
+
const arrival = changeProgress(frame, event.start);
|
|
18
|
+
const latest = emphasis > 0;
|
|
14
19
|
const received = stateAt(timeline, event.start).epoch;
|
|
15
|
-
return (_jsxs("article", { style: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
padding: '2px 7px',
|
|
30
|
-
fontWeight: 400,
|
|
31
|
-
}, children: [event.corrects ? '정정 · ' : '', event.level] }), _jsxs("span", { style: {
|
|
32
|
-
marginLeft: 'auto',
|
|
33
|
-
fontFamily: numberFont,
|
|
34
|
-
fontVariantNumeric: 'tabular-nums',
|
|
20
|
+
return (_jsx("div", { style: { display: 'grid', gridTemplateRows: `${arrival}fr` }, children: _jsx("div", { style: { minHeight: 0, overflow: 'hidden' }, children: _jsxs("article", { style: {
|
|
21
|
+
padding: `${17 + 3 * emphasis}px 24px ${20 + 2 * emphasis}px`,
|
|
22
|
+
opacity: arrival,
|
|
23
|
+
transform: `translateY(${-14 * (1 - arrival)}px)`,
|
|
24
|
+
boxShadow: `inset 4px 0 ${colors[event.level]}${Math.round((1 - changeProgress(frame, event.start, 66)) * 180)
|
|
25
|
+
.toString(16)
|
|
26
|
+
.padStart(2, '0')}`,
|
|
27
|
+
borderBottom: '1px solid #343a3f',
|
|
28
|
+
background: latest ? '#131719' : '#0e1214',
|
|
29
|
+
borderLeft: `2px solid ${colors[event.level]}`,
|
|
30
|
+
}, children: [_jsxs("div", { style: {
|
|
31
|
+
display: 'flex',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
gap: 10,
|
|
35
34
|
fontSize: 17,
|
|
36
|
-
color:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
color: colors[event.level],
|
|
36
|
+
marginBottom: 12,
|
|
37
|
+
}, children: [_jsxs("b", { style: {
|
|
38
|
+
border: `1px solid ${colors[event.level]}`,
|
|
39
|
+
padding: '2px 7px',
|
|
40
|
+
fontWeight: 400,
|
|
41
|
+
}, children: [event.corrects ? '정정 · ' : '', event.level] }), _jsxs("span", { style: {
|
|
42
|
+
marginLeft: 'auto',
|
|
43
|
+
fontFamily: numberFont,
|
|
44
|
+
fontVariantNumeric: 'tabular-nums',
|
|
45
|
+
fontSize: 17,
|
|
46
|
+
color: '#aab3b9',
|
|
47
|
+
}, children: ["\uC218\uC2E0 ", clock(received, timeline.scenario.timezone)] })] }), (event.occurredAt || event.publishedAt) && (_jsxs("div", { style: { fontSize: 16, color: '#939fa7', marginBottom: 10 }, children: [event.occurredAt &&
|
|
48
|
+
`발생 ${clock(Date.parse(event.occurredAt), timeline.scenario.timezone)} `, event.publishedAt &&
|
|
49
|
+
`발표 ${clock(Date.parse(event.publishedAt), timeline.scenario.timezone)}`] })), _jsx("div", { style: {
|
|
50
|
+
fontSize: 22 + 6 * emphasis,
|
|
51
|
+
lineHeight: 1.65,
|
|
52
|
+
whiteSpace: 'pre-wrap',
|
|
53
|
+
wordBreak: 'keep-all',
|
|
54
|
+
overflowWrap: 'anywhere',
|
|
55
|
+
color: latest ? '#e0e4e7' : '#aab5bd',
|
|
56
|
+
}, children: _jsx(PagedText, { chunks: chunks, frame: frame, start: event.start, seconds: event.pageSeconds }) }), chunks.length > 1 && (_jsxs("div", { style: {
|
|
57
|
+
fontFamily: numberFont,
|
|
58
|
+
fontSize: 16,
|
|
59
|
+
color: '#839099',
|
|
60
|
+
marginTop: 12,
|
|
61
|
+
textAlign: 'right',
|
|
62
|
+
}, children: [page + 1, " / ", chunks.length] })), event.corrects && (_jsxs("div", { style: {
|
|
63
|
+
fontSize: 16,
|
|
64
|
+
color: '#a4afb6',
|
|
65
|
+
marginTop: 12,
|
|
66
|
+
borderTop: '1px solid #343a3f',
|
|
67
|
+
paddingTop: 10,
|
|
68
|
+
}, children: ["\uC774\uC804 \uBB38\uC790 \uC815\uC815 \u00B7", ' ', clock(stateAt(timeline, timeline.events.find((e) => e.id === event.corrects).start)
|
|
69
|
+
.epoch, timeline.scenario.timezone)] }))] }) }) }));
|
|
59
70
|
}
|
|
60
71
|
export function AlertHistory({ timeline, frame }) {
|
|
61
|
-
|
|
72
|
+
const alerts = stateAt(timeline, frame).alerts;
|
|
73
|
+
return (_jsx(_Fragment, { children: alerts.map((event, i) => (_jsx(AlertCard, { event: event, emphasis: i === 0
|
|
74
|
+
? 1
|
|
75
|
+
: alerts[i - 1].start > event.start
|
|
76
|
+
? 1 - changeProgress(frame, alerts[i - 1].start)
|
|
77
|
+
: 0, frame: frame, timeline: timeline }, event.id))) }));
|
|
62
78
|
}
|
package/dist/video/Broadcast.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AbsoluteFill, useCurrentFrame } from 'remotion';
|
|
3
3
|
import { FPS } from '../scenario.js';
|
|
4
|
-
import { stateAt, clock, envelope, receiptHighlights,
|
|
4
|
+
import { stateAt, clock, envelope, receiptHighlights, shutdownAt } from '../timeline.js';
|
|
5
5
|
import { AlertHistory } from './Alerts.js';
|
|
6
6
|
import { BroadcastMap } from './Map.js';
|
|
7
7
|
import { SignalEffects, signalLevels } from './SignalEffects.js';
|
|
@@ -9,18 +9,18 @@ import { AudioTrack } from './AudioTrack.js';
|
|
|
9
9
|
import { Bulletin } from './Bulletin.js';
|
|
10
10
|
import { useBundledFonts } from './useBundledFonts.js';
|
|
11
11
|
import { bodyFont, numberFont } from './fonts.js';
|
|
12
|
+
import { News, Status } from './Information.js';
|
|
13
|
+
import { changeProgress, transitionAt } from './transitions.js';
|
|
12
14
|
export function Broadcast(props) {
|
|
13
15
|
const { timeline, regions, countries, provinces, audio } = props;
|
|
14
16
|
const openingFrames = 5 * FPS;
|
|
15
|
-
const frame = useCurrentFrame(), state = stateAt(timeline, frame)
|
|
17
|
+
const frame = useCurrentFrame(), state = stateAt(timeline, frame);
|
|
16
18
|
useBundledFonts(JSON.stringify(timeline.scenario));
|
|
17
|
-
const
|
|
18
|
-
const bulletin = state.visible.find((e) => e.type === 'bulletin');
|
|
19
|
+
const bulletins = transitionAt(timeline.events, 'bulletin', state.contentFrame);
|
|
19
20
|
const inset = state.visible.find((e) => e.type === 'inset');
|
|
20
21
|
const legend = state.visible.find((e) => e.type === 'legend');
|
|
21
22
|
const signal = signalLevels(state.visible, frame);
|
|
22
23
|
const mapEvents = [...state.visible, ...receiptHighlights(timeline, state.contentFrame)];
|
|
23
|
-
const entry = statusEntry(state.contentFrame, status?.start ?? 0);
|
|
24
24
|
const shutdown = shutdownAt(state.visible, frame);
|
|
25
25
|
return (_jsxs(AbsoluteFill, { style: {
|
|
26
26
|
filter: signal.chromatic > 0 ? 'url(#dbs-chromatic)' : undefined,
|
|
@@ -41,22 +41,9 @@ export function Broadcast(props) {
|
|
|
41
41
|
borderBottom: '1px solid #495159',
|
|
42
42
|
background: '#0b0e10',
|
|
43
43
|
gap: 14,
|
|
44
|
-
}, children: [
|
|
45
|
-
status.items.map((item, i) => (_jsx("span", { style: {
|
|
46
|
-
opacity: entry.opacity,
|
|
47
|
-
transform: `translateY(${entry.offset}px)`,
|
|
48
|
-
background: `${item.color}${Math.round(entry.glow * 255)
|
|
49
|
-
.toString(16)
|
|
50
|
-
.padStart(2, '0')}`,
|
|
51
|
-
border: `1px solid ${item.color}`,
|
|
52
|
-
color: item.color,
|
|
53
|
-
padding: '3px 10px',
|
|
54
|
-
fontSize: 18,
|
|
55
|
-
lineHeight: 1.3,
|
|
56
|
-
whiteSpace: 'nowrap',
|
|
57
|
-
flexShrink: 0,
|
|
58
|
-
}, children: item.text }, `${status.id}-${i}`))), _jsx("time", { style: {
|
|
44
|
+
}, children: [_jsx(Status, { timeline: timeline, frame: state.contentFrame }), _jsx("time", { style: {
|
|
59
45
|
marginLeft: 'auto',
|
|
46
|
+
flexShrink: 0,
|
|
60
47
|
fontSize: 23,
|
|
61
48
|
fontFamily: numberFont,
|
|
62
49
|
fontVariantNumeric: 'tabular-nums',
|
|
@@ -85,6 +72,8 @@ export function Broadcast(props) {
|
|
|
85
72
|
padding: '14px 18px',
|
|
86
73
|
fontSize: 21,
|
|
87
74
|
color: '#d0d5da',
|
|
75
|
+
opacity: changeProgress(state.contentFrame, legend.start),
|
|
76
|
+
transform: `translateY(${10 * (1 - changeProgress(state.contentFrame, legend.start))}px)`,
|
|
88
77
|
}, children: legend.items.map((item, i) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12 }, children: [_jsx("span", { style: {
|
|
89
78
|
width: 23,
|
|
90
79
|
height: item.symbol === 'line' ? 0 : 16,
|
|
@@ -97,6 +86,8 @@ export function Broadcast(props) {
|
|
|
97
86
|
width: 420,
|
|
98
87
|
height: 290,
|
|
99
88
|
border: '1px solid #78858e',
|
|
89
|
+
opacity: changeProgress(state.contentFrame, inset.start),
|
|
90
|
+
transform: `translateY(${10 * (1 - changeProgress(state.contentFrame, inset.start))}px)`,
|
|
100
91
|
}, children: [_jsx(BroadcastMap, { camera: inset.camera, countries: countries, regions: regions, provinces: provinces, events: mapEvents, frame: state.contentFrame, inset: true }), _jsx("div", { style: {
|
|
101
92
|
position: 'absolute',
|
|
102
93
|
top: 0,
|
|
@@ -115,7 +106,7 @@ export function Broadcast(props) {
|
|
|
115
106
|
borderLeft: '1px solid #495159',
|
|
116
107
|
background: '#0c1012',
|
|
117
108
|
overflow: 'hidden',
|
|
118
|
-
}, children: _jsx(AlertHistory, { timeline: timeline, frame: state.contentFrame }) }), bulletin && (_jsx(Bulletin, { event: bulletin, source: timeline.scenario.agencies[bulletin.source], frame: state.contentFrame })), _jsx("footer", { style: {
|
|
109
|
+
}, children: _jsx(AlertHistory, { timeline: timeline, frame: state.contentFrame }) }), [bulletins.previous, bulletins.current].map((bulletin, i) => bulletin && (_jsx(Bulletin, { event: bulletin, source: timeline.scenario.agencies[bulletin.source], frame: i === 0 ? Math.min(state.contentFrame, bulletin.end - 1) : state.contentFrame, progress: i === 0 ? 1 - bulletins.progress : bulletins.progress }, bulletin.id))), _jsx("footer", { style: {
|
|
119
110
|
position: 'absolute',
|
|
120
111
|
bottom: 0,
|
|
121
112
|
left: 0,
|
|
@@ -127,22 +118,7 @@ export function Broadcast(props) {
|
|
|
127
118
|
background: '#0b0e10',
|
|
128
119
|
padding: '0 26px',
|
|
129
120
|
gap: 24,
|
|
130
|
-
}, children:
|
|
131
|
-
fontSize: 16,
|
|
132
|
-
color: '#85959f',
|
|
133
|
-
marginTop: 5,
|
|
134
|
-
fontFamily: numberFont,
|
|
135
|
-
fontVariantNumeric: 'tabular-nums',
|
|
136
|
-
}, children: clock(news.publishedAt
|
|
137
|
-
? Date.parse(news.publishedAt)
|
|
138
|
-
: stateAt(timeline, news.start).epoch, timeline.scenario.timezone) })] }), _jsx("div", { style: {
|
|
139
|
-
fontSize: news.headline.length > 120 ? 23 : news.headline.length > 85 ? 25 : 30,
|
|
140
|
-
lineHeight: 1.3,
|
|
141
|
-
fontWeight: 400,
|
|
142
|
-
wordBreak: 'keep-all',
|
|
143
|
-
overflowWrap: 'anywhere',
|
|
144
|
-
opacity: Math.min(1, (frame - news.start + 1) / 12),
|
|
145
|
-
}, children: news.headline })] })) })] }), state.visible
|
|
121
|
+
}, children: _jsx(News, { timeline: timeline, frame: state.contentFrame }) })] }), state.visible
|
|
146
122
|
.filter((e) => e.type === 'screen' && ['blackout', 'flash', 'interference'].includes(e.effect))
|
|
147
123
|
.map((event) => {
|
|
148
124
|
if (event.type !== 'screen')
|
package/dist/video/Bulletin.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { CompiledEvent } from '../scenario.js';
|
|
3
|
-
export declare function Bulletin({ event, source, frame, }: {
|
|
3
|
+
export declare function Bulletin({ event, source, frame, progress, }: {
|
|
4
4
|
event: Extract<CompiledEvent, {
|
|
5
5
|
type: 'bulletin';
|
|
6
6
|
}>;
|
|
7
7
|
source: string;
|
|
8
8
|
frame: number;
|
|
9
|
+
progress?: number;
|
|
9
10
|
}): React.JSX.Element;
|
package/dist/video/Bulletin.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { pages } from '../scenario.js';
|
|
3
3
|
import { numberFont } from './fonts.js';
|
|
4
|
-
|
|
4
|
+
import { PagedText } from './Information.js';
|
|
5
|
+
import { pageAt } from './transitions.js';
|
|
6
|
+
export function Bulletin({ event, source, frame, progress = 1, }) {
|
|
5
7
|
const chunks = pages(event.text);
|
|
6
|
-
const page =
|
|
8
|
+
const { page } = pageAt(frame, event.start, event.pageSeconds, chunks.length);
|
|
7
9
|
return (_jsxs("section", { style: {
|
|
8
10
|
position: 'absolute',
|
|
9
11
|
top: 49,
|
|
@@ -11,6 +13,8 @@ export function Bulletin({ event, source, frame, }) {
|
|
|
11
13
|
left: 0,
|
|
12
14
|
right: 0,
|
|
13
15
|
background: '#0a0d0f',
|
|
16
|
+
opacity: progress,
|
|
17
|
+
transform: `translateY(${16 * (1 - progress)}px)`,
|
|
14
18
|
display: 'flex',
|
|
15
19
|
flexDirection: 'column',
|
|
16
20
|
justifyContent: 'center',
|
|
@@ -26,7 +30,7 @@ export function Bulletin({ event, source, frame, }) {
|
|
|
26
30
|
lineHeight: 1.9,
|
|
27
31
|
whiteSpace: 'pre-wrap',
|
|
28
32
|
wordBreak: 'keep-all',
|
|
29
|
-
}, children: chunks
|
|
33
|
+
}, children: _jsx(PagedText, { chunks: chunks, frame: frame, start: event.start, seconds: event.pageSeconds }) }), chunks.length > 1 && (_jsxs("div", { style: {
|
|
30
34
|
fontSize: 18,
|
|
31
35
|
marginTop: 30,
|
|
32
36
|
color: '#8c969d',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type Timeline } from '../scenario.js';
|
|
3
|
+
export declare function PagedText({ chunks, frame, start, seconds, }: {
|
|
4
|
+
chunks: string[];
|
|
5
|
+
frame: number;
|
|
6
|
+
start: number;
|
|
7
|
+
seconds: number;
|
|
8
|
+
}): React.JSX.Element;
|
|
9
|
+
export declare function News({ timeline, frame }: {
|
|
10
|
+
timeline: Timeline;
|
|
11
|
+
frame: number;
|
|
12
|
+
}): React.JSX.Element;
|
|
13
|
+
export declare function Status({ timeline, frame }: {
|
|
14
|
+
timeline: Timeline;
|
|
15
|
+
frame: number;
|
|
16
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,104 @@
|
|
|
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, pageAt, transitionAt } from './transitions.js';
|
|
6
|
+
export function PagedText({ chunks, frame, start, seconds, }) {
|
|
7
|
+
const { page, previous, progress } = pageAt(frame, start, seconds, chunks.length);
|
|
8
|
+
return (_jsxs("div", { style: { display: 'grid', overflow: 'hidden' }, children: [previous !== undefined && progress < 1 && (_jsx("div", { "aria-hidden": true, style: {
|
|
9
|
+
gridArea: '1 / 1',
|
|
10
|
+
opacity: 1 - progress,
|
|
11
|
+
transform: `translateY(${-12 * progress}px)`,
|
|
12
|
+
}, children: chunks[previous] })), _jsx("div", { style: {
|
|
13
|
+
gridArea: '1 / 1',
|
|
14
|
+
opacity: progress,
|
|
15
|
+
transform: `translateY(${12 * (1 - progress)}px)`,
|
|
16
|
+
}, children: chunks[page] })] }));
|
|
17
|
+
}
|
|
18
|
+
export function News({ timeline, frame }) {
|
|
19
|
+
const { current, previous, progress } = transitionAt(timeline.events, 'news', frame);
|
|
20
|
+
return (_jsxs("div", { style: {
|
|
21
|
+
position: 'relative',
|
|
22
|
+
flex: 1,
|
|
23
|
+
alignSelf: 'stretch',
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
}, children: [[previous, current].map((news, i) => news && (_jsxs("div", { "aria-hidden": i === 0 || undefined, style: {
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
inset: 0,
|
|
28
|
+
display: 'flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
gap: 24,
|
|
31
|
+
opacity: i === 0 ? 1 - progress : progress,
|
|
32
|
+
transform: `translateY(${i === 0 ? -92 * progress : 92 * (1 - progress)}px)`,
|
|
33
|
+
}, children: [_jsxs("div", { style: { minWidth: 135, flexShrink: 0 }, children: [_jsx("div", { style: { fontSize: 18, color: '#b3c0c9', fontWeight: 600 }, children: timeline.scenario.agencies[news.source] }), _jsx("div", { style: {
|
|
34
|
+
fontSize: 16,
|
|
35
|
+
color: '#85959f',
|
|
36
|
+
marginTop: 5,
|
|
37
|
+
fontFamily: numberFont,
|
|
38
|
+
fontVariantNumeric: 'tabular-nums',
|
|
39
|
+
}, children: clock(news.publishedAt
|
|
40
|
+
? Date.parse(news.publishedAt)
|
|
41
|
+
: stateAt(timeline, news.start).epoch, timeline.scenario.timezone) })] }), _jsx("div", { style: {
|
|
42
|
+
fontSize: news.headline.length > 120 ? 23 : news.headline.length > 85 ? 25 : 30,
|
|
43
|
+
lineHeight: 1.3,
|
|
44
|
+
wordBreak: 'keep-all',
|
|
45
|
+
overflowWrap: 'anywhere',
|
|
46
|
+
}, children: news.headline })] }, news.id))), _jsx("div", { style: {
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
top: 0,
|
|
49
|
+
left: 0,
|
|
50
|
+
right: 0,
|
|
51
|
+
height: 2,
|
|
52
|
+
background: '#b3c0c9',
|
|
53
|
+
opacity: current ? (1 - changeProgress(frame, current.start, 60)) * 0.7 : 0,
|
|
54
|
+
} })] }));
|
|
55
|
+
}
|
|
56
|
+
export function Status({ timeline, frame }) {
|
|
57
|
+
const { current, previous, progress, start } = transitionAt(timeline.events, 'status', frame);
|
|
58
|
+
const pulse = notificationPulse(frame, start + changeFrames, start + changeFrames + 1.8 * FPS);
|
|
59
|
+
// Compare authored values, so unchanged badges do not animate on event replacement.
|
|
60
|
+
const before = timeline.events.find((e) => e.type === 'status' && e.end === current?.start);
|
|
61
|
+
const oldItems = before?.type === 'status' ? before.items : [];
|
|
62
|
+
const same = (a, b) => a.text === b.text && a.color === b.color;
|
|
63
|
+
const removed = previous?.items.filter((item) => !current?.items.some((next) => same(item, next))) ?? [];
|
|
64
|
+
const count = Math.max(current?.items.length ?? 0, previous?.items.length ?? 0);
|
|
65
|
+
return (_jsx("div", { style: { display: 'flex', alignItems: 'center', overflow: 'hidden', minWidth: 0 }, children: Array.from({ length: count }, (_, i) => {
|
|
66
|
+
const item = current?.items[i];
|
|
67
|
+
const old = previous?.items[i];
|
|
68
|
+
const outgoing = old && removed.includes(old) ? old : undefined;
|
|
69
|
+
const changed = item && !oldItems.some((old) => same(item, old));
|
|
70
|
+
return (_jsx("div", { style: {
|
|
71
|
+
display: 'grid',
|
|
72
|
+
flexShrink: 0,
|
|
73
|
+
gridTemplateColumns: `${item ? 1 : 1 - progress}fr`,
|
|
74
|
+
}, 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: {
|
|
75
|
+
gridArea: '1 / 1',
|
|
76
|
+
position: 'relative',
|
|
77
|
+
display: 'block',
|
|
78
|
+
marginRight: 14,
|
|
79
|
+
opacity: layer === 0 ? 1 - progress : changed ? progress * (1 - 0.6 * pulse) : 1,
|
|
80
|
+
transform: `translateY(${layer === 0 ? -24 * progress : changed ? 24 * (1 - progress) : 0}px)`,
|
|
81
|
+
border: `1px solid ${badge.color}`,
|
|
82
|
+
color: badge.color,
|
|
83
|
+
padding: '3px 10px',
|
|
84
|
+
fontSize: 18,
|
|
85
|
+
lineHeight: 1.3,
|
|
86
|
+
whiteSpace: 'nowrap',
|
|
87
|
+
background: `${badge.color}${Math.round((layer === 1 && changed
|
|
88
|
+
? Math.max(1 - changeProgress(frame, start, 66), pulse)
|
|
89
|
+
: 0) * 96)
|
|
90
|
+
.toString(16)
|
|
91
|
+
.padStart(2, '0')}`,
|
|
92
|
+
}, children: [badge.text, layer === 1 && changed && (_jsx("span", { style: {
|
|
93
|
+
position: 'absolute',
|
|
94
|
+
left: 0,
|
|
95
|
+
right: 0,
|
|
96
|
+
bottom: 0,
|
|
97
|
+
height: 2,
|
|
98
|
+
background: badge.color,
|
|
99
|
+
transformOrigin: 'left',
|
|
100
|
+
transform: `scaleX(${progress})`,
|
|
101
|
+
opacity: Math.max(1 - changeProgress(frame, start, 66), pulse),
|
|
102
|
+
} }))] }, layer))) }) }, i));
|
|
103
|
+
}) }));
|
|
104
|
+
}
|
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,
|
|
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:
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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')
|
|
@@ -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
|
+
}
|