creator-editing-studio 1.2.0 → 1.2.2

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,75 @@
1
+ import React from 'react';
2
+ import {Img, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
3
+ import {PALETTE, RADIUS, SHADOW} from '../design/tokens';
4
+ import {EASE} from '../design/motion';
5
+ import {progress} from '../lib/animate';
6
+
7
+ /**
8
+ * The corner bug, which exists to cover the teleprompter app's "RECORDED ON BIGVU" watermark
9
+ * burnt into the bottom-right of every frame of the talking head.
10
+ *
11
+ * Measured off the source at 1920x1080: the watermark sits at roughly x 1780-1900, y 962-1060.
12
+ * WATERMARK below is that box plus a margin, and the badge is sized from it rather than from
13
+ * taste, so the cover is guaranteed rather than eyeballed. If the footage is ever re-shot at a
14
+ * different size, re-measure and change this one constant.
15
+ *
16
+ * IT LIVES IN SCREEN SPACE, NOT FOOTAGE SPACE. Any push-in or reframe must be applied to the
17
+ * video underneath this, never to this badge — scale the badge with the footage and it slides
18
+ * off the thing it is covering, which is the one failure that matters here. The alternative,
19
+ * when a shot is punched in far enough, is to crop the watermark out of frame entirely and let
20
+ * the badge ride along as ordinary branding.
21
+ */
22
+ const WATERMARK = {left: 1770, top: 952, right: 1910, bottom: 1068} as const;
23
+
24
+ export const CornerMark: React.FC<{
25
+ /** Frame it fades in on. It then stays for the rest of the shot. */
26
+ at?: number;
27
+ /** Canvas width/height the measurements were taken against. */
28
+ canvas?: {width: number; height: number};
29
+ }> = ({at = 0, canvas = {width: 1920, height: 1080}}) => {
30
+ const frame = useCurrentFrame();
31
+ const {fps} = useVideoConfig();
32
+
33
+ /**
34
+ * Flush to the corner, rounded on the inner corner only.
35
+ *
36
+ * A pill was the obvious choice and it does not work: the watermark reaches to within 10px of
37
+ * the frame edge, so the cap's curve cuts across its corners and leaves white slivers showing.
38
+ * Covering those with a pill needs the badge to extend past x=1937 on a 1920 frame. Running
39
+ * the badge into the corner instead covers to both edges by construction, and one generous
40
+ * inner radius keeps it in the brand's geometry rather than looking like a crop.
41
+ */
42
+ const height = canvas.height - WATERMARK.top;
43
+ const minWidth = canvas.width - WATERMARK.left;
44
+
45
+ const show = progress(frame, at, Math.round(0.4 * fps), EASE.out);
46
+
47
+ return (
48
+ <div
49
+ style={{
50
+ position: 'absolute',
51
+ right: 0,
52
+ bottom: 0,
53
+ height,
54
+ minWidth,
55
+ display: 'flex',
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ gap: 18,
59
+ // Extra padding on the right and bottom keeps the type off the frame edge even though
60
+ // the panel itself runs into it.
61
+ padding: '0 40px 18px 34px',
62
+ borderTopLeftRadius: RADIUS.lg,
63
+ // The supplied mark is black letterforms with a accent bar: it is drawn for a light
64
+ // background and disappears entirely on ink. So the badge is paper, not ink. It also
65
+ // sits more quietly over the light wall behind him than a black tab would.
66
+ backgroundColor: PALETTE.paper,
67
+ boxShadow: SHADOW.card,
68
+ opacity: show,
69
+ transform: `translateY(${(1 - show) * 12}px)`,
70
+ }}
71
+ >
72
+ <Img src={staticFile('setup/ceos.svg')} style={{width: 150, display: 'block'}} />
73
+ </div>
74
+ );
75
+ };
@@ -1,93 +1,153 @@
1
- import React from 'react';
2
- import {useCurrentFrame, useVideoConfig} from 'remotion';
3
- import {EASE} from '../design/motion';
4
- import {COLOR, RADIUS} from '../design/tokens';
5
- import {durFrames, perFrame, progress, travelFrames} from '../lib/animate';
6
- import {Presence} from './Presence';
7
- import {VelocityBlur} from './VelocityBlur';
8
-
9
- /** A point the cursor ARRIVES at by frame `at` (the tip of the arrow). */
10
- export type CursorStop = {at: number; x: number; y: number; click?: boolean};
11
-
12
- const CLICK_FRAMES = 6;
13
- /** Pointer size and where its tip sits inside the 64px artwork. */
14
- const POINTER = 68;
15
- const TIP = 6 * (POINTER / 64);
16
-
17
- /**
18
- * An arrow cursor that guides the eye: travels between stops on gentle arcs,
19
- * arriving exactly on time, and clicks with a press + ripple.
20
- */
21
- export const Cursor: React.FC<{stops: CursorStop[]; hideAt: number}> = ({stops, hideAt}) => {
22
- const frame = useCurrentFrame();
23
- const {fps} = useVideoConfig();
24
-
25
- const positionAt = (f: number) => {
26
- let x = stops[0].x;
27
- let y = stops[0].y;
28
- for (let i = 1; i < stops.length; i++) {
29
- const from = stops[i - 1];
30
- const to = stops[i];
31
- const distance = Math.hypot(to.x - from.x, to.y - from.y);
32
- const travel = Math.min(travelFrames(distance, fps), Math.max(1, to.at - from.at));
33
- const depart = to.at - travel;
34
- if (f <= depart) break;
35
- const t = progress(f, depart, travel, EASE.inOut);
36
- // Arc: bulge perpendicular to the path, peaking mid-way.
37
- const bulge = Math.sin(Math.PI * t) * distance * 0.1;
38
- const nx = distance ? -(to.y - from.y) / distance : 0;
39
- const ny = distance ? (to.x - from.x) / distance : 0;
40
- x = from.x + (to.x - from.x) * t + nx * bulge;
41
- y = from.y + (to.y - from.y) * t + ny * bulge;
42
- }
43
- return {x, y};
44
- };
45
-
46
- const {x, y} = positionAt(frame);
47
- const click = stops.find((s) => s.click && frame >= s.at && frame < s.at + durFrames('slow', fps));
48
- const pressT = click ? progress(frame, click.at, CLICK_FRAMES, EASE.inOut) : 0;
49
- const press = 1 - 0.14 * Math.sin(Math.PI * pressT);
50
- const ripple = click ? progress(frame, click.at, durFrames('slow', fps)) : 0;
51
-
52
- return (
53
- <Presence at={stops[0].at} until={hideAt} dur="fast" from="none" style={{position: 'absolute', inset: 0}}>
54
- {click ? (
55
- <div
56
- style={{
57
- position: 'absolute',
58
- left: click.x - 70 * ripple,
59
- top: click.y - 70 * ripple,
60
- width: 140 * ripple,
61
- height: 140 * ripple,
62
- borderRadius: RADIUS.pill,
63
- border: `4px solid ${COLOR.accent}`,
64
- opacity: 1 - ripple,
65
- }}
66
- />
67
- ) : null}
68
- <VelocityBlur
69
- vx={perFrame((f) => positionAt(f).x, frame)}
70
- vy={perFrame((f) => positionAt(f).y, frame)}
71
- style={{
72
- position: 'absolute',
73
- left: x - TIP,
74
- top: y - TIP,
75
- transformOrigin: `${TIP}px ${TIP}px`,
76
- transform: `scale(${press.toFixed(4)})`,
77
- filter: `drop-shadow(0 8px 14px ${COLOR.fg}66)`,
78
- }}
79
- >
80
- {/* Solid pointer (as in the reference): tip top-left, notched tail. */}
81
- <svg width={POINTER} height={POINTER} viewBox="0 0 64 64">
82
- <path
83
- d="M6 6 L58 26 Q60 27 58 28 L35 35 L28 58 Q27 60 26 58 Z"
84
- fill={COLOR.accent}
85
- stroke={COLOR.fg}
86
- strokeWidth={3}
87
- strokeLinejoin="round"
88
- />
89
- </svg>
90
- </VelocityBlur>
91
- </Presence>
92
- );
93
- };
1
+ import React from 'react';
2
+ import {useCurrentFrame, useVideoConfig} from 'remotion';
3
+ import {EASE} from '../design/motion';
4
+ import {COLOR, FONT, RADIUS, WEIGHT} from '../design/tokens';
5
+ import {durFrames, perFrame, progress, travelFrames} from '../lib/animate';
6
+ import {Presence} from './Presence';
7
+ import {VelocityBlur} from './VelocityBlur';
8
+
9
+ /** A point the cursor ARRIVES at by frame `at` (the tip of the arrow). */
10
+ export type CursorStop = {at: number; x: number; y: number; click?: boolean};
11
+
12
+ const CLICK_FRAMES = 6;
13
+ /** Pointer size and where its tip sits inside the 64px artwork. */
14
+ const POINTER = 68;
15
+ const TIP = 6 * (POINTER / 64);
16
+
17
+ /**
18
+ * An arrow cursor that guides the eye: travels between stops on gentle arcs,
19
+ * arriving exactly on time, and clicks with a press + ripple.
20
+ */
21
+ export const Cursor: React.FC<{
22
+ stops: CursorStop[];
23
+ hideAt: number;
24
+ /**
25
+ * A name tag riding behind the arrow, the way a collaborator's cursor is labelled in Figma.
26
+ * Optional and additive: every existing use renders exactly as before without it.
27
+ *
28
+ * Pass a function of the frame to change the word mid-move — "copy" becoming "copied" the
29
+ * instant the click lands reads as the action succeeding, with no second element needed.
30
+ */
31
+ label?: string | ((frame: number) => string);
32
+ /**
33
+ * Arrow colour. Defaults to the brand accent, which is what every earlier use gets.
34
+ *
35
+ * The setup walkthrough passes ink instead, asked for directly ("the arrow should be black"):
36
+ * over a white email and a light desktop a accent arrow reads as a graphic sitting on the shot,
37
+ * where a black one reads as the viewer's own pointer.
38
+ */
39
+ fill?: string;
40
+ /** Tag colour, as a function of the frame where it changes state mid-move. */
41
+ tagColour?: string | ((frame: number) => string);
42
+ /** Tag text colour, for tags too dark for ink to read on. */
43
+ tagText?: string | ((frame: number) => string);
44
+ }> = ({
45
+ stops,
46
+ hideAt,
47
+ label,
48
+ fill = COLOR.accent,
49
+ tagColour = COLOR.accent,
50
+ tagText = COLOR.fg,
51
+ }) => {
52
+ const frame = useCurrentFrame();
53
+ const {fps} = useVideoConfig();
54
+
55
+ const positionAt = (f: number) => {
56
+ let x = stops[0].x;
57
+ let y = stops[0].y;
58
+ for (let i = 1; i < stops.length; i++) {
59
+ const from = stops[i - 1];
60
+ const to = stops[i];
61
+ const distance = Math.hypot(to.x - from.x, to.y - from.y);
62
+ const travel = Math.min(travelFrames(distance, fps), Math.max(1, to.at - from.at));
63
+ const depart = to.at - travel;
64
+ if (f <= depart) break;
65
+ const t = progress(f, depart, travel, EASE.inOut);
66
+ // Arc: bulge perpendicular to the path, peaking mid-way.
67
+ const bulge = Math.sin(Math.PI * t) * distance * 0.1;
68
+ const nx = distance ? -(to.y - from.y) / distance : 0;
69
+ const ny = distance ? (to.x - from.x) / distance : 0;
70
+ x = from.x + (to.x - from.x) * t + nx * bulge;
71
+ y = from.y + (to.y - from.y) * t + ny * bulge;
72
+ }
73
+ return {x, y};
74
+ };
75
+
76
+ const {x, y} = positionAt(frame);
77
+ const click = stops.find((s) => s.click && frame >= s.at && frame < s.at + durFrames('slow', fps));
78
+ const pressT = click ? progress(frame, click.at, CLICK_FRAMES, EASE.inOut) : 0;
79
+ const press = 1 - 0.14 * Math.sin(Math.PI * pressT);
80
+ const ripple = click ? progress(frame, click.at, durFrames('slow', fps)) : 0;
81
+
82
+ // Resolved first so a label that returns an empty string removes the tag outright, which is
83
+ // how "copied" disappears once the line has been pasted.
84
+ const labelText = typeof label === 'function' ? label(frame) : label;
85
+
86
+ return (
87
+ <Presence at={stops[0].at} until={hideAt} dur="fast" from="none" style={{position: 'absolute', inset: 0}}>
88
+ {click ? (
89
+ <div
90
+ style={{
91
+ position: 'absolute',
92
+ left: click.x - 70 * ripple,
93
+ top: click.y - 70 * ripple,
94
+ width: 140 * ripple,
95
+ height: 140 * ripple,
96
+ borderRadius: RADIUS.pill,
97
+ border: `4px solid ${fill}`,
98
+ opacity: 1 - ripple,
99
+ }}
100
+ />
101
+ ) : null}
102
+ <VelocityBlur
103
+ vx={perFrame((f) => positionAt(f).x, frame)}
104
+ vy={perFrame((f) => positionAt(f).y, frame)}
105
+ style={{
106
+ position: 'absolute',
107
+ left: x - TIP,
108
+ top: y - TIP,
109
+ transformOrigin: `${TIP}px ${TIP}px`,
110
+ transform: `scale(${press.toFixed(4)})`,
111
+ filter: `drop-shadow(0 8px 14px ${COLOR.fg}66)`,
112
+ }}
113
+ >
114
+ {/* Solid pointer (as in the reference): tip top-left, notched tail. */}
115
+ <svg width={POINTER} height={POINTER} viewBox="0 0 64 64">
116
+ <path
117
+ d="M6 6 L58 26 Q60 27 58 28 L35 35 L28 58 Q27 60 26 58 Z"
118
+ fill={fill}
119
+ stroke={COLOR.fg}
120
+ strokeWidth={3}
121
+ strokeLinejoin="round"
122
+ />
123
+ </svg>
124
+ </VelocityBlur>
125
+
126
+ {labelText ? (
127
+ <div
128
+ style={{
129
+ position: 'absolute',
130
+ // Sits behind and below the tip, where a Figma cursor label sits, so it never
131
+ // covers the thing being pointed at.
132
+ left: x + POINTER * 0.42,
133
+ top: y + POINTER * 0.52,
134
+ display: 'flex',
135
+ alignItems: 'center',
136
+ padding: '7px 16px',
137
+ borderRadius: RADIUS.pill,
138
+ backgroundColor: typeof tagColour === 'function' ? tagColour(frame) : tagColour,
139
+ fontFamily: FONT.sans,
140
+ fontSize: 26,
141
+ fontWeight: WEIGHT.semibold,
142
+ letterSpacing: '-0.015em',
143
+ color: typeof tagText === 'function' ? tagText(frame) : tagText,
144
+ whiteSpace: 'nowrap',
145
+ boxShadow: `0 6px 16px ${COLOR.fg}33`,
146
+ }}
147
+ >
148
+ {labelText}
149
+ </div>
150
+ ) : null}
151
+ </Presence>
152
+ );
153
+ };