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<{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
};
|