Skip to content

Commit 2f87ec5

Browse files
cursoragentJonnyBurger
authored andcommitted
Support interpolateColors keyframes
1 parent 43839eb commit 2f87ec5

5 files changed

Lines changed: 139 additions & 10 deletions

File tree

packages/example/src/KeyframedPropsTest.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
AbsoluteFill,
66
AnimatedImage,
77
interpolate,
8+
interpolateColors,
89
Sequence,
10+
Solid,
911
staticFile,
1012
useCurrentFrame,
1113
} from 'remotion';
@@ -120,6 +122,19 @@ const KeyframedPropsTest: React.FC = () => {
120122
<Sequence from={30} name="effect keyframes should be shown at 30 and 90">
121123
<ShiftedEffect />
122124
</Sequence>
125+
<Sequence
126+
name="color keyframes should be shown at 0 and 100"
127+
durationInFrames={120}
128+
>
129+
<Solid
130+
width={180}
131+
height={180}
132+
color={interpolateColors(frame, [0, 100], ['#0b84f3', '#f43b00'])}
133+
style={{
134+
borderRadius: 24,
135+
}}
136+
/>
137+
</Sequence>
123138
<Sequence
124139
from={30}
125140
name="nested effect keyframes should be shown at 50 and 110"

packages/studio-server/src/codemods/update-keyframes/update-keyframes.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,18 @@ const getNumericValue = (node: Expression): number | null => {
101101
return null;
102102
};
103103

104-
const getInterpolateExpression = (
104+
const getInterpolationExpression = (
105105
node: Expression,
106106
): InterpolateExpression | null => {
107107
if (node.type === 'TSAsExpression') {
108-
return getInterpolateExpression(node.expression as Expression);
108+
return getInterpolationExpression(node.expression as Expression);
109109
}
110110

111111
if (
112112
node.type !== 'CallExpression' ||
113113
node.callee.type !== 'Identifier' ||
114-
node.callee.name !== 'interpolate'
114+
(node.callee.name !== 'interpolate' &&
115+
node.callee.name !== 'interpolateColors')
115116
) {
116117
return null;
117118
}
@@ -177,6 +178,20 @@ const getInterpolateExpression = (
177178
};
178179
};
179180

181+
const getInterpolationCalleeForValues = ({
182+
staticValue,
183+
newValue,
184+
}: {
185+
staticValue: unknown;
186+
newValue: unknown;
187+
}): ExpressionKind => {
188+
return b.identifier(
189+
typeof staticValue === 'string' && typeof newValue === 'string'
190+
? 'interpolateColors'
191+
: 'interpolate',
192+
);
193+
};
194+
180195
const createFrameExpression = (frame: number): ExpressionKind => {
181196
return parseValueExpression(frame);
182197
};
@@ -214,7 +229,7 @@ const addKeyframe = ({
214229
frame: number;
215230
value: unknown;
216231
}): ExpressionKind => {
217-
const existing = getInterpolateExpression(expression);
232+
const existing = getInterpolationExpression(expression);
218233
const newOutput = parseValueExpression(value);
219234

220235
if (existing) {
@@ -251,7 +266,10 @@ const addKeyframe = ({
251266
const staticValue = extractStaticValue(expression);
252267
const staticOutput = parseValueExpression(staticValue);
253268
return createInterpolateExpression({
254-
callee: b.identifier('interpolate'),
269+
callee: getInterpolationCalleeForValues({
270+
staticValue,
271+
newValue: value,
272+
}),
255273
input: b.identifier('frame'),
256274
extraArgs: [],
257275
keyframes: [
@@ -268,7 +286,7 @@ const removeKeyframe = ({
268286
expression: Expression;
269287
frame: number;
270288
}): ExpressionKind => {
271-
const existing = getInterpolateExpression(expression);
289+
const existing = getInterpolationExpression(expression);
272290
if (!existing) {
273291
throw new Error('Cannot remove keyframe from non-interpolated expression');
274292
}

packages/studio-server/src/preview-server/routes/can-update-sequence-props.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,11 @@ const getNumericValue = (node: Expression): number | null => {
140140
return null;
141141
};
142142

143-
const getInterpolateKeyframes = (
143+
const getInterpolationKeyframes = (
144144
node: Expression,
145145
): PropKeyframes | undefined => {
146146
if (node.type === 'TSAsExpression') {
147-
return getInterpolateKeyframes(node.expression as Expression);
147+
return getInterpolationKeyframes(node.expression as Expression);
148148
}
149149

150150
if (node.type !== 'CallExpression') {
@@ -154,7 +154,8 @@ const getInterpolateKeyframes = (
154154
const callExpression = node as CallExpression;
155155
if (
156156
callExpression.callee.type !== 'Identifier' ||
157-
callExpression.callee.name !== 'interpolate'
157+
(callExpression.callee.name !== 'interpolate' &&
158+
callExpression.callee.name !== 'interpolateColors')
158159
) {
159160
return undefined;
160161
}
@@ -202,7 +203,7 @@ const getInterpolateKeyframes = (
202203
};
203204

204205
export const getComputedStatus = (node: Expression): CanUpdatePropStatus => {
205-
const keyframes = getInterpolateKeyframes(node);
206+
const keyframes = getInterpolationKeyframes(node);
206207
if (!keyframes) {
207208
return {canUpdate: false, reason: 'computed'};
208209
}

packages/studio-server/src/test/compute-sequence-props-status.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path';
44
import {parseAst} from '../codemods/parse-ast';
55
import {
66
computeSequencePropsStatus,
7+
computeSequencePropsStatusFromContent,
78
lineColumnToNodePath,
89
} from '../preview-server/routes/can-update-sequence-props';
910

@@ -18,6 +19,16 @@ const getNodePath = (filePath: string, line: number) => {
1819
return result;
1920
};
2021

22+
const getNodePathFromContent = (content: string, line: number) => {
23+
const ast = parseAst(content);
24+
const result = lineColumnToNodePath(ast, line);
25+
if (!result) {
26+
throw new Error(`No JSX element found at line ${line}`);
27+
}
28+
29+
return result;
30+
};
31+
2132
test('canUpdateSequenceProps should flag computed props', () => {
2233
const filePath = path.join(__dirname, 'snapshots', 'light-leak-computed.tsx');
2334
const result = computeSequencePropsStatus({
@@ -45,6 +56,37 @@ test('canUpdateSequenceProps should flag computed props', () => {
4556
});
4657
});
4758

59+
test('computeSequencePropsStatus should return keyframes for interpolated color props', () => {
60+
const input = `import React from 'react';
61+
import {Solid, interpolateColors, useCurrentFrame} from 'remotion';
62+
63+
export const Example: React.FC = () => {
64+
\tconst frame = useCurrentFrame();
65+
\treturn (
66+
\t\t<Solid color={interpolateColors(frame, [0, 100], ['red', 'blue'])} width={100} height={100} />
67+
\t);
68+
};
69+
`;
70+
const result = computeSequencePropsStatusFromContent({
71+
fileContents: input,
72+
nodePath: getNodePathFromContent(input, 7),
73+
keys: ['color'],
74+
effects: [],
75+
});
76+
77+
expect(result.canUpdate).toBe(true);
78+
if (!result.canUpdate) throw new Error('Expected canUpdate to be true');
79+
80+
expect(result.props.color).toEqual({
81+
canUpdate: false,
82+
reason: 'computed',
83+
keyframes: [
84+
{frame: 0, value: 'red'},
85+
{frame: 100, value: 'blue'},
86+
],
87+
});
88+
});
89+
4890
test('computeSequencePropsStatus should explain why outside-project file reads were blocked', () => {
4991
const remotionRoot = path.join(__dirname, 'snapshots');
5092
const fileName = '../outside.tsx';

packages/studio-server/src/test/update-keyframes.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ export const Example: React.FC = () => {
1919
};
2020
`;
2121

22+
const colorInput = `import React from 'react';
23+
import {Solid, interpolateColors, useCurrentFrame} from 'remotion';
24+
25+
export const Example: React.FC = () => {
26+
\tconst frame = useCurrentFrame();
27+
\treturn (
28+
\t\t<Solid color={interpolateColors(frame, [0, 100], ['red', 'blue'])} width={100} height={100} />
29+
\t);
30+
};
31+
`;
32+
2233
const effectInput = `import {tint} from '@remotion/effects/tint';
2334
import {HtmlInCanvas} from '@remotion/html-in-canvas';
2435
import {interpolate, useCurrentFrame} from 'remotion';
@@ -86,6 +97,48 @@ test('updateSequenceKeyframes converts a static value to an interpolation', asyn
8697
expect(output).toContain('opacity: interpolate(frame, [0, 25], [0.5, 0.75])');
8798
});
8899

100+
test('updateSequenceKeyframes adds a keyframe to an existing color interpolation', async () => {
101+
const {output, oldValueStrings} = await updateSequenceKeyframes({
102+
input: colorInput,
103+
nodePath: lineColumnToNodePath(colorInput, getLine(colorInput, '<Solid')),
104+
updates: [
105+
{
106+
key: 'color',
107+
operation: {type: 'add', frame: 50, value: '#00ff00'},
108+
},
109+
],
110+
});
111+
112+
expect(oldValueStrings).toEqual([
113+
"interpolateColors(frame, [0, 100], ['red', 'blue'])",
114+
]);
115+
expect(output).toContain(
116+
"color={interpolateColors(frame, [0, 50, 100], ['red', '#00ff00', 'blue'])}",
117+
);
118+
});
119+
120+
test('updateSequenceKeyframes converts a static string value to a color interpolation', async () => {
121+
const input = colorInput.replace(
122+
"interpolateColors(frame, [0, 100], ['red', 'blue'])",
123+
"'red'",
124+
);
125+
const {output, oldValueStrings} = await updateSequenceKeyframes({
126+
input,
127+
nodePath: lineColumnToNodePath(input, getLine(input, '<Solid')),
128+
updates: [
129+
{
130+
key: 'color',
131+
operation: {type: 'add', frame: 50, value: 'blue'},
132+
},
133+
],
134+
});
135+
136+
expect(oldValueStrings).toEqual(["'red'"]);
137+
expect(output).toContain(
138+
"color={interpolateColors(frame, [0, 50], ['red', 'blue'])}",
139+
);
140+
});
141+
89142
test('updateSequenceKeyframes collapses to a static value when one keyframe remains', async () => {
90143
const {output, oldValueStrings} = await updateSequenceKeyframes({
91144
input: sequenceInput,

0 commit comments

Comments
 (0)