@xcelsior/demo-pipeline 0.1.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.
@@ -0,0 +1,268 @@
1
+ import React from 'react';
2
+ import {
3
+ AbsoluteFill,
4
+ OffthreadVideo,
5
+ Audio,
6
+ Sequence,
7
+ staticFile,
8
+ useCurrentFrame,
9
+ useVideoConfig,
10
+ interpolate,
11
+ } from 'remotion';
12
+ import { BRAND } from '../scenes';
13
+ import { FONT } from '../ui';
14
+ import { Cursor, Ripples } from './Cursor';
15
+ import {
16
+ WEB_OFFSET,
17
+ WEB_W,
18
+ WEB_H,
19
+ WEB,
20
+ WEB_BEATS,
21
+ MOBILE_BEATS,
22
+ MOBILE_DUR,
23
+ MOBILE_CAM,
24
+ kf,
25
+ } from './data';
26
+
27
+ const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
28
+
29
+ const BeatCaption: React.FC<{ text: string; start: number; dur: number }> = ({
30
+ text,
31
+ start,
32
+ dur,
33
+ }) => {
34
+ const frame = useCurrentFrame();
35
+ const { fps } = useVideoConfig();
36
+ const f = frame - start * fps;
37
+ const D = dur * fps;
38
+ if (f < 0 || f > D) return null;
39
+ const o = interpolate(f, [0, 10, D - 12, D], [0, 1, 1, 0], {
40
+ extrapolateLeft: 'clamp',
41
+ extrapolateRight: 'clamp',
42
+ });
43
+ const y = interpolate(f, [0, 12], [20, 0], { extrapolateRight: 'clamp' });
44
+ return (
45
+ <div
46
+ style={{
47
+ position: 'absolute',
48
+ bottom: 56,
49
+ left: 0,
50
+ right: 0,
51
+ display: 'flex',
52
+ justifyContent: 'center',
53
+ opacity: o,
54
+ transform: `translateY(${y}px)`,
55
+ zIndex: 60,
56
+ }}
57
+ >
58
+ <div
59
+ style={{
60
+ background: 'linear-gradient(180deg, rgba(11,18,32,0.82), rgba(7,11,20,0.9))',
61
+ border: '1px solid rgba(255,255,255,0.10)',
62
+ borderRadius: 16,
63
+ padding: '16px 34px',
64
+ fontFamily: FONT,
65
+ color: BRAND.text,
66
+ fontSize: 36,
67
+ fontWeight: 600,
68
+ boxShadow: '0 16px 44px rgba(0,0,0,0.5)',
69
+ }}
70
+ >
71
+ {text}
72
+ </div>
73
+ </div>
74
+ );
75
+ };
76
+
77
+ const Beats: React.FC<{
78
+ beats: { start: number; dur: number; vo: string; cap: string }[];
79
+ withVo: boolean;
80
+ }> = ({ beats, withVo }) => {
81
+ const { fps } = useVideoConfig();
82
+ return (
83
+ <>
84
+ {beats.map((b, i) => (
85
+ <React.Fragment key={i}>
86
+ <BeatCaption text={b.cap} start={b.start} dur={b.dur} />
87
+ {withVo && (
88
+ <Sequence from={Math.round(b.start * fps)}>
89
+ <Audio src={staticFile('run/vo/' + b.vo + '.wav')} />
90
+ </Sequence>
91
+ )}
92
+ </React.Fragment>
93
+ ))}
94
+ </>
95
+ );
96
+ };
97
+
98
+ export const WebSegment: React.FC<{ withVo: boolean; framed?: boolean; cursorRate?: number }> = ({
99
+ withVo,
100
+ framed = false,
101
+ cursorRate = 1,
102
+ }) => {
103
+ const frame = useCurrentFrame();
104
+ const { fps } = useVideoConfig();
105
+ const t = frame / fps;
106
+ const s = kf(t, WEB.camT, WEB.camS);
107
+ const cx = kf(t, WEB.camT, WEB.camX);
108
+ const cy = kf(t, WEB.camT, WEB.camY);
109
+ const tx = clamp(WEB_W / 2 - cx * s, WEB_W - WEB_W * s, 0);
110
+ const ty = clamp(WEB_H / 2 - cy * s, WEB_H - WEB_H * s, 0);
111
+ const ct = t / cursorRate;
112
+ const cux = kf(ct, WEB.curT, WEB.curX);
113
+ const cuy = kf(ct, WEB.curT, WEB.curY);
114
+ const clicks =
115
+ cursorRate === 1 ? WEB.clicks : WEB.clicks.map((c) => ({ ...c, t: c.t * cursorRate }));
116
+ const videoLayer = (
117
+ <div
118
+ style={{
119
+ position: 'absolute',
120
+ width: WEB_W,
121
+ height: WEB_H,
122
+ transform: `translate(${tx}px, ${ty}px) scale(${s})`,
123
+ transformOrigin: '0 0',
124
+ }}
125
+ >
126
+ <OffthreadVideo
127
+ src={staticFile('run/web.webm')}
128
+ trimBefore={Math.round(WEB_OFFSET * fps)}
129
+ muted
130
+ style={{ width: WEB_W, height: WEB_H }}
131
+ />
132
+ <Ripples clicks={clicks} camScale={s} />
133
+ <Cursor x={cux} y={cuy} camScale={s} />
134
+ </div>
135
+ );
136
+ if (framed) {
137
+ const k = 0.84;
138
+ const IW = WEB_W * k,
139
+ IH = WEB_H * k;
140
+ return (
141
+ <AbsoluteFill
142
+ style={{
143
+ background: 'linear-gradient(135deg, #1c2c4d 0%, #0c1322 55%, #0a0f1a 100%)',
144
+ }}
145
+ >
146
+ <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
147
+ <div
148
+ style={{
149
+ width: IW,
150
+ height: IH,
151
+ borderRadius: 22,
152
+ overflow: 'hidden',
153
+ boxShadow: '0 50px 120px rgba(0,0,0,0.55)',
154
+ border: '1px solid rgba(255,255,255,0.10)',
155
+ position: 'relative',
156
+ }}
157
+ >
158
+ <div
159
+ style={{
160
+ position: 'absolute',
161
+ width: WEB_W,
162
+ height: WEB_H,
163
+ transform: `scale(${k})`,
164
+ transformOrigin: '0 0',
165
+ }}
166
+ >
167
+ {videoLayer}
168
+ </div>
169
+ </div>
170
+ </AbsoluteFill>
171
+ <Beats beats={WEB_BEATS} withVo={withVo} />
172
+ </AbsoluteFill>
173
+ );
174
+ }
175
+ return (
176
+ <AbsoluteFill style={{ background: '#05070C' }}>
177
+ <AbsoluteFill style={{ overflow: 'hidden' }}>{videoLayer}</AbsoluteFill>
178
+ <Beats beats={WEB_BEATS} withVo={withVo} />
179
+ </AbsoluteFill>
180
+ );
181
+ };
182
+
183
+ export const MobileSegment: React.FC<{ withVo: boolean; sync?: boolean }> = ({
184
+ withVo,
185
+ sync = false,
186
+ }) => {
187
+ const frame = useCurrentFrame();
188
+ const { fps } = useVideoConfig();
189
+ const t = frame / fps;
190
+ const FRAME_H = 1010;
191
+ const FRAME_W = Math.round(FRAME_H * (1206 / 2622));
192
+ const k = FRAME_H / 2622;
193
+ let s: number, cx: number, cy: number;
194
+ if (sync) {
195
+ s = kf(t, MOBILE_CAM.T, MOBILE_CAM.S);
196
+ cx = kf(t, MOBILE_CAM.T, MOBILE_CAM.X);
197
+ cy = kf(t, MOBILE_CAM.T, MOBILE_CAM.Y);
198
+ } else {
199
+ s = interpolate(t, [0, MOBILE_DUR], [1.02, 1.08], { extrapolateRight: 'clamp' });
200
+ cx = 603;
201
+ cy = 1311;
202
+ }
203
+ const tx = clamp(FRAME_W / 2 - cx * k * s, FRAME_W - FRAME_W * s, 0);
204
+ const ty = clamp(FRAME_H / 2 - cy * k * s, FRAME_H - FRAME_H * s, 0);
205
+ return (
206
+ <AbsoluteFill style={{ background: BRAND.darker }}>
207
+ <AbsoluteFill>
208
+ <OffthreadVideo
209
+ src={staticFile('run/mobile.mov')}
210
+ muted
211
+ style={{
212
+ width: '100%',
213
+ height: '100%',
214
+ objectFit: 'cover',
215
+ filter: 'blur(70px) brightness(0.3)',
216
+ transform: 'scale(1.2)',
217
+ }}
218
+ />
219
+ </AbsoluteFill>
220
+ <AbsoluteFill
221
+ style={{
222
+ background:
223
+ 'radial-gradient(900px 700px at 50% 45%, rgba(7,11,20,0.15), rgba(7,11,20,0.8))',
224
+ }}
225
+ />
226
+ <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
227
+ <div
228
+ style={{
229
+ width: FRAME_W + 22,
230
+ height: FRAME_H + 22,
231
+ background: '#05070C',
232
+ borderRadius: 56,
233
+ padding: 11,
234
+ boxShadow: '0 40px 90px rgba(0,0,0,0.6)',
235
+ border: '1px solid rgba(255,255,255,0.08)',
236
+ }}
237
+ >
238
+ <div
239
+ style={{
240
+ width: FRAME_W,
241
+ height: FRAME_H,
242
+ borderRadius: 46,
243
+ overflow: 'hidden',
244
+ background: '#000',
245
+ }}
246
+ >
247
+ <div
248
+ style={{
249
+ position: 'absolute',
250
+ width: FRAME_W,
251
+ height: FRAME_H,
252
+ transform: `translate(${tx}px, ${ty}px) scale(${s})`,
253
+ transformOrigin: '0 0',
254
+ }}
255
+ >
256
+ <OffthreadVideo
257
+ src={staticFile('run/mobile.mov')}
258
+ muted
259
+ style={{ width: FRAME_W, height: FRAME_H, objectFit: 'cover' }}
260
+ />
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </AbsoluteFill>
265
+ <Beats beats={MOBILE_BEATS} withVo={withVo} />
266
+ </AbsoluteFill>
267
+ );
268
+ };
@@ -0,0 +1,38 @@
1
+ {
2
+ "s0_intro": {
3
+ "text": "Load and Go. From dispatch, to delivery.",
4
+ "dur": 2.475
5
+ },
6
+ "s1_daysheet": {
7
+ "text": "It starts on the Day Sheet. Every job for the day, in one grid.",
8
+ "dur": 3.669
9
+ },
10
+ "s2_create": {
11
+ "text": "Add a job inline. Customer, loading time, pickup and delivery, with address lookup as you type.",
12
+ "dur": 5.867
13
+ },
14
+ "s3_details": {
15
+ "text": "Site contacts and job details, right in the row.",
16
+ "dur": 2.987
17
+ },
18
+ "s4_crew": {
19
+ "text": "Allocate the crew. Driver and truck, straight from the row.",
20
+ "dur": 3.243
21
+ },
22
+ "s5_publish": {
23
+ "text": "Publish, and it goes straight to the driver's phone.",
24
+ "dur": 2.517
25
+ },
26
+ "s6_push": {
27
+ "text": "On the truck, an instant notification. A new job.",
28
+ "dur": 3.072
29
+ },
30
+ "s7_jobdet": {
31
+ "text": "They open it to the full picture. Pickup, delivery, load, and contacts.",
32
+ "dur": 4.267
33
+ },
34
+ "s8_outro": {
35
+ "text": "Load and Go. One platform, from dispatch to delivery.",
36
+ "dur": 3.413
37
+ }
38
+ }
@@ -0,0 +1,274 @@
1
+ {
2
+ "video": "raw/web.webm",
3
+ "w": 1920,
4
+ "h": 1080,
5
+ "video_offset": 8.022,
6
+ "events": [
7
+ {
8
+ "t": 0.0,
9
+ "kind": "scene",
10
+ "x": null,
11
+ "y": null,
12
+ "label": "daysheet",
13
+ "box": null
14
+ },
15
+ {
16
+ "t": 0.812,
17
+ "kind": "move",
18
+ "x": 633,
19
+ "y": 425,
20
+ "label": "Customer",
21
+ "box": {
22
+ "x": 533,
23
+ "y": 408,
24
+ "w": 200,
25
+ "h": 34
26
+ }
27
+ },
28
+ {
29
+ "t": 0.885,
30
+ "kind": "click",
31
+ "x": 633,
32
+ "y": 425,
33
+ "label": "open Customer",
34
+ "box": {
35
+ "x": 533,
36
+ "y": 408,
37
+ "w": 200,
38
+ "h": 34
39
+ }
40
+ },
41
+ {
42
+ "t": 3.748,
43
+ "kind": "move",
44
+ "x": 806,
45
+ "y": 426,
46
+ "label": "Loading Time",
47
+ "box": {
48
+ "x": 733,
49
+ "y": 408,
50
+ "w": 146,
51
+ "h": 36
52
+ }
53
+ },
54
+ {
55
+ "t": 3.803,
56
+ "kind": "click",
57
+ "x": 806,
58
+ "y": 426,
59
+ "label": "open Loading Time",
60
+ "box": {
61
+ "x": 733,
62
+ "y": 408,
63
+ "w": 146,
64
+ "h": 36
65
+ }
66
+ },
67
+ {
68
+ "t": 4.518,
69
+ "kind": "move",
70
+ "x": 990,
71
+ "y": 426,
72
+ "label": "Pickup",
73
+ "box": {
74
+ "x": 880,
75
+ "y": 408,
76
+ "w": 220,
77
+ "h": 36
78
+ }
79
+ },
80
+ {
81
+ "t": 4.603,
82
+ "kind": "click",
83
+ "x": 990,
84
+ "y": 426,
85
+ "label": "open Pickup",
86
+ "box": {
87
+ "x": 880,
88
+ "y": 408,
89
+ "w": 220,
90
+ "h": 36
91
+ }
92
+ },
93
+ {
94
+ "t": 8.688,
95
+ "kind": "move",
96
+ "x": 1363,
97
+ "y": 426,
98
+ "label": "Delivery",
99
+ "box": {
100
+ "x": 1253,
101
+ "y": 408,
102
+ "w": 220,
103
+ "h": 36
104
+ }
105
+ },
106
+ {
107
+ "t": 8.74,
108
+ "kind": "click",
109
+ "x": 1397,
110
+ "y": 426,
111
+ "label": "open Delivery",
112
+ "box": {
113
+ "x": 1287,
114
+ "y": 408,
115
+ "w": 220,
116
+ "h": 36
117
+ }
118
+ },
119
+ {
120
+ "t": 12.68,
121
+ "kind": "scene",
122
+ "x": null,
123
+ "y": null,
124
+ "label": "sitecontacts",
125
+ "box": null
126
+ },
127
+ {
128
+ "t": 12.683,
129
+ "kind": "click",
130
+ "x": 117,
131
+ "y": 426,
132
+ "label": "expand details",
133
+ "box": {
134
+ "x": 108,
135
+ "y": 417,
136
+ "w": 18,
137
+ "h": 18
138
+ }
139
+ },
140
+ {
141
+ "t": 13.417,
142
+ "kind": "move",
143
+ "x": 306,
144
+ "y": 690,
145
+ "label": "Pickup site contact",
146
+ "box": {
147
+ "x": 126,
148
+ "y": 673,
149
+ "w": 360,
150
+ "h": 34
151
+ }
152
+ },
153
+ {
154
+ "t": 15.448,
155
+ "kind": "move",
156
+ "x": 682,
157
+ "y": 690,
158
+ "label": "Delivery site contact",
159
+ "box": {
160
+ "x": 502,
161
+ "y": 673,
162
+ "w": 360,
163
+ "h": 34
164
+ }
165
+ },
166
+ {
167
+ "t": 18.163,
168
+ "kind": "scene",
169
+ "x": null,
170
+ "y": null,
171
+ "label": "crew",
172
+ "box": null
173
+ },
174
+ {
175
+ "t": 18.185,
176
+ "kind": "click",
177
+ "x": 1091,
178
+ "y": 426,
179
+ "label": "add crew",
180
+ "box": {
181
+ "x": 1078,
182
+ "y": 415,
183
+ "w": 26,
184
+ "h": 22
185
+ }
186
+ },
187
+ {
188
+ "t": 19.232,
189
+ "kind": "move",
190
+ "x": 1248,
191
+ "y": 567,
192
+ "label": "Driver",
193
+ "box": {
194
+ "x": 1091,
195
+ "y": 552,
196
+ "w": 314,
197
+ "h": 30
198
+ }
199
+ },
200
+ {
201
+ "t": 21.67,
202
+ "kind": "move",
203
+ "x": 1248,
204
+ "y": 623,
205
+ "label": "Truck",
206
+ "box": {
207
+ "x": 1091,
208
+ "y": 608,
209
+ "w": 314,
210
+ "h": 30
211
+ }
212
+ },
213
+ {
214
+ "t": 23.468,
215
+ "kind": "info",
216
+ "x": 1248,
217
+ "y": 623,
218
+ "label": "truck='BUNGA1'",
219
+ "box": {
220
+ "x": 1091,
221
+ "y": 608,
222
+ "w": 314,
223
+ "h": 30
224
+ }
225
+ },
226
+ {
227
+ "t": 23.476,
228
+ "kind": "click",
229
+ "x": 1362,
230
+ "y": 778,
231
+ "label": "save crew",
232
+ "box": {
233
+ "x": 1320,
234
+ "y": 762,
235
+ "w": 85,
236
+ "h": 32
237
+ }
238
+ },
239
+ {
240
+ "t": 25.004,
241
+ "kind": "scene",
242
+ "x": null,
243
+ "y": null,
244
+ "label": "publish",
245
+ "box": null
246
+ },
247
+ {
248
+ "t": 25.008,
249
+ "kind": "move",
250
+ "x": 1834,
251
+ "y": 192,
252
+ "label": "Publish day",
253
+ "box": {
254
+ "x": 1765,
255
+ "y": 172,
256
+ "w": 139,
257
+ "h": 40
258
+ }
259
+ },
260
+ {
261
+ "t": 25.056,
262
+ "kind": "click",
263
+ "x": 1834,
264
+ "y": 192,
265
+ "label": "publish",
266
+ "box": {
267
+ "x": 1765,
268
+ "y": 172,
269
+ "w": 139,
270
+ "h": 40
271
+ }
272
+ }
273
+ ]
274
+ }
@@ -0,0 +1,4 @@
1
+ import { registerRoot } from 'remotion';
2
+ import { RemotionRoot } from './Root';
3
+
4
+ registerRoot(RemotionRoot);
@@ -0,0 +1,111 @@
1
+ export const FPS = 30;
2
+
3
+ // Brand
4
+ export const BRAND = {
5
+ red: '#E4002B',
6
+ dark: '#0B1220',
7
+ darker: '#070B14',
8
+ panel: '#111B2E',
9
+ text: '#F8FAFC',
10
+ sub: '#94A3B8',
11
+ accent: '#3B82F6',
12
+ };
13
+
14
+ export type Focus = { x: number; y: number; zoom: number; to?: { x: number; y: number } };
15
+
16
+ export type SceneDef = {
17
+ id: string;
18
+ kind: 'title' | 'web' | 'mobile';
19
+ durF: number; // duration in frames
20
+ vo: string; // wav basename in public/vo
21
+ // title
22
+ title?: string;
23
+ subtitle?: string;
24
+ // shot
25
+ img?: string; // file in public/shots
26
+ label?: string; // top-left chip
27
+ caption?: string; // lower third
28
+ focus?: Focus;
29
+ };
30
+
31
+ // Durations derived from measured VO length + ~1s padding (30fps)
32
+ export const SCENES: SceneDef[] = [
33
+ {
34
+ id: 'title',
35
+ kind: 'title',
36
+ durF: 138,
37
+ vo: 'scene0',
38
+ title: 'LOAD & GO',
39
+ subtitle: 'From dispatch to delivery',
40
+ },
41
+ {
42
+ id: 'daysheet',
43
+ kind: 'web',
44
+ durF: 159,
45
+ vo: 'scene1',
46
+ img: 'web1.png',
47
+ label: 'Day Sheet',
48
+ caption: 'Every job for the day, in one fast grid',
49
+ focus: { x: 0.5, y: 0.46, zoom: 1.14 },
50
+ },
51
+ {
52
+ id: 'create',
53
+ kind: 'web',
54
+ durF: 219,
55
+ vo: 'scene2',
56
+ img: 'web2.png',
57
+ label: 'Create a job · inline',
58
+ caption: 'Customer · time · pickup · delivery — lookup as you type',
59
+ focus: { x: 0.26, y: 0.37, zoom: 1.5, to: { x: 0.62, y: 0.37 } },
60
+ },
61
+ {
62
+ id: 'crew',
63
+ kind: 'web',
64
+ durF: 150,
65
+ vo: 'scene3',
66
+ img: 'web3.png',
67
+ label: 'Allocate crew',
68
+ caption: 'Driver · Truck · Trailer — from the row',
69
+ focus: { x: 0.82, y: 0.34, zoom: 1.5 },
70
+ },
71
+ {
72
+ id: 'publish',
73
+ kind: 'web',
74
+ durF: 132,
75
+ vo: 'scene4',
76
+ img: 'web4.png',
77
+ label: 'Publish the day',
78
+ caption: 'Sent straight to the driver’s phone',
79
+ focus: { x: 0.88, y: 0.16, zoom: 1.5 },
80
+ },
81
+ {
82
+ id: 'push',
83
+ kind: 'mobile',
84
+ durF: 150,
85
+ vo: 'scene5',
86
+ img: 'm1.png',
87
+ label: 'Driver app',
88
+ caption: 'Instant notification — a new job has landed',
89
+ focus: { x: 0.5, y: 0.11, zoom: 1.35 },
90
+ },
91
+ {
92
+ id: 'details',
93
+ kind: 'mobile',
94
+ durF: 270,
95
+ vo: 'scene6',
96
+ img: 'm2.png',
97
+ label: 'Job details',
98
+ caption: 'Pickup · delivery · load details · contacts',
99
+ focus: { x: 0.5, y: 0.26, zoom: 1.18, to: { x: 0.5, y: 0.6 } },
100
+ },
101
+ {
102
+ id: 'outro',
103
+ kind: 'title',
104
+ durF: 153,
105
+ vo: 'scene7',
106
+ title: 'LOAD & GO',
107
+ subtitle: 'One platform, dispatch to delivery',
108
+ },
109
+ ];
110
+
111
+ export const TOTAL_FRAMES = SCENES.reduce((sum, s) => sum + s.durF, 0);