Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/example/src/KeyframedPropsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
AbsoluteFill,
AnimatedImage,
interpolate,
interpolateColors,
Sequence,
Solid,
staticFile,
useCurrentFrame,
} from 'remotion';
Expand Down Expand Up @@ -120,6 +122,19 @@ const KeyframedPropsTest: React.FC = () => {
<Sequence from={30} name="effect keyframes should be shown at 30 and 90">
<ShiftedEffect />
</Sequence>
<Sequence
name="color keyframes should be shown at 0 and 100"
durationInFrames={120}
>
<Solid
width={180}
height={180}
color={interpolateColors(frame, [0, 100], ['#0b84f3', '#f43b00'])}
style={{
borderRadius: 24,
}}
/>
</Sequence>
<Sequence
from={30}
name="nested effect keyframes should be shown at 50 and 110"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,18 @@ const getNumericValue = (node: Expression): number | null => {
return null;
};

const getInterpolateExpression = (
const getInterpolationExpression = (
node: Expression,
): InterpolateExpression | null => {
if (node.type === 'TSAsExpression') {
return getInterpolateExpression(node.expression as Expression);
return getInterpolationExpression(node.expression as Expression);
}

if (
node.type !== 'CallExpression' ||
node.callee.type !== 'Identifier' ||
node.callee.name !== 'interpolate'
(node.callee.name !== 'interpolate' &&
node.callee.name !== 'interpolateColors')
) {
return null;
}
Expand Down Expand Up @@ -177,6 +178,20 @@ const getInterpolateExpression = (
};
};

const getInterpolationCalleeForValues = ({
staticValue,
newValue,
}: {
staticValue: unknown;
newValue: unknown;
}): ExpressionKind => {
return b.identifier(
typeof staticValue === 'string' && typeof newValue === 'string'
? 'interpolateColors'
: 'interpolate',
);
};

const createFrameExpression = (frame: number): ExpressionKind => {
return parseValueExpression(frame);
};
Expand Down Expand Up @@ -214,7 +229,7 @@ const addKeyframe = ({
frame: number;
value: unknown;
}): ExpressionKind => {
const existing = getInterpolateExpression(expression);
const existing = getInterpolationExpression(expression);
const newOutput = parseValueExpression(value);

if (existing) {
Expand Down Expand Up @@ -251,7 +266,10 @@ const addKeyframe = ({
const staticValue = extractStaticValue(expression);
const staticOutput = parseValueExpression(staticValue);
return createInterpolateExpression({
callee: b.identifier('interpolate'),
callee: getInterpolationCalleeForValues({
staticValue,
newValue: value,
}),
input: b.identifier('frame'),
extraArgs: [],
keyframes: [
Expand All @@ -268,7 +286,7 @@ const removeKeyframe = ({
expression: Expression;
frame: number;
}): ExpressionKind => {
const existing = getInterpolateExpression(expression);
const existing = getInterpolationExpression(expression);
if (!existing) {
throw new Error('Cannot remove keyframe from non-interpolated expression');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ const getNumericValue = (node: Expression): number | null => {
return null;
};

const getInterpolateKeyframes = (
const getInterpolationKeyframes = (
node: Expression,
): PropKeyframes | undefined => {
if (node.type === 'TSAsExpression') {
return getInterpolateKeyframes(node.expression as Expression);
return getInterpolationKeyframes(node.expression as Expression);
}

if (node.type !== 'CallExpression') {
Expand All @@ -154,7 +154,8 @@ const getInterpolateKeyframes = (
const callExpression = node as CallExpression;
if (
callExpression.callee.type !== 'Identifier' ||
callExpression.callee.name !== 'interpolate'
(callExpression.callee.name !== 'interpolate' &&
callExpression.callee.name !== 'interpolateColors')
) {
return undefined;
}
Expand Down Expand Up @@ -202,7 +203,7 @@ const getInterpolateKeyframes = (
};

export const getComputedStatus = (node: Expression): CanUpdatePropStatus => {
const keyframes = getInterpolateKeyframes(node);
const keyframes = getInterpolationKeyframes(node);
if (!keyframes) {
return {canUpdate: false, reason: 'computed'};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'node:path';
import {parseAst} from '../codemods/parse-ast';
import {
computeSequencePropsStatus,
computeSequencePropsStatusFromContent,
lineColumnToNodePath,
} from '../preview-server/routes/can-update-sequence-props';

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

const getNodePathFromContent = (content: string, line: number) => {
const ast = parseAst(content);
const result = lineColumnToNodePath(ast, line);
if (!result) {
throw new Error(`No JSX element found at line ${line}`);
}

return result;
};

test('canUpdateSequenceProps should flag computed props', () => {
const filePath = path.join(__dirname, 'snapshots', 'light-leak-computed.tsx');
const result = computeSequencePropsStatus({
Expand Down Expand Up @@ -45,6 +56,37 @@ test('canUpdateSequenceProps should flag computed props', () => {
});
});

test('computeSequencePropsStatus should return keyframes for interpolated color props', () => {
const input = `import React from 'react';
import {Solid, interpolateColors, useCurrentFrame} from 'remotion';

export const Example: React.FC = () => {
\tconst frame = useCurrentFrame();
\treturn (
\t\t<Solid color={interpolateColors(frame, [0, 100], ['red', 'blue'])} width={100} height={100} />
\t);
};
`;
const result = computeSequencePropsStatusFromContent({
fileContents: input,
nodePath: getNodePathFromContent(input, 7),
keys: ['color'],
effects: [],
});

expect(result.canUpdate).toBe(true);
if (!result.canUpdate) throw new Error('Expected canUpdate to be true');

expect(result.props.color).toEqual({
canUpdate: false,
reason: 'computed',
keyframes: [
{frame: 0, value: 'red'},
{frame: 100, value: 'blue'},
],
});
});

test('computeSequencePropsStatus should explain why outside-project file reads were blocked', () => {
const remotionRoot = path.join(__dirname, 'snapshots');
const fileName = '../outside.tsx';
Expand Down
53 changes: 53 additions & 0 deletions packages/studio-server/src/test/update-keyframes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export const Example: React.FC = () => {
};
`;

const colorInput = `import React from 'react';
import {Solid, interpolateColors, useCurrentFrame} from 'remotion';

export const Example: React.FC = () => {
\tconst frame = useCurrentFrame();
\treturn (
\t\t<Solid color={interpolateColors(frame, [0, 100], ['red', 'blue'])} width={100} height={100} />
\t);
};
`;

const effectInput = `import {tint} from '@remotion/effects/tint';
import {HtmlInCanvas} from '@remotion/html-in-canvas';
import {interpolate, useCurrentFrame} from 'remotion';
Expand Down Expand Up @@ -86,6 +97,48 @@ test('updateSequenceKeyframes converts a static value to an interpolation', asyn
expect(output).toContain('opacity: interpolate(frame, [0, 25], [0.5, 0.75])');
});

test('updateSequenceKeyframes adds a keyframe to an existing color interpolation', async () => {
const {output, oldValueStrings} = await updateSequenceKeyframes({
input: colorInput,
nodePath: lineColumnToNodePath(colorInput, getLine(colorInput, '<Solid')),
updates: [
{
key: 'color',
operation: {type: 'add', frame: 50, value: '#00ff00'},
},
],
});

expect(oldValueStrings).toEqual([
"interpolateColors(frame, [0, 100], ['red', 'blue'])",
]);
expect(output).toContain(
"color={interpolateColors(frame, [0, 50, 100], ['red', '#00ff00', 'blue'])}",
);
});

test('updateSequenceKeyframes converts a static string value to a color interpolation', async () => {
const input = colorInput.replace(
"interpolateColors(frame, [0, 100], ['red', 'blue'])",
"'red'",
);
const {output, oldValueStrings} = await updateSequenceKeyframes({
input,
nodePath: lineColumnToNodePath(input, getLine(input, '<Solid')),
updates: [
{
key: 'color',
operation: {type: 'add', frame: 50, value: 'blue'},
},
],
});

expect(oldValueStrings).toEqual(["'red'"]);
expect(output).toContain(
"color={interpolateColors(frame, [0, 50], ['red', 'blue'])}",
);
});

test('updateSequenceKeyframes collapses to a static value when one keyframe remains', async () => {
const {output, oldValueStrings} = await updateSequenceKeyframes({
input: sequenceInput,
Expand Down
Loading