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
@@ -1,6 +1,36 @@
1
1
  import { type Timeline, type Camera, type CompiledEvent } from './scenario.js';
2
2
  export declare const active: (e: CompiledEvent, frame: number) => boolean;
3
+ export declare function chatAt(t: Timeline, frame: number): {
4
+ panel: {
5
+ type: "chat";
6
+ title: string;
7
+ duration: number;
8
+ carry: boolean;
9
+ id: string;
10
+ cut: string;
11
+ at: number;
12
+ } & {
13
+ start: number;
14
+ end: number;
15
+ order: number;
16
+ };
17
+ messages: ({
18
+ type: "chat-message";
19
+ chat: string;
20
+ author: string;
21
+ text: string;
22
+ color: string;
23
+ id: string;
24
+ cut: string;
25
+ at: number;
26
+ } & {
27
+ start: number;
28
+ end: number;
29
+ order: number;
30
+ })[];
31
+ } | null;
3
32
  export declare const mix: (a: number, b: number, t: number) => number;
33
+ export declare function notificationPulse(frame: number, start: number, end: number): number;
4
34
  export declare function cameraAt(t: Timeline, frame: number): Camera;
5
35
  export declare function receiptHighlights(t: Timeline, frame: number): Extract<CompiledEvent, {
6
36
  type: 'region';
@@ -8,11 +38,6 @@ export declare function receiptHighlights(t: Timeline, frame: number): Extract<C
8
38
  export declare function regionOpacity(e: Extract<CompiledEvent, {
9
39
  type: 'region';
10
40
  }>, frame: number, fill?: boolean): number;
11
- export declare function statusEntry(frame: number, start: number, duration?: number): {
12
- opacity: number;
13
- offset: number;
14
- glow: number;
15
- };
16
41
  export declare function shutdownAt(events: CompiledEvent[], frame: number): {
17
42
  y: number;
18
43
  x: number;
@@ -26,6 +51,10 @@ export declare function stateAt(t: Timeline, frame: number): {
26
51
  displayEpoch: number;
27
52
  jumping: boolean;
28
53
  clockPhase: string | null;
54
+ timeJump: {
55
+ progress: number;
56
+ elapsedFrames: number;
57
+ } | null;
29
58
  contentFrame: number;
30
59
  camera: {
31
60
  center: [number, number];
@@ -34,9 +63,43 @@ export declare function stateAt(t: Timeline, frame: number): {
34
63
  pitch: number;
35
64
  };
36
65
  alerts: CompiledEvent[];
66
+ chat: {
67
+ panel: {
68
+ type: "chat";
69
+ title: string;
70
+ duration: number;
71
+ carry: boolean;
72
+ id: string;
73
+ cut: string;
74
+ at: number;
75
+ } & {
76
+ start: number;
77
+ end: number;
78
+ order: number;
79
+ };
80
+ messages: ({
81
+ type: "chat-message";
82
+ chat: string;
83
+ author: string;
84
+ text: string;
85
+ color: string;
86
+ id: string;
87
+ cut: string;
88
+ at: number;
89
+ } & {
90
+ start: number;
91
+ end: number;
92
+ order: number;
93
+ })[];
94
+ } | null;
37
95
  visible: CompiledEvent[];
38
96
  news: ({
39
97
  type: "news";
98
+ sound: false | "default" | {
99
+ asset: string;
100
+ duration: number;
101
+ volume: number;
102
+ };
40
103
  source: string;
41
104
  headline: string;
42
105
  duration: number;
package/dist/timeline.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import { FPS } from './scenario.js';
2
2
  export const active = (e, frame) => e.start <= frame && frame < e.end;
3
+ export function chatAt(t, frame) {
4
+ const panel = t.events.find((e) => e.type === 'chat' && active(e, frame));
5
+ if (!panel || panel.type !== 'chat')
6
+ return null;
7
+ const messages = t.events.filter((e) => e.type === 'chat-message' && e.chat === panel.id && e.start <= frame);
8
+ return { panel, messages };
9
+ }
3
10
  export const mix = (a, b, t) => a + (b - a) * t;
11
+ // Three smooth pulses, bounded by the authored frame interval.
12
+ export function notificationPulse(frame, start, end) {
13
+ if (frame < start || frame >= end)
14
+ return 0;
15
+ return Math.sin(((frame - start) / (end - start)) * Math.PI * 3) ** 2;
16
+ }
4
17
  export function cameraAt(t, frame) {
5
18
  let cam = { ...t.scenario.map.camera };
6
19
  for (const e of t.events) {
@@ -21,7 +34,8 @@ export function receiptHighlights(t, frame) {
21
34
  return t.events.flatMap((e) => {
22
35
  if (e.type !== 'alert' || !e.mapHighlight || !e.regions.length)
23
36
  return [];
24
- const end = Math.min(t.durationInFrames, e.start + Math.round(e.mapHighlight.duration * FPS));
37
+ const highlightEnd = e.start + Math.round(e.mapHighlight.duration * FPS);
38
+ const end = Math.min(t.durationInFrames, highlightEnd);
25
39
  if (frame < e.start || frame >= end)
26
40
  return [];
27
41
  return [
@@ -37,13 +51,13 @@ export function receiptHighlights(t, frame) {
37
51
  order: e.order,
38
52
  regions: e.regions,
39
53
  color: e.mapHighlight.color,
40
- fillOpacity: 0.28,
54
+ fillOpacity: 0.28 * notificationPulse(frame, e.start, highlightEnd),
41
55
  strokeColor: e.mapHighlight.color,
42
56
  strokeOpacity: 0,
43
57
  strokeWidth: 0,
44
58
  pulse: false,
45
- fadeIn: Math.min(0.2, e.mapHighlight.duration * 0.2),
46
- fadeOut: Math.min(0.7, e.mapHighlight.duration * 0.4),
59
+ fadeIn: Math.min(0.1, e.mapHighlight.duration / 12),
60
+ fadeOut: Math.min(0.1, e.mapHighlight.duration / 12),
47
61
  },
48
62
  ];
49
63
  });
@@ -52,10 +66,6 @@ export function regionOpacity(e, frame, fill = true) {
52
66
  const fade = envelope(frame, e.start, e.end, e.fadeIn, e.fadeOut);
53
67
  return (fade * (fill && e.pulse ? 0.88 + 0.12 * Math.cos(((frame - e.start) / FPS) * Math.PI * 0.9) : 1));
54
68
  }
55
- export function statusEntry(frame, start, duration = 0.55) {
56
- const p = Math.max(0, Math.min(1, (frame - start) / (duration * FPS)));
57
- return { opacity: 0.4 + 0.6 * p, offset: (1 - p) * -5, glow: (1 - p) * 0.22 };
58
- }
59
69
  export function shutdownAt(events, frame) {
60
70
  const e = events.find((e) => e.type === 'screen' && e.effect === 'shutdown' && e.intensity > 0 && active(e, frame));
61
71
  if (!e)
@@ -76,12 +86,15 @@ export function stateAt(t, frame) {
76
86
  const jumping = index > 0 && transition > 0 && frame < cut.start + transition;
77
87
  const contentFrame = jumping ? cut.start - 1 : frame;
78
88
  const previous = index > 0 ? t.cuts[index - 1] : null;
89
+ const previousEnd = previous
90
+ ? previous.epoch + ((previous.end - previous.start) * 1000) / FPS
91
+ : cut.epoch;
79
92
  const epoch = cut.epoch + ((frame - cut.start) * 1000) / FPS;
80
93
  const p = jumping ? (frame - cut.start) / transition : 1;
81
94
  const clockPhase = jumping ? (p < 0.15 ? 'lead' : p < 0.75 ? 'run' : 'hold') : null;
82
95
  const roll = Math.max(0, Math.min(1, (p - 0.15) / 0.6));
83
96
  const displayEpoch = jumping
84
- ? mix(previous.epoch + ((previous.end - previous.start) * 1000) / FPS, cut.epoch + (transition * 1000) / FPS, roll)
97
+ ? mix(previousEnd, cut.epoch + (transition * 1000) / FPS, roll)
85
98
  : epoch;
86
99
  const visible = t.events.filter((e) => active(e, contentFrame));
87
100
  return {
@@ -91,9 +104,16 @@ export function stateAt(t, frame) {
91
104
  displayEpoch,
92
105
  jumping,
93
106
  clockPhase,
107
+ timeJump: jumping && cut.epoch > previousEnd
108
+ ? {
109
+ progress: (frame - cut.start) / Math.max(1, transition - 1),
110
+ elapsedFrames: frame - cut.start,
111
+ }
112
+ : null,
94
113
  contentFrame,
95
114
  camera: cameraAt(t, contentFrame),
96
115
  alerts: t.events.filter((e) => e.type === 'alert' && e.start <= contentFrame).reverse(),
116
+ chat: chatAt(t, contentFrame),
97
117
  visible,
98
118
  news: visible.find((e) => e.type === 'news'),
99
119
  opacity: Math.min(1, cut.fadeIn ? (frame - cut.start) / FPS / cut.fadeIn : 1, cut.fadeOut ? (cut.end - 1 - frame) / FPS / cut.fadeOut : 1),
@@ -1,62 +1,62 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { FPS, pages, alertText } from '../scenario.js';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { alertText } from '../scenario.js';
3
3
  import { stateAt, clock } from '../timeline.js';
4
4
  import { numberFont } from './fonts.js';
5
+ import { changeProgress } from './transitions.js';
5
6
  const colors = {
6
7
  안전안내: '#a0adb5',
7
8
  긴급재난: '#c2746e',
8
9
  위급재난: '#dd655f',
9
10
  };
10
- function AlertCard({ event, latest, frame, timeline, }) {
11
+ function AlertCard({ event, emphasis, frame, timeline, }) {
11
12
  if (event.type !== 'alert')
12
13
  return null;
13
- const chunks = pages(alertText(timeline.scenario.agencies[event.sender], event.text)), page = Math.floor((frame - event.start) / FPS / event.pageSeconds) % chunks.length;
14
+ const arrival = changeProgress(frame, event.start);
15
+ const latest = emphasis > 0;
14
16
  const received = stateAt(timeline, event.start).epoch;
15
- return (_jsxs("article", { style: {
16
- padding: latest ? '20px 24px 22px' : '17px 24px 20px',
17
- borderBottom: '1px solid #343a3f',
18
- background: latest ? '#131719' : '#0e1214',
19
- borderLeft: `2px solid ${latest ? colors[event.level] : '#434b51'}`,
20
- }, children: [_jsxs("div", { style: {
21
- display: 'flex',
22
- alignItems: 'center',
23
- gap: 10,
24
- fontSize: 17,
25
- color: latest ? colors[event.level] : '#8c979e',
26
- marginBottom: 12,
27
- }, children: [_jsxs("b", { style: {
28
- border: `1px solid ${latest ? colors[event.level] : '#4b565d'}`,
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',
35
- fontSize: 17,
36
- color: '#aab3b9',
37
- }, children: ["\uC218\uC2E0 ", clock(received, timeline.scenario.timezone)] })] }), (event.occurredAt || event.publishedAt) && (_jsxs("div", { style: { fontSize: 16, color: '#939fa7', marginBottom: 10 }, children: [event.occurredAt &&
38
- `발생 ${clock(Date.parse(event.occurredAt), timeline.scenario.timezone)} `, event.publishedAt &&
39
- `발표 ${clock(Date.parse(event.publishedAt), timeline.scenario.timezone)}`] })), _jsx("div", { style: {
40
- fontSize: latest ? 28 : 22,
41
- lineHeight: 1.65,
42
- whiteSpace: 'pre-wrap',
43
- wordBreak: 'keep-all',
44
- overflowWrap: 'anywhere',
45
- color: latest ? '#e0e4e7' : '#aab5bd',
46
- }, children: chunks[page] }), chunks.length > 1 && (_jsxs("div", { style: {
47
- fontFamily: numberFont,
48
- fontSize: 16,
49
- color: '#839099',
50
- marginTop: 12,
51
- textAlign: 'right',
52
- }, children: [page + 1, " / ", chunks.length] })), event.corrects && (_jsxs("div", { style: {
53
- fontSize: 16,
54
- color: '#a4afb6',
55
- marginTop: 12,
56
- borderTop: '1px solid #343a3f',
57
- paddingTop: 10,
58
- }, children: ["\uC774\uC804 \uBB38\uC790 \uC815\uC815 \u00B7", ' ', clock(stateAt(timeline, timeline.events.find((e) => e.id === event.corrects).start).epoch, timeline.scenario.timezone)] }))] }));
17
+ return (_jsx("div", { style: { display: 'grid', gridTemplateRows: `${arrival}fr` }, children: _jsx("div", { style: { minHeight: 0, overflow: 'hidden' }, children: _jsxs("article", { style: {
18
+ padding: '16px 24px',
19
+ opacity: arrival,
20
+ transform: `translateY(${-14 * (1 - arrival)}px)`,
21
+ boxShadow: `inset 4px 0 ${colors[event.level]}${Math.round((1 - changeProgress(frame, event.start, 66)) * 180)
22
+ .toString(16)
23
+ .padStart(2, '0')}`,
24
+ borderBottom: '1px solid #343a3f',
25
+ background: latest ? '#131719' : '#0e1214',
26
+ borderLeft: `2px solid ${colors[event.level]}`,
27
+ }, children: [_jsxs("div", { style: {
28
+ display: 'flex',
29
+ alignItems: 'center',
30
+ gap: 10,
31
+ fontSize: 28,
32
+ color: colors[event.level],
33
+ marginBottom: 10,
34
+ }, children: [_jsx("b", { style: {
35
+ border: `1px solid ${colors[event.level]}`,
36
+ padding: '2px 7px',
37
+ fontWeight: 400,
38
+ }, children: event.level }), _jsxs("span", { style: {
39
+ marginLeft: 'auto',
40
+ fontFamily: numberFont,
41
+ fontVariantNumeric: 'tabular-nums',
42
+ fontSize: 26,
43
+ color: '#aab3b9',
44
+ }, children: ["\uC218\uC2E0 ", clock(received, timeline.scenario.timezone)] })] }), (event.occurredAt || event.publishedAt) && (_jsxs("div", { style: { fontSize: 26, color: '#b0bac2', marginBottom: 8 }, children: [event.occurredAt &&
45
+ `발생 ${clock(Date.parse(event.occurredAt), timeline.scenario.timezone)} `, event.publishedAt &&
46
+ `발표 ${clock(Date.parse(event.publishedAt), timeline.scenario.timezone)}`] })), _jsx("div", { style: {
47
+ fontSize: 32 + 8 * emphasis,
48
+ lineHeight: 1.4,
49
+ whiteSpace: 'pre-wrap',
50
+ wordBreak: 'normal',
51
+ overflowWrap: 'anywhere',
52
+ color: latest ? '#edf0f2' : '#c0c9d0',
53
+ }, children: alertText(timeline.scenario.agencies[event.sender], event.text) })] }) }) }));
59
54
  }
60
55
  export function AlertHistory({ timeline, frame }) {
61
- return (_jsx(_Fragment, { children: stateAt(timeline, frame).alerts.map((event, i) => (_jsx(AlertCard, { event: event, latest: i === 0, frame: frame, timeline: timeline }, event.id))) }));
56
+ const alerts = stateAt(timeline, frame).alerts;
57
+ return (_jsx(_Fragment, { children: alerts.map((event, i) => (_jsx(AlertCard, { event: event, emphasis: i === 0
58
+ ? 1
59
+ : alerts[i - 1].start > event.start
60
+ ? 1 - changeProgress(frame, alerts[i - 1].start)
61
+ : 0, frame: frame, timeline: timeline }, event.id))) }));
62
62
  }
@@ -1,27 +1,31 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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, statusEntry, shutdownAt, } from '../timeline.js';
4
+ import { stateAt, clock, envelope, receiptHighlights, shutdownAt } from '../timeline.js';
5
5
  import { AlertHistory } from './Alerts.js';
6
+ import { ChatPanel } from './Chat.js';
6
7
  import { BroadcastMap } from './Map.js';
7
8
  import { SignalEffects, signalLevels } from './SignalEffects.js';
8
9
  import { AudioTrack } from './AudioTrack.js';
9
10
  import { Bulletin } from './Bulletin.js';
10
11
  import { useBundledFonts } from './useBundledFonts.js';
11
12
  import { bodyFont, numberFont } from './fonts.js';
13
+ import { News, Status } from './Information.js';
14
+ import { changeProgress, transitionAt } from './transitions.js';
15
+ import { layout } from './layout.js';
16
+ import { TimeJump, timeJumpStrength } from './TimeJump.js';
12
17
  export function Broadcast(props) {
13
18
  const { timeline, regions, countries, provinces, audio } = props;
14
19
  const openingFrames = 5 * FPS;
15
- const frame = useCurrentFrame(), state = stateAt(timeline, frame), news = state.news;
20
+ const frame = useCurrentFrame(), state = stateAt(timeline, frame);
16
21
  useBundledFonts(JSON.stringify(timeline.scenario));
17
- const status = state.visible.find((e) => e.type === 'status');
18
- const bulletin = state.visible.find((e) => e.type === 'bulletin');
22
+ const bulletins = transitionAt(timeline.events, 'bulletin', state.contentFrame);
19
23
  const inset = state.visible.find((e) => e.type === 'inset');
20
24
  const legend = state.visible.find((e) => e.type === 'legend');
21
25
  const signal = signalLevels(state.visible, frame);
22
26
  const mapEvents = [...state.visible, ...receiptHighlights(timeline, state.contentFrame)];
23
- const entry = statusEntry(state.contentFrame, status?.start ?? 0);
24
27
  const shutdown = shutdownAt(state.visible, frame);
28
+ const jumpStrength = timeJumpStrength(state.timeJump);
25
29
  return (_jsxs(AbsoluteFill, { style: {
26
30
  filter: signal.chromatic > 0 ? 'url(#dbs-chromatic)' : undefined,
27
31
  background: shutdown ? '#000' : '#080b0d',
@@ -34,30 +38,17 @@ export function Broadcast(props) {
34
38
  }, children: [_jsxs("header", { style: {
35
39
  position: 'absolute',
36
40
  inset: '0 0 auto 0',
37
- height: 48,
41
+ height: layout.header,
38
42
  display: 'flex',
39
43
  alignItems: 'center',
40
44
  padding: '0 26px',
41
45
  borderBottom: '1px solid #495159',
42
46
  background: '#0b0e10',
43
47
  gap: 14,
44
- }, children: [status?.type === 'status' &&
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: {
48
+ }, children: [_jsx(Status, { timeline: timeline, frame: state.contentFrame }), _jsx("time", { style: {
59
49
  marginLeft: 'auto',
60
- fontSize: 23,
50
+ flexShrink: 0,
51
+ fontSize: 32,
61
52
  fontFamily: numberFont,
62
53
  fontVariantNumeric: 'tabular-nums',
63
54
  color: state.jumping ? '#eef2f4' : '#bdc7ce',
@@ -68,81 +59,76 @@ export function Broadcast(props) {
68
59
  padding: '2px 8px',
69
60
  fontWeight: state.jumping ? 600 : 400,
70
61
  letterSpacing: 0.5,
71
- }, children: clock(state.displayEpoch, timeline.scenario.timezone, true) })] }), _jsxs("main", { style: {
72
- position: 'absolute',
73
- left: 0,
74
- top: 49,
75
- right: 640,
76
- bottom: 93,
77
- }, children: [_jsx(BroadcastMap, { camera: state.camera, countries: countries, regions: regions, provinces: provinces, events: mapEvents, frame: state.contentFrame }), legend?.type === 'legend' && (_jsx("div", { style: {
62
+ }, children: clock(state.displayEpoch, timeline.scenario.timezone, true) })] }), _jsxs(AbsoluteFill, { style: {
63
+ filter: jumpStrength > 0
64
+ ? `url(#dbs-time-jump) saturate(${1 - jumpStrength * 0.35})`
65
+ : undefined,
66
+ }, children: [_jsxs("main", { style: {
67
+ position: 'absolute',
68
+ left: 0,
69
+ top: layout.header + 1,
70
+ right: layout.sidebar,
71
+ bottom: layout.footer + 1,
72
+ }, children: [_jsx(BroadcastMap, { camera: state.camera, countries: countries, regions: regions, provinces: provinces, events: mapEvents, frame: state.contentFrame }), legend?.type === 'legend' && (_jsx("div", { style: {
73
+ position: 'absolute',
74
+ left: 24,
75
+ bottom: 24,
76
+ display: 'flex',
77
+ flexDirection: 'column',
78
+ gap: 10,
79
+ background: '#0b0e10ed',
80
+ padding: '14px 18px',
81
+ fontSize: 30,
82
+ color: '#d0d5da',
83
+ opacity: changeProgress(state.contentFrame, legend.start),
84
+ transform: `translateY(${10 * (1 - changeProgress(state.contentFrame, legend.start))}px)`,
85
+ }, children: legend.items.map((item, i) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12 }, children: [_jsx("span", { style: {
86
+ width: 23,
87
+ height: item.symbol === 'line' ? 0 : 16,
88
+ background: item.symbol === 'fill' ? item.color : 'transparent',
89
+ border: item.symbol === 'fill' ? 'none' : `2px solid ${item.color}`,
90
+ } }), _jsx("span", { children: item.text })] }, i))) })), inset?.type === 'inset' && (_jsxs("div", { style: {
91
+ position: 'absolute',
92
+ right: 20,
93
+ bottom: 20,
94
+ width: 420,
95
+ height: 290,
96
+ border: '1px solid #78858e',
97
+ opacity: changeProgress(state.contentFrame, inset.start),
98
+ transform: `translateY(${10 * (1 - changeProgress(state.contentFrame, inset.start))}px)`,
99
+ }, children: [_jsx(BroadcastMap, { camera: inset.camera, countries: countries, regions: regions, provinces: provinces, events: mapEvents, frame: state.contentFrame, inset: true }), _jsx("div", { style: {
100
+ position: 'absolute',
101
+ top: 0,
102
+ left: 0,
103
+ right: 0,
104
+ padding: '7px 12px',
105
+ background: '#0b0e10f2',
106
+ borderBottom: '1px solid #485159',
107
+ fontSize: 28,
108
+ }, children: inset.title })] }))] }), _jsxs("aside", { style: {
78
109
  position: 'absolute',
79
- left: 24,
80
- bottom: 24,
110
+ top: layout.header + 1,
111
+ right: 0,
112
+ bottom: layout.footer + 1,
113
+ width: layout.sidebar,
114
+ borderLeft: '1px solid #495159',
115
+ background: '#0c1012',
116
+ overflow: 'hidden',
81
117
  display: 'flex',
82
118
  flexDirection: 'column',
83
- gap: 10,
84
- background: '#0b0e10ed',
85
- padding: '14px 18px',
86
- fontSize: 21,
87
- color: '#d0d5da',
88
- }, children: legend.items.map((item, i) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12 }, children: [_jsx("span", { style: {
89
- width: 23,
90
- height: item.symbol === 'line' ? 0 : 16,
91
- background: item.symbol === 'fill' ? item.color : 'transparent',
92
- border: item.symbol === 'fill' ? 'none' : `2px solid ${item.color}`,
93
- } }), _jsx("span", { children: item.text })] }, i))) })), inset?.type === 'inset' && (_jsxs("div", { style: {
119
+ }, children: [_jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'hidden' }, children: _jsx(AlertHistory, { timeline: timeline, frame: state.contentFrame }) }), state.chat && _jsx(ChatPanel, { timeline: timeline, chat: state.chat, frame: state.contentFrame })] }), [bulletins.previous, bulletins.current].map((bulletin, i) => bulletin && (_jsx(Bulletin, { event: bulletin, source: timeline.scenario.agencies[bulletin.source], progress: i === 0 ? 1 - bulletins.progress : bulletins.progress }, bulletin.id))), _jsx("footer", { style: {
94
120
  position: 'absolute',
95
- right: 20,
96
- bottom: 20,
97
- width: 420,
98
- height: 290,
99
- border: '1px solid #78858e',
100
- }, children: [_jsx(BroadcastMap, { camera: inset.camera, countries: countries, regions: regions, provinces: provinces, events: mapEvents, frame: state.contentFrame, inset: true }), _jsx("div", { style: {
101
- position: 'absolute',
102
- top: 0,
103
- left: 0,
104
- right: 0,
105
- padding: '7px 12px',
106
- background: '#0b0e10f2',
107
- borderBottom: '1px solid #485159',
108
- fontSize: 17,
109
- }, children: inset.title })] }))] }), _jsx("aside", { style: {
110
- position: 'absolute',
111
- top: 49,
112
- right: 0,
113
- bottom: 93,
114
- width: 640,
115
- borderLeft: '1px solid #495159',
116
- background: '#0c1012',
117
- 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: {
119
- position: 'absolute',
120
- bottom: 0,
121
- left: 0,
122
- right: 0,
123
- height: 92,
124
- borderTop: '1px solid #606a72',
125
- display: 'flex',
126
- alignItems: 'center',
127
- background: '#0b0e10',
128
- padding: '0 26px',
129
- gap: 24,
130
- }, children: news?.type === 'news' && (_jsxs(_Fragment, { children: [_jsxs("div", { style: { minWidth: 135 }, children: [_jsx("div", { style: { fontSize: 18, color: '#b3c0c9', fontWeight: 600 }, children: timeline.scenario.agencies[news.source] }), _jsx("div", { style: {
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
+ bottom: 0,
122
+ left: 0,
123
+ right: 0,
124
+ height: layout.footer,
125
+ borderTop: '1px solid #606a72',
126
+ display: 'flex',
127
+ alignItems: 'center',
128
+ background: '#0b0e10',
129
+ padding: '0 26px',
130
+ gap: 24,
131
+ }, children: _jsx(News, { timeline: timeline, frame: state.contentFrame }) })] }), _jsx(TimeJump, { jump: state.timeJump })] }), state.visible
146
132
  .filter((e) => e.type === 'screen' && ['blackout', 'flash', 'interference'].includes(e.effect))
147
133
  .map((event) => {
148
134
  if (event.type !== 'screen')
@@ -173,14 +159,14 @@ export function Broadcast(props) {
173
159
  alignItems: 'center',
174
160
  opacity: frame < openingFrames - 30 ? 1 : (openingFrames - frame) / 30,
175
161
  }, children: [_jsx("div", { style: {
176
- fontSize: timeline.scenario.title.length > 40 ? 30 : 36,
162
+ fontSize: 54,
177
163
  fontWeight: 600,
178
164
  maxWidth: 1440,
179
165
  textAlign: 'center',
180
166
  overflowWrap: 'anywhere',
181
167
  lineHeight: 1.5,
182
168
  }, children: timeline.scenario.title }), _jsx("div", { style: {
183
- fontSize: 24,
169
+ fontSize: 36,
184
170
  color: '#bac3c9',
185
171
  marginTop: 22,
186
172
  maxWidth: 1200,
@@ -1,9 +1,9 @@
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, progress, }: {
4
4
  event: Extract<CompiledEvent, {
5
5
  type: 'bulletin';
6
6
  }>;
7
7
  source: string;
8
- frame: number;
8
+ progress?: number;
9
9
  }): React.JSX.Element;
@@ -1,35 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { FPS, pages } from '../scenario.js';
3
- import { numberFont } from './fonts.js';
4
- export function Bulletin({ event, source, frame, }) {
5
- const chunks = pages(event.text);
6
- const page = Math.floor((frame - event.start) / FPS / event.pageSeconds) % chunks.length;
2
+ import { layout } from './layout.js';
3
+ export function Bulletin({ event, source, progress = 1, }) {
7
4
  return (_jsxs("section", { style: {
8
5
  position: 'absolute',
9
- top: 49,
10
- bottom: 93,
6
+ top: layout.header + 1,
7
+ bottom: layout.footer + 1,
11
8
  left: 0,
12
9
  right: 0,
13
10
  background: '#0a0d0f',
11
+ opacity: progress,
12
+ transform: `translateY(${16 * (1 - progress)}px)`,
14
13
  display: 'flex',
15
14
  flexDirection: 'column',
16
15
  justifyContent: 'center',
17
16
  padding: '50px 190px',
18
- }, children: [_jsx("div", { style: { fontSize: 23, color: '#9ba5ac', marginBottom: 32 }, children: source }), _jsx("h2", { style: {
19
- fontSize: 46,
17
+ }, children: [_jsx("div", { style: { fontSize: 32, color: '#b8c3cb', marginBottom: 24 }, children: source }), _jsx("h2", { style: {
18
+ fontSize: 54,
20
19
  fontWeight: 600,
21
20
  color: event.color,
22
21
  margin: '0 0 32px',
23
22
  lineHeight: 1.5,
24
23
  }, children: event.heading }), _jsx("div", { style: {
25
- fontSize: 34,
26
- lineHeight: 1.9,
24
+ fontSize: 44,
25
+ lineHeight: 1.6,
27
26
  whiteSpace: 'pre-wrap',
28
- wordBreak: 'keep-all',
29
- }, children: chunks[page] }), chunks.length > 1 && (_jsxs("div", { style: {
30
- fontSize: 18,
31
- marginTop: 30,
32
- color: '#8c969d',
33
- fontFamily: numberFont,
34
- }, children: [page + 1, " / ", chunks.length] }))] }));
27
+ wordBreak: 'normal',
28
+ overflowWrap: 'anywhere',
29
+ }, children: event.text })] }));
35
30
  }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { Timeline } from '../scenario.js';
3
+ import { stateAt } from '../timeline.js';
4
+ export declare const chatHeight = 260;
5
+ export declare function ChatPanel({ timeline, chat, frame, }: {
6
+ timeline: Timeline;
7
+ chat: NonNullable<ReturnType<typeof stateAt>['chat']>;
8
+ frame: number;
9
+ }): React.JSX.Element;