@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.
- package/README.md +47 -0
- package/bin/cli.js +88 -0
- package/package.json +19 -0
- package/template/.claude/commands/demo-video.md +42 -0
- package/template/README.md +69 -0
- package/template/demo.config.json +15 -0
- package/template/package.json +13 -0
- package/template/pipeline/capture_mobile.sh +60 -0
- package/template/pipeline/capture_web.py +186 -0
- package/template/pipeline/gen_music.py +52 -0
- package/template/pipeline/gen_vo.py +29 -0
- package/template/pipeline/run.sh +45 -0
- package/template/remotion/package.json +16 -0
- package/template/remotion/public/brand/logo-white.png +0 -0
- package/template/remotion/remotion.config.ts +5 -0
- package/template/remotion/src/DemoVideo.tsx +18 -0
- package/template/remotion/src/Root.tsx +47 -0
- package/template/remotion/src/Scene.tsx +131 -0
- package/template/remotion/src/edit/Cursor.tsx +83 -0
- package/template/remotion/src/edit/EditVideo.tsx +68 -0
- package/template/remotion/src/edit/data.ts +134 -0
- package/template/remotion/src/edit/mobile.events.json +11 -0
- package/template/remotion/src/edit/segments.tsx +268 -0
- package/template/remotion/src/edit/vo-manifest.json +38 -0
- package/template/remotion/src/edit/web.events.json +274 -0
- package/template/remotion/src/index.ts +4 -0
- package/template/remotion/src/scenes.ts +111 -0
- package/template/remotion/src/ui.tsx +161 -0
- package/template/remotion/tsconfig.json +13 -0
- package/template/setup.sh +29 -0
|
@@ -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,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);
|