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
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ const removeKeyframe = ({
(_keyframe, index) => index !== keyframeIndex,
);

if (nextKeyframes.length === 0) {
return existing.keyframes[keyframeIndex].output;
}

return createInterpolateExpression({
callee: existing.callee,
input: existing.input,
Expand Down
91 changes: 91 additions & 0 deletions packages/studio-server/src/test/update-keyframes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,38 @@ test('updateSequenceKeyframes keeps an interpolation when one keyframe remains',
expect(output).toContain('style={{scale: interpolate(frame, [100], [4])}}');
});

test('updateSequenceKeyframes converts the last keyframe to a static value', async () => {
const input = sequenceInput.replace(
'interpolate(frame, [0, 100], [2, 4])',
'interpolate(frame, [12], [320])',
);
const {output, oldValueStrings, newValueStrings, updatedNodePath} =
await updateSequenceKeyframes({
input,
nodePath: lineColumnToNodePath(input, getLine(input, 'scale')),
updates: [
{
key: 'style.scale',
operation: {type: 'remove', frame: 12},
},
],
});

expect(oldValueStrings).toEqual(['interpolate(frame, [12], [320])']);
expect(newValueStrings).toEqual(['320']);
expect(output).toContain('style={{scale: 320}}');
const status = computeSequencePropsStatusFromContent({
fileContents: output,
nodePath: updatedNodePath,
keys: ['style.scale'],
effects: [],
});
expect(status.props['style.scale']).toEqual({
canUpdate: true,
codeValue: 320,
});
});

test('updateSequenceKeyframes keeps a color interpolation when one keyframe remains', async () => {
const {output, oldValueStrings} = await updateSequenceKeyframes({
input: colorInput,
Expand All @@ -253,6 +285,38 @@ test('updateSequenceKeyframes keeps a color interpolation when one keyframe rema
expect(output).toContain("color={interpolateColors(frame, [100], ['blue'])}");
});

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

expect(oldValueStrings).toEqual(["interpolateColors(frame, [15], ['blue'])"]);
expect(newValueStrings).toEqual(["'blue'"]);
expect(output).toContain("color={'blue'}");
const status = computeSequencePropsStatusFromContent({
fileContents: output,
nodePath: updatedNodePath,
keys: ['color'],
effects: [],
});
expect(status.props.color).toEqual({
canUpdate: true,
codeValue: 'blue',
});
});

test('updateEffectKeyframes removes a keyframe from an effect prop interpolation', () => {
const {serialized, oldValueStrings, effectCallee} = updateEffectKeyframesAst({
input: effectInput,
Expand Down Expand Up @@ -300,3 +364,30 @@ test('updateEffectKeyframes keeps an effect prop interpolation with one keyframe

expect(serialized).toContain('amount: interpolate(frame, [0], [0.2])');
});

test('updateEffectKeyframes converts the last effect keyframe to a static value', () => {
const input = effectInput.replace(
'interpolate(frame, [0, 50, 100], [0.2, 0.5, 0.8])',
'interpolate(frame, [40], [0.6])',
);
const {serialized, oldValueStrings, newValueStrings, effectCallee} =
updateEffectKeyframesAst({
input,
sequenceNodePath: lineColumnToNodePath(
input,
getLine(input, '<HtmlInCanvas'),
),
effectIndex: 0,
updates: [
{
key: 'amount',
operation: {type: 'remove', frame: 40},
},
],
});

expect(effectCallee).toBe('tint');
expect(oldValueStrings).toEqual(['interpolate(frame, [40], [0.6])']);
expect(newValueStrings).toEqual(['0.6']);
expect(serialized).toContain('amount: 0.6');
});
6 changes: 6 additions & 0 deletions packages/studio-shared/src/optimistic-delete-keyframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const removeKeyframeFromPropStatus = ({
}

const keyframes = status.keyframes.filter((_, i) => i !== index);
if (keyframes.length === 0) {
return {
canUpdate: true,
codeValue: status.keyframes[index].value,
};
}

// Easing holds one segment per gap between consecutive keyframes
// (keyframes.length - 1 entries). Drop the segment adjacent to the removed
Expand Down
79 changes: 79 additions & 0 deletions packages/studio-shared/src/test/optimistic-delete-keyframe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,39 @@ test('optimisticDeleteSequenceKeyframe removes the matching keyframe and an easi
expect(status.easing).toEqual(['linear']);
});

test('optimisticDeleteSequenceKeyframe converts the last keyframe to a static value', () => {
const previous: CanUpdateSequencePropsResponse = {
canUpdate: true,
props: {
width: {
canUpdate: false,
reason: 'keyframed',
interpolationFunction: 'interpolate',
keyframes: [{frame: 12, value: 320}],
easing: [],
clamping: {left: 'extend', right: 'extend'},
posterize: undefined,
},
},
effects: [],
};

const updated = optimisticDeleteSequenceKeyframe({
previous,
fieldKey: 'width',
frame: 12,
});

if (!updated.canUpdate) {
throw new Error('expected canUpdate true');
}

expect(updated.props.width).toEqual({
canUpdate: true,
codeValue: 320,
});
});

test('optimisticDeleteSequenceKeyframe is a no-op when no keyframe matches', () => {
const previous: CanUpdateSequencePropsResponse = {
canUpdate: true,
Expand Down Expand Up @@ -143,6 +176,52 @@ test('optimisticDeleteEffectKeyframe removes the matching keyframe on the target
expect(status.easing).toEqual([]);
});

test('optimisticDeleteEffectKeyframe converts the last keyframe on the target effect to a static value', () => {
const previous: CanUpdateSequencePropsResponse = {
canUpdate: true,
props: {},
effects: [
{
canUpdate: true,
effectIndex: 0,
callee: 'tint',
props: {
amount: {
canUpdate: false,
reason: 'keyframed',
interpolationFunction: 'interpolate',
keyframes: [{frame: 40, value: 0.6}],
easing: [],
clamping: {left: 'extend', right: 'extend'},
posterize: undefined,
},
},
},
],
};

const updated = optimisticDeleteEffectKeyframe({
previous,
effectIndex: 0,
fieldKey: 'amount',
frame: 40,
});

if (!updated.canUpdate) {
throw new Error('expected canUpdate true');
}

const effect = updated.effects[0];
if (!effect.canUpdate) {
throw new Error('expected effect canUpdate true');
}

expect(effect.props.amount).toEqual({
canUpdate: true,
codeValue: 0.6,
});
});

test('optimisticDeleteEffectKeyframe is a no-op when effect index not found', () => {
const previous: CanUpdateSequencePropsResponse = {
canUpdate: true,
Expand Down
Loading