@remotion/studio-server 4.0.474 → 4.0.475

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.
@@ -39,10 +39,7 @@ const imports_1 = require("../helpers/imports");
39
39
  const update_nested_prop_1 = require("./update-nested-prop");
40
40
  const b = recast.types.builders;
41
41
  const isNonLinearEasing = (easing) => easing !== 'linear';
42
- const keyframedParamNeedsEasingImport = (param) => {
43
- return (param.interpolationFunction !== 'interpolateColors' &&
44
- param.easing.some(isNonLinearEasing));
45
- };
42
+ const keyframedParamNeedsEasingImport = (param) => param.easing.some(isNonLinearEasing);
46
43
  const getRequiredRemotionImportsForEffectParams = (params) => {
47
44
  const requiredImports = new Set();
48
45
  for (const param of params) {
@@ -91,10 +88,10 @@ const makeKeyframedOptions = ({ param, remotionLocalNames, }) => {
91
88
  if (param.clamping.right !== 'extend') {
92
89
  properties.push(b.objectProperty(b.identifier('extrapolateRight'), b.stringLiteral(param.clamping.right)));
93
90
  }
94
- if (keyframedParamNeedsEasingImport(param)) {
95
- const easingLocalName = (_a = remotionLocalNames.Easing) !== null && _a !== void 0 ? _a : 'Easing';
96
- properties.push(b.objectProperty(b.identifier('easing'), b.arrayExpression(param.easing.map((easing) => makeEasingExpression({ easing, easingLocalName })))));
97
- }
91
+ }
92
+ if (keyframedParamNeedsEasingImport(param)) {
93
+ const easingLocalName = (_a = remotionLocalNames.Easing) !== null && _a !== void 0 ? _a : 'Easing';
94
+ properties.push(b.objectProperty(b.identifier('easing'), b.arrayExpression(param.easing.map((easing) => makeEasingExpression({ easing, easingLocalName })))));
98
95
  }
99
96
  if (param.posterize !== undefined) {
100
97
  properties.push(b.objectProperty(b.identifier('posterize'), (0, update_nested_prop_1.parseValueExpression)(param.posterize)));
@@ -1,5 +1,8 @@
1
1
  import { type KeyframeInterpolationFunction } from '@remotion/studio-shared';
2
- import type { ExtrapolateType, SequenceNodePath, SequenceSchema } from 'remotion';
2
+ import type { CanUpdateSequencePropStatus, ExtrapolateType, SequenceNodePath, SequenceSchema } from 'remotion';
3
+ type KeyframeEasing = Extract<CanUpdateSequencePropStatus, {
4
+ status: 'keyframed';
5
+ }>['easing'][number];
3
6
  export type KeyframeOperation = {
4
7
  type: 'add';
5
8
  frame: number;
@@ -14,6 +17,10 @@ export type KeyframeOperation = {
14
17
  right: ExtrapolateType;
15
18
  } | undefined;
16
19
  posterize: number | undefined;
20
+ } | {
21
+ type: 'easing';
22
+ segmentIndex: number;
23
+ easing: KeyframeEasing;
17
24
  } | {
18
25
  type: 'move';
19
26
  moves: {
@@ -32,6 +39,7 @@ export type EffectKeyframeUpdate = {
32
39
  export type IntroducedKeyframeIdentifiers = {
33
40
  calleeName: KeyframeInterpolationFunction | null;
34
41
  needsFrameHook: boolean;
42
+ needsEasingImport: boolean;
35
43
  };
36
44
  export declare const updateSequenceKeyframesAst: ({ input, nodePath, updates, schema, }: {
37
45
  input: string;
@@ -89,3 +97,4 @@ export declare const updateEffectKeyframes: ({ input, sequenceNodePath, effectIn
89
97
  effectCallee: string;
90
98
  updatedSequenceNodePath: SequenceNodePath;
91
99
  }>;
100
+ export {};
@@ -44,6 +44,62 @@ const update_effect_props_1 = require("../update-effect-props/update-effect-prop
44
44
  const update_nested_prop_1 = require("../update-nested-prop");
45
45
  const ensure_imports_and_frame_hook_1 = require("./ensure-imports-and-frame-hook");
46
46
  const b = recast.types.builders;
47
+ const getObjectPropertyNameFromUpdateKey = (key) => {
48
+ const dotIndex = key.indexOf('.');
49
+ return dotIndex === -1 ? key : key.slice(dotIndex + 1);
50
+ };
51
+ const lineStartsWithPropertyName = ({ line, propertyName, }) => {
52
+ const trimmed = line.trimStart();
53
+ return (trimmed.startsWith(`${propertyName}:`) ||
54
+ trimmed.startsWith(`'${propertyName}':`) ||
55
+ trimmed.startsWith(`"${propertyName}":`));
56
+ };
57
+ const getIndent = (line) => {
58
+ var _a;
59
+ var _b;
60
+ return (_b = (_a = line.match(/^[ \t]*/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : '';
61
+ };
62
+ const removeBlankLinesFromObjectsWithProperties = ({ input, propertyNames, }) => {
63
+ if (propertyNames.length === 0) {
64
+ return input;
65
+ }
66
+ const lines = input.split('\n');
67
+ const linesToRemove = new Set();
68
+ for (let i = 0; i < lines.length; i++) {
69
+ const propertyName = propertyNames.find((name) => lineStartsWithPropertyName({ line: lines[i], propertyName: name }));
70
+ if (!propertyName) {
71
+ continue;
72
+ }
73
+ const propertyIndentLength = getIndent(lines[i]).length;
74
+ let objectStart = i;
75
+ for (let j = i - 1; j >= 0; j--) {
76
+ const trimmed = lines[j].trim();
77
+ if (trimmed.endsWith('{') &&
78
+ getIndent(lines[j]).length < propertyIndentLength) {
79
+ objectStart = j;
80
+ break;
81
+ }
82
+ }
83
+ let objectEnd = i;
84
+ for (let j = i + 1; j < lines.length; j++) {
85
+ const trimmed = lines[j].trim();
86
+ if (trimmed.startsWith('}') &&
87
+ getIndent(lines[j]).length < propertyIndentLength) {
88
+ objectEnd = j;
89
+ break;
90
+ }
91
+ }
92
+ for (let j = objectStart + 1; j < objectEnd; j++) {
93
+ if (lines[j].trim() === '') {
94
+ linesToRemove.add(j);
95
+ }
96
+ }
97
+ }
98
+ if (linesToRemove.size === 0) {
99
+ return input;
100
+ }
101
+ return lines.filter((_, index) => !linesToRemove.has(index)).join('\n');
102
+ };
47
103
  const getSupportedCallArgument = (arg) => {
48
104
  if (arg === undefined ||
49
105
  arg.type === 'ArgumentPlaceholder' ||
@@ -168,6 +224,173 @@ const setOptionsProperty = ({ options, propertyName, value, }) => {
168
224
  }
169
225
  options.properties.push(b.objectProperty(b.identifier(propertyName), value));
170
226
  };
227
+ const isLinearEasing = (easing) => easing === 'linear';
228
+ const getKeyframeEasing = (node) => {
229
+ if (node.type === 'TSAsExpression') {
230
+ return getKeyframeEasing(node.expression);
231
+ }
232
+ if (node.type === 'MemberExpression' &&
233
+ node.object.type === 'Identifier' &&
234
+ node.object.name === 'Easing' &&
235
+ node.property.type === 'Identifier' &&
236
+ node.property.name === 'linear' &&
237
+ node.computed === false) {
238
+ return 'linear';
239
+ }
240
+ if (node.type !== 'CallExpression' ||
241
+ node.callee.type !== 'MemberExpression' ||
242
+ node.callee.object.type !== 'Identifier' ||
243
+ node.callee.object.name !== 'Easing' ||
244
+ node.callee.property.type !== 'Identifier' ||
245
+ node.callee.property.name !== 'bezier' ||
246
+ node.callee.computed ||
247
+ node.arguments.length !== 4) {
248
+ return null;
249
+ }
250
+ const values = node.arguments.map((arg) => {
251
+ if (arg.type === 'ArgumentPlaceholder' ||
252
+ arg.type === 'JSXNamespacedName' ||
253
+ arg.type === 'SpreadElement') {
254
+ return null;
255
+ }
256
+ return getNumericValue(arg);
257
+ });
258
+ if (values.some((value) => value === null)) {
259
+ return null;
260
+ }
261
+ return values;
262
+ };
263
+ const getKeyframeEasingArray = ({ easingNode, segmentCount, }) => {
264
+ if (segmentCount === 0) {
265
+ return [];
266
+ }
267
+ if (easingNode.type === 'TSAsExpression') {
268
+ return getKeyframeEasingArray({
269
+ easingNode: easingNode.expression,
270
+ segmentCount,
271
+ });
272
+ }
273
+ if (easingNode.type === 'ArrayExpression') {
274
+ if (easingNode.elements.length > segmentCount) {
275
+ return null;
276
+ }
277
+ const parsed = easingNode.elements.map((element) => {
278
+ if (!element || element.type === 'SpreadElement') {
279
+ return null;
280
+ }
281
+ return getKeyframeEasing(element);
282
+ });
283
+ if (parsed.some((value) => value === null)) {
284
+ return null;
285
+ }
286
+ const easingArray = parsed;
287
+ while (easingArray.length < segmentCount) {
288
+ easingArray.push('linear');
289
+ }
290
+ return easingArray;
291
+ }
292
+ const easing = getKeyframeEasing(easingNode);
293
+ if (!easing) {
294
+ return null;
295
+ }
296
+ return new Array(segmentCount).fill(easing);
297
+ };
298
+ const getExistingEasingArray = ({ options, segmentCount, }) => {
299
+ const { prop } = findObjectOptionProperty(options, 'easing');
300
+ if (!prop) {
301
+ return new Array(segmentCount).fill('linear');
302
+ }
303
+ const easing = getKeyframeEasingArray({
304
+ easingNode: prop.value,
305
+ segmentCount,
306
+ });
307
+ if (!easing) {
308
+ throw new Error('Cannot update easing: easing must be inline');
309
+ }
310
+ return easing;
311
+ };
312
+ const getExistingEasingArrayOrNull = ({ options, segmentCount, }) => {
313
+ const { prop } = findObjectOptionProperty(options, 'easing');
314
+ if (!prop) {
315
+ return null;
316
+ }
317
+ return getExistingEasingArray({ options, segmentCount });
318
+ };
319
+ const createEasingExpression = (easing) => {
320
+ if (easing === 'linear') {
321
+ return b.memberExpression(b.identifier('Easing'), b.identifier('linear'));
322
+ }
323
+ return b.callExpression(b.memberExpression(b.identifier('Easing'), b.identifier('bezier')), easing.map((value) => (0, update_nested_prop_1.parseValueExpression)(value)));
324
+ };
325
+ const createEasingArrayExpression = (easing) => b.arrayExpression(easing.map((easingValue) => createEasingExpression(easingValue)));
326
+ const setEasingOption = ({ options, easing, }) => {
327
+ const hasNonLinearEasing = easing.some((easingValue) => !isLinearEasing(easingValue));
328
+ setOptionsProperty({
329
+ options,
330
+ propertyName: 'easing',
331
+ value: hasNonLinearEasing ? createEasingArrayExpression(easing) : null,
332
+ });
333
+ return hasNonLinearEasing;
334
+ };
335
+ const getExtraArgsWithOptions = ({ extraArgs, options, }) => {
336
+ return options.properties.length === 0
337
+ ? extraArgs.slice(1)
338
+ : [options, ...extraArgs.slice(1)];
339
+ };
340
+ const getInlineOptionsFromExtraArgs = (extraArgs) => {
341
+ const existingOptions = extraArgs[0];
342
+ if (!existingOptions || existingOptions.type !== 'ObjectExpression') {
343
+ return null;
344
+ }
345
+ return existingOptions;
346
+ };
347
+ const normalizeEasingAfterAddingKeyframe = ({ extraArgs, previousSegmentCount, nextSegmentCount, }) => {
348
+ const options = getInlineOptionsFromExtraArgs(extraArgs);
349
+ if (!options) {
350
+ return { extraArgs, needsEasingImport: false };
351
+ }
352
+ const easing = getExistingEasingArrayOrNull({
353
+ options,
354
+ segmentCount: previousSegmentCount,
355
+ });
356
+ if (easing === null) {
357
+ return { extraArgs, needsEasingImport: false };
358
+ }
359
+ while (easing.length < nextSegmentCount) {
360
+ easing.push('linear');
361
+ }
362
+ return {
363
+ extraArgs: getExtraArgsWithOptions({ extraArgs, options }),
364
+ needsEasingImport: setEasingOption({ options, easing }),
365
+ };
366
+ };
367
+ const normalizeEasingAfterRemovingKeyframe = ({ extraArgs, previousSegmentCount, nextSegmentCount, removedKeyframeIndex, }) => {
368
+ const options = getInlineOptionsFromExtraArgs(extraArgs);
369
+ if (!options) {
370
+ return { extraArgs, needsEasingImport: false };
371
+ }
372
+ const easing = getExistingEasingArrayOrNull({
373
+ options,
374
+ segmentCount: previousSegmentCount,
375
+ });
376
+ if (easing === null) {
377
+ return { extraArgs, needsEasingImport: false };
378
+ }
379
+ if (easing.length > 0) {
380
+ const easingIndexToRemove = removedKeyframeIndex === 0 ? 0 : removedKeyframeIndex - 1;
381
+ easing.splice(easingIndexToRemove, 1);
382
+ }
383
+ while (easing.length > nextSegmentCount) {
384
+ easing.pop();
385
+ }
386
+ while (easing.length < nextSegmentCount) {
387
+ easing.push('linear');
388
+ }
389
+ return {
390
+ extraArgs: getExtraArgsWithOptions({ extraArgs, options }),
391
+ needsEasingImport: setEasingOption({ options, easing }),
392
+ };
393
+ };
171
394
  const validatePosterize = (posterize) => {
172
395
  if (posterize === undefined) {
173
396
  return;
@@ -227,6 +450,46 @@ const updateKeyframeSettings = ({ expression, clamping, posterize, }) => {
227
450
  keyframes: existing.keyframes,
228
451
  });
229
452
  };
453
+ const updateKeyframeEasing = ({ expression, segmentIndex, easing, }) => {
454
+ const existing = getInterpolationExpression(expression);
455
+ if (!existing) {
456
+ throw new Error('Cannot update easing on non-keyframed value');
457
+ }
458
+ const segmentCount = Math.max(0, existing.keyframes.length - 1);
459
+ if (!Number.isInteger(segmentIndex) ||
460
+ segmentIndex < 0 ||
461
+ segmentIndex >= segmentCount) {
462
+ throw new Error('Cannot update easing: segment index out of range');
463
+ }
464
+ const extraArgs = [...existing.extraArgs];
465
+ const existingOptions = extraArgs[0];
466
+ if (existingOptions && existingOptions.type !== 'ObjectExpression') {
467
+ throw new Error('Cannot update easing: options must be inline');
468
+ }
469
+ const options = (existingOptions === null || existingOptions === void 0 ? void 0 : existingOptions.type) === 'ObjectExpression'
470
+ ? existingOptions
471
+ : createEmptyOptionsExpression();
472
+ const nextEasing = getExistingEasingArray({ options, segmentCount });
473
+ nextEasing[segmentIndex] = easing;
474
+ const hasNonLinearEasing = nextEasing.some((easingValue) => !isLinearEasing(easingValue));
475
+ setOptionsProperty({
476
+ options,
477
+ propertyName: 'easing',
478
+ value: hasNonLinearEasing ? createEasingArrayExpression(nextEasing) : null,
479
+ });
480
+ const nextExtraArgs = options.properties.length === 0
481
+ ? extraArgs.slice(1)
482
+ : [options, ...extraArgs.slice(1)];
483
+ return {
484
+ expression: createInterpolateExpression({
485
+ callee: existing.callee,
486
+ input: existing.input,
487
+ extraArgs: nextExtraArgs,
488
+ keyframes: existing.keyframes,
489
+ }),
490
+ needsEasingImport: hasNonLinearEasing,
491
+ };
492
+ };
230
493
  const createInterpolateExpression = ({ callee, input, extraArgs, keyframes, }) => {
231
494
  const sortedKeyframes = [...keyframes].sort((first, second) => first.frame - second.frame);
232
495
  return b.callExpression(callee, [
@@ -239,6 +502,7 @@ const createInterpolateExpression = ({ callee, input, extraArgs, keyframes, }) =
239
502
  const noIntroducedIdentifiers = {
240
503
  calleeName: null,
241
504
  needsFrameHook: false,
505
+ needsEasingImport: false,
242
506
  };
243
507
  const addKeyframe = ({ expression, key, frame, value, schema, }) => {
244
508
  if (!(0, studio_shared_1.isSchemaFieldKeyframable)({ schema, key })) {
@@ -261,11 +525,18 @@ const addKeyframe = ({ expression, key, frame, value, schema, }) => {
261
525
  : existing.keyframes.map((keyframe, index) => index === existingIndex
262
526
  ? { frame, output: newOutput, value }
263
527
  : keyframe);
528
+ const normalizedEasing = existingIndex === -1
529
+ ? normalizeEasingAfterAddingKeyframe({
530
+ extraArgs: existing.extraArgs,
531
+ previousSegmentCount: Math.max(existing.keyframes.length - 1, 0),
532
+ nextSegmentCount: Math.max(nextKeyframes.length - 1, 0),
533
+ })
534
+ : { extraArgs: existing.extraArgs, needsEasingImport: false };
264
535
  return {
265
536
  expression: createInterpolateExpression({
266
537
  callee: b.identifier(nextCalleeName),
267
538
  input: existing.input,
268
- extraArgs: existing.extraArgs,
539
+ extraArgs: normalizedEasing.extraArgs,
269
540
  keyframes: nextKeyframes,
270
541
  }),
271
542
  introduced: {
@@ -273,6 +544,7 @@ const addKeyframe = ({ expression, key, frame, value, schema, }) => {
273
544
  ? schemaCalleeName
274
545
  : null,
275
546
  needsFrameHook: false,
547
+ needsEasingImport: normalizedEasing.needsEasingImport,
276
548
  },
277
549
  };
278
550
  }
@@ -302,6 +574,7 @@ const addKeyframe = ({ expression, key, frame, value, schema, }) => {
302
574
  ? callee.name
303
575
  : null,
304
576
  needsFrameHook: true,
577
+ needsEasingImport: false,
305
578
  },
306
579
  };
307
580
  };
@@ -316,14 +589,29 @@ const removeKeyframe = ({ expression, frame, }) => {
316
589
  }
317
590
  const nextKeyframes = existing.keyframes.filter((_keyframe, index) => index !== keyframeIndex);
318
591
  if (nextKeyframes.length === 0) {
319
- return existing.keyframes[keyframeIndex].output;
592
+ return {
593
+ expression: existing.keyframes[keyframeIndex].output,
594
+ introduced: noIntroducedIdentifiers,
595
+ };
320
596
  }
321
- return createInterpolateExpression({
322
- callee: existing.callee,
323
- input: existing.input,
597
+ const normalizedEasing = normalizeEasingAfterRemovingKeyframe({
324
598
  extraArgs: existing.extraArgs,
325
- keyframes: nextKeyframes,
599
+ previousSegmentCount: Math.max(existing.keyframes.length - 1, 0),
600
+ nextSegmentCount: Math.max(nextKeyframes.length - 1, 0),
601
+ removedKeyframeIndex: keyframeIndex,
326
602
  });
603
+ return {
604
+ expression: createInterpolateExpression({
605
+ callee: existing.callee,
606
+ input: existing.input,
607
+ extraArgs: normalizedEasing.extraArgs,
608
+ keyframes: nextKeyframes,
609
+ }),
610
+ introduced: {
611
+ ...noIntroducedIdentifiers,
612
+ needsEasingImport: normalizedEasing.needsEasingImport,
613
+ },
614
+ };
327
615
  };
328
616
  const moveKeyframes = ({ expression, moves, }) => {
329
617
  var _a;
@@ -395,16 +683,27 @@ const applyKeyframeOperation = ({ expression, key, operation, schema, }) => {
395
683
  introduced: noIntroducedIdentifiers,
396
684
  };
397
685
  }
686
+ if (operation.type === 'easing') {
687
+ const updated = updateKeyframeEasing({
688
+ expression,
689
+ segmentIndex: operation.segmentIndex,
690
+ easing: operation.easing,
691
+ });
692
+ return {
693
+ expression: updated.expression,
694
+ introduced: {
695
+ ...noIntroducedIdentifiers,
696
+ needsEasingImport: updated.needsEasingImport,
697
+ },
698
+ };
699
+ }
398
700
  if (operation.type === 'move') {
399
701
  return {
400
702
  expression: moveKeyframes({ expression, moves: operation.moves }),
401
703
  introduced: noIntroducedIdentifiers,
402
704
  };
403
705
  }
404
- return {
405
- expression: removeKeyframe({ expression, frame: operation.frame }),
406
- introduced: noIntroducedIdentifiers,
407
- };
706
+ return removeKeyframe({ expression, frame: operation.frame });
408
707
  };
409
708
  const getExpressionFromJsxAttribute = (attr) => {
410
709
  if (!attr.value) {
@@ -609,6 +908,9 @@ const updateSequenceKeyframesAst = ({ input, nodePath, updates, schema, }) => {
609
908
  requiredImports.add('useCurrentFrame');
610
909
  needsFrameHook = true;
611
910
  }
911
+ if (introduced.needsEasingImport) {
912
+ requiredImports.add('Easing');
913
+ }
612
914
  }
613
915
  if (needsFrameHook) {
614
916
  const fnPath = (0, ensure_imports_and_frame_hook_1.findEnclosingFunctionPath)(jsxPath);
@@ -641,8 +943,12 @@ const updateSequenceKeyframes = async ({ input, nodePath, updates, schema, prett
641
943
  input: serialized,
642
944
  prettierConfigOverride,
643
945
  });
946
+ const outputWithoutInsertedBlankLines = removeBlankLinesFromObjectsWithProperties({
947
+ input: output,
948
+ propertyNames: updates.map((update) => getObjectPropertyNameFromUpdateKey(update.key)),
949
+ });
644
950
  return {
645
- output,
951
+ output: outputWithoutInsertedBlankLines,
646
952
  formatted,
647
953
  oldValueStrings,
648
954
  newValueStrings,
@@ -699,6 +1005,9 @@ const updateEffectKeyframesAst = ({ input, sequenceNodePath, effectIndex, update
699
1005
  requiredImports.add('useCurrentFrame');
700
1006
  needsFrameHook = true;
701
1007
  }
1008
+ if (introduced.needsEasingImport) {
1009
+ requiredImports.add('Easing');
1010
+ }
702
1011
  }
703
1012
  if (needsFrameHook) {
704
1013
  const fnPath = (0, ensure_imports_and_frame_hook_1.findEnclosingFunctionPath)(jsxPath);
@@ -733,8 +1042,12 @@ const updateEffectKeyframes = async ({ input, sequenceNodePath, effectIndex, upd
733
1042
  input: serialized,
734
1043
  prettierConfigOverride,
735
1044
  });
1045
+ const outputWithoutInsertedBlankLines = removeBlankLinesFromObjectsWithProperties({
1046
+ input: output,
1047
+ propertyNames: updates.map((update) => update.key),
1048
+ });
736
1049
  return {
737
- output,
1050
+ output: outputWithoutInsertedBlankLines,
738
1051
  formatted,
739
1052
  oldValueStrings,
740
1053
  newValueStrings,
@@ -105,18 +105,23 @@ const ensureNamedImport = ({ ast, importedName, sourcePath, localName, }) => {
105
105
  };
106
106
  exports.ensureNamedImport = ensureNamedImport;
107
107
  const ensureNamedImports = ({ ast, importedNames, sourcePath, }) => {
108
- var _a, _b;
108
+ var _a;
109
+ var _b, _c, _d;
109
110
  if (importedNames.size === 0) {
110
111
  return;
111
112
  }
112
113
  const existingImports = findImportDeclarations(ast, sourcePath);
113
114
  const existingNames = new Set();
114
115
  for (const existingImportDeclaration of existingImports) {
115
- for (const importSpecifierCandidate of (_a = existingImportDeclaration.specifiers) !== null && _a !== void 0 ? _a : []) {
116
+ for (const importSpecifierCandidate of (_b = existingImportDeclaration.specifiers) !== null && _b !== void 0 ? _b : []) {
116
117
  if (importSpecifierCandidate.type !== 'ImportSpecifier') {
117
118
  continue;
118
119
  }
119
- existingNames.add((0, exports.getImportedName)(importSpecifierCandidate));
120
+ const importedName = (0, exports.getImportedName)(importSpecifierCandidate);
121
+ const localName = (_c = (_a = importSpecifierCandidate.local) === null || _a === void 0 ? void 0 : _a.name) !== null && _c !== void 0 ? _c : importedName;
122
+ if (localName === importedName) {
123
+ existingNames.add(importedName);
124
+ }
120
125
  }
121
126
  }
122
127
  const existingImport = existingImports.find((candidateImportDeclaration) => !hasNamespaceSpecifier(candidateImportDeclaration));
@@ -126,7 +131,7 @@ const ensureNamedImports = ({ ast, importedNames, sourcePath, }) => {
126
131
  continue;
127
132
  }
128
133
  existingImport.specifiers = [
129
- ...((_b = existingImport.specifiers) !== null && _b !== void 0 ? _b : []),
134
+ ...((_d = existingImport.specifiers) !== null && _d !== void 0 ? _d : []),
130
135
  b.importSpecifier(b.identifier(importedName)),
131
136
  ];
132
137
  existingNames.add(importedName);
@@ -548,12 +548,12 @@ const createComponentElement = ({ localName, props, }) => {
548
548
  createPositionAbsoluteStyleAttribute(),
549
549
  ], true), null, []);
550
550
  };
551
- const createAssetElement = ({ localName, staticFileLocalName, src, dimensions, }) => {
551
+ const createAssetElement = ({ addPositionStyle, localName, staticFileLocalName, src, dimensions, }) => {
552
552
  return recast.types.builders.jsxElement(recast.types.builders.jsxOpeningElement(recast.types.builders.jsxIdentifier(localName), [
553
553
  staticFileLocalName === null
554
554
  ? createStringSrcAttribute(src)
555
555
  : createStaticFileSrcAttribute({ staticFileLocalName, src }),
556
- createPositionAbsoluteStyleAttribute(),
556
+ ...(addPositionStyle ? [createPositionAbsoluteStyleAttribute()] : []),
557
557
  ...(dimensions
558
558
  ? [
559
559
  createNumberAttribute('width', dimensions.width),
@@ -1043,6 +1043,7 @@ const createInsertableJsxElement = ({ ast, element, }) => {
1043
1043
  throw new Error('Unsupported asset type');
1044
1044
  }
1045
1045
  return createAssetElement({
1046
+ addPositionStyle: element.assetType !== 'audio',
1046
1047
  localName,
1047
1048
  staticFileLocalName,
1048
1049
  src: element.src,
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.allApiRoutes = void 0;
4
4
  const add_effect_1 = require("./routes/add-effect");
5
5
  const add_effect_keyframe_1 = require("./routes/add-effect-keyframe");
6
+ const add_keyframes_1 = require("./routes/add-keyframes");
6
7
  const add_render_1 = require("./routes/add-render");
7
8
  const add_sequence_keyframe_1 = require("./routes/add-sequence-keyframe");
8
9
  const apply_codemod_1 = require("./routes/apply-codemod");
@@ -71,6 +72,7 @@ exports.allApiRoutes = {
71
72
  '/api/move-keyframes': move_keyframes_1.moveKeyframesHandler,
72
73
  '/api/add-sequence-keyframe': add_sequence_keyframe_1.addSequenceKeyframeHandler,
73
74
  '/api/add-effect-keyframe': add_effect_keyframe_1.addEffectKeyframeHandler,
75
+ '/api/add-keyframes': add_keyframes_1.addKeyframesHandler,
74
76
  '/api/update-sequence-keyframe-settings': update_sequence_keyframe_settings_1.updateSequenceKeyframeSettingsHandler,
75
77
  '/api/update-effect-keyframe-settings': update_effect_keyframe_settings_1.updateEffectKeyframeSettingsHandler,
76
78
  '/api/delete-effect': delete_effect_1.deleteEffectHandler,
@@ -0,0 +1,7 @@
1
+ import type { AddKeyframesRequest, AddKeyframesResponse } from '@remotion/studio-shared';
2
+ import type { ApiHandler } from '../api-types';
3
+ export declare const addKeyframes: ({ sequenceKeyframes, effectKeyframes, clientId, remotionRoot, logLevel, }: AddKeyframesRequest & {
4
+ readonly remotionRoot: string;
5
+ readonly logLevel: "error" | "info" | "trace" | "verbose" | "warn";
6
+ }) => Promise<void>;
7
+ export declare const addKeyframesHandler: ApiHandler<AddKeyframesRequest, AddKeyframesResponse>;
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addKeyframesHandler = exports.addKeyframes = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const renderer_1 = require("@remotion/renderer");
6
+ const update_keyframes_1 = require("../../codemods/update-keyframes/update-keyframes");
7
+ const file_watcher_1 = require("../../file-watcher");
8
+ const resolve_file_inside_project_1 = require("../../helpers/resolve-file-inside-project");
9
+ const undo_stack_1 = require("../undo-stack");
10
+ const watch_ignore_next_change_1 = require("../watch-ignore-next-change");
11
+ const log_effect_update_1 = require("./log-updates/log-effect-update");
12
+ const log_update_1 = require("./log-updates/log-update");
13
+ const save_props_mutex_1 = require("./save-props-mutex");
14
+ const groupBy = (items, getKey) => {
15
+ var _a;
16
+ const groups = new Map();
17
+ for (const item of items) {
18
+ const key = getKey(item);
19
+ const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : [];
20
+ group.push(item);
21
+ groups.set(key, group);
22
+ }
23
+ return [...groups.values()];
24
+ };
25
+ const getBatchDescription = ({ totalKeyframes, firstKeyframe, }) => {
26
+ if (totalKeyframes === 1) {
27
+ return {
28
+ undoMessage: `↩️ ${firstKeyframe.key} keyframe removed at frame ${firstKeyframe.frame}`,
29
+ redoMessage: `↪️ ${firstKeyframe.key} keyframe added at frame ${firstKeyframe.frame}`,
30
+ };
31
+ }
32
+ return {
33
+ undoMessage: `↩️ ${totalKeyframes} keyframes removed`,
34
+ redoMessage: `↪️ ${totalKeyframes} keyframes added`,
35
+ };
36
+ };
37
+ const addKeyframes = async ({ sequenceKeyframes, effectKeyframes, clientId, remotionRoot, logLevel, }) => {
38
+ var _a, _b;
39
+ const totalKeyframes = sequenceKeyframes.length + effectKeyframes.length;
40
+ if (totalKeyframes === 0) {
41
+ throw new Error('No keyframes were specified for adding');
42
+ }
43
+ const fileGroups = new Map();
44
+ for (const [index, keyframe] of sequenceKeyframes.entries()) {
45
+ const { absolutePath, fileRelativeToRoot } = (0, resolve_file_inside_project_1.resolveFileInsideProject)({
46
+ remotionRoot,
47
+ fileName: keyframe.fileName,
48
+ action: 'modify',
49
+ });
50
+ const group = (_a = fileGroups.get(absolutePath)) !== null && _a !== void 0 ? _a : {
51
+ fileRelativeToRoot,
52
+ sequenceKeyframes: [],
53
+ effectKeyframes: [],
54
+ };
55
+ group.sequenceKeyframes.push({
56
+ ...keyframe,
57
+ index,
58
+ absolutePath,
59
+ fileRelativeToRoot,
60
+ });
61
+ fileGroups.set(absolutePath, group);
62
+ }
63
+ for (const [index, keyframe] of effectKeyframes.entries()) {
64
+ const { absolutePath, fileRelativeToRoot } = (0, resolve_file_inside_project_1.resolveFileInsideProject)({
65
+ remotionRoot,
66
+ fileName: keyframe.fileName,
67
+ action: 'modify',
68
+ });
69
+ const group = (_b = fileGroups.get(absolutePath)) !== null && _b !== void 0 ? _b : {
70
+ fileRelativeToRoot,
71
+ sequenceKeyframes: [],
72
+ effectKeyframes: [],
73
+ };
74
+ group.effectKeyframes.push({
75
+ ...keyframe,
76
+ index,
77
+ absolutePath,
78
+ fileRelativeToRoot,
79
+ });
80
+ fileGroups.set(absolutePath, group);
81
+ }
82
+ const snapshots = [];
83
+ const sequenceLogs = [];
84
+ const effectLogs = [];
85
+ for (const [absolutePath, group] of fileGroups) {
86
+ const fileContents = (0, node_fs_1.readFileSync)(absolutePath, 'utf-8');
87
+ let output = fileContents;
88
+ let firstLogLine = Number.POSITIVE_INFINITY;
89
+ for (const keyframeGroup of groupBy(group.sequenceKeyframes, (keyframe) => JSON.stringify(keyframe.nodePath.nodePath))) {
90
+ const [firstSequenceKeyframe] = keyframeGroup;
91
+ if (!firstSequenceKeyframe) {
92
+ continue;
93
+ }
94
+ const result = await (0, update_keyframes_1.updateSequenceKeyframes)({
95
+ input: output,
96
+ nodePath: firstSequenceKeyframe.nodePath.nodePath,
97
+ schema: firstSequenceKeyframe.schema,
98
+ updates: keyframeGroup.map((keyframe) => ({
99
+ key: keyframe.key,
100
+ operation: {
101
+ type: 'add',
102
+ frame: keyframe.frame,
103
+ value: JSON.parse(keyframe.value),
104
+ },
105
+ })),
106
+ });
107
+ output = result.output;
108
+ firstLogLine = Math.min(firstLogLine, result.logLine);
109
+ for (const [keyframeIndex, keyframe] of keyframeGroup.entries()) {
110
+ sequenceLogs.push({
111
+ fileRelativeToRoot: keyframe.fileRelativeToRoot,
112
+ line: result.logLine,
113
+ key: keyframe.key,
114
+ oldValueString: result.oldValueStrings[keyframeIndex],
115
+ newValueString: result.newValueStrings[keyframeIndex],
116
+ formatted: result.formatted,
117
+ });
118
+ }
119
+ }
120
+ for (const keyframeGroup of groupBy(group.effectKeyframes, (keyframe) => `${JSON.stringify(keyframe.sequenceNodePath.nodePath)}:${keyframe.effectIndex}`)) {
121
+ const [firstEffectKeyframe] = keyframeGroup;
122
+ if (!firstEffectKeyframe) {
123
+ continue;
124
+ }
125
+ const result = await (0, update_keyframes_1.updateEffectKeyframes)({
126
+ input: output,
127
+ sequenceNodePath: firstEffectKeyframe.sequenceNodePath.nodePath,
128
+ effectIndex: firstEffectKeyframe.effectIndex,
129
+ schema: firstEffectKeyframe.schema,
130
+ updates: keyframeGroup.map((keyframe) => ({
131
+ key: keyframe.key,
132
+ operation: {
133
+ type: 'add',
134
+ frame: keyframe.frame,
135
+ value: JSON.parse(keyframe.value),
136
+ },
137
+ })),
138
+ });
139
+ output = result.output;
140
+ firstLogLine = Math.min(firstLogLine, result.logLine);
141
+ for (const [keyframeIndex, keyframe] of keyframeGroup.entries()) {
142
+ effectLogs.push({
143
+ fileRelativeToRoot: keyframe.fileRelativeToRoot,
144
+ line: result.logLine,
145
+ effectName: result.effectCallee,
146
+ propKey: keyframe.key,
147
+ oldValueString: result.oldValueStrings[keyframeIndex],
148
+ newValueString: result.newValueStrings[keyframeIndex],
149
+ formatted: result.formatted,
150
+ });
151
+ }
152
+ }
153
+ snapshots.push({
154
+ filePath: absolutePath,
155
+ oldContents: fileContents,
156
+ newContents: output,
157
+ logLine: Number.isFinite(firstLogLine) ? firstLogLine : 1,
158
+ });
159
+ }
160
+ const [firstKeyframe] = sequenceKeyframes.length > 0 ? sequenceKeyframes : effectKeyframes;
161
+ if (!firstKeyframe) {
162
+ throw new Error('No keyframes were specified for adding');
163
+ }
164
+ (0, undo_stack_1.pushTransactionToUndoStack)({
165
+ snapshots,
166
+ logLevel,
167
+ remotionRoot,
168
+ description: getBatchDescription({ totalKeyframes, firstKeyframe }),
169
+ entryType: sequenceKeyframes.length > 0 && effectKeyframes.length > 0
170
+ ? // Dead code for now: sequence props and effect props cannot be selected
171
+ // together yet. Keep the mixed transaction type for the planned UI.
172
+ 'keyframe-add'
173
+ : sequenceKeyframes.length > 0
174
+ ? 'sequence-props'
175
+ : 'effect-props',
176
+ suppressHmrOnFileRestore: true,
177
+ });
178
+ for (const snapshot of snapshots) {
179
+ (0, undo_stack_1.suppressUndoStackInvalidation)(snapshot.filePath);
180
+ (0, watch_ignore_next_change_1.suppressBundlerUpdateForFile)(snapshot.filePath);
181
+ (0, file_watcher_1.writeFileAndNotifyFileWatchers)(snapshot.filePath, snapshot.newContents, clientId);
182
+ }
183
+ for (const log of sequenceLogs) {
184
+ (0, log_update_1.logUpdate)({
185
+ fileRelativeToRoot: log.fileRelativeToRoot,
186
+ line: log.line,
187
+ key: log.key,
188
+ oldValueString: log.oldValueString,
189
+ newValueString: log.newValueString,
190
+ defaultValueString: null,
191
+ formatted: log.formatted,
192
+ logLevel,
193
+ removedProps: [],
194
+ addedProps: [],
195
+ });
196
+ }
197
+ for (const log of effectLogs) {
198
+ (0, log_effect_update_1.logEffectUpdate)({
199
+ fileRelativeToRoot: log.fileRelativeToRoot,
200
+ line: log.line,
201
+ effectName: log.effectName,
202
+ propKey: log.propKey,
203
+ oldValueString: log.oldValueString,
204
+ newValueString: log.newValueString,
205
+ defaultValueString: null,
206
+ formatted: log.formatted,
207
+ logLevel,
208
+ removedProps: [],
209
+ addedProps: [],
210
+ });
211
+ }
212
+ (0, undo_stack_1.printUndoHint)(logLevel);
213
+ };
214
+ exports.addKeyframes = addKeyframes;
215
+ const addKeyframesHandler = ({ input: { sequenceKeyframes, effectKeyframes, clientId }, remotionRoot, logLevel, }) => (0, save_props_mutex_1.withSavePropsLock)(async () => {
216
+ renderer_1.RenderInternals.Log.trace({ indent: false, logLevel }, `[add-keyframes] Received request to add ${sequenceKeyframes.length + effectKeyframes.length} keyframe(s)`);
217
+ await (0, exports.addKeyframes)({
218
+ sequenceKeyframes,
219
+ effectKeyframes,
220
+ clientId,
221
+ remotionRoot,
222
+ logLevel,
223
+ });
224
+ return { success: true };
225
+ });
226
+ exports.addKeyframesHandler = addKeyframesHandler;
@@ -260,9 +260,6 @@ const getInterpolationMetadata = (interpolationFunction, callExpression, keyfram
260
260
  }
261
261
  const value = property.value;
262
262
  if (key === 'easing') {
263
- if (interpolationFunction === 'interpolateColors') {
264
- return null;
265
- }
266
263
  const parsedEasing = getKeyframeEasingArray({
267
264
  easingNode: value,
268
265
  segments,
@@ -10,7 +10,7 @@ const formatInnerPropChange = ({ key, oldValueString, newValueString, defaultVal
10
10
  if (defaultValueString !== null && oldValueString === defaultValueString) {
11
11
  return (0, formatting_1.formatAddition)({ valueString: newValueString, key });
12
12
  }
13
- return `${(0, formatting_1.formatPropDelta)({ valueString: oldValueString, key })} \u2192 ${(0, formatting_1.formatPropDelta)({ valueString: newValueString, key })}`;
13
+ return (0, formatting_1.formatPropChangeDelta)({ key, oldValueString, newValueString });
14
14
  };
15
15
  const formatPropChange = ({ key, oldValueString, newValueString, defaultValueString, removedProps, addedProps, }) => {
16
16
  const suffix = (0, format_side_props_1.formatSideProps)({ removedProps, addedProps });
@@ -7,6 +7,7 @@ export declare const equals: (str: string) => string;
7
7
  export declare const punctuation: (str: string) => string;
8
8
  export declare const stringValue: (str: string) => string;
9
9
  export declare const numberValue: (str: string) => string;
10
+ export declare const inlineAddition: (str: string) => string;
10
11
  export declare const strikeThrough: (str: string) => string;
11
12
  export declare const strikeThroughOrRemovedPrefix: (str: string) => string;
12
13
  export declare const addedPrefixIfNoColor: (str: string) => string;
@@ -15,5 +16,10 @@ export type PropDelta = {
15
16
  valueString: string;
16
17
  };
17
18
  export declare const formatPropDelta: ({ key, valueString }: PropDelta) => string;
19
+ export declare const formatPropChangeDelta: ({ key, oldValueString, newValueString, }: {
20
+ key: string;
21
+ oldValueString: string;
22
+ newValueString: string;
23
+ }) => string;
18
24
  export declare const formatDeletion: (prop: PropDelta) => string;
19
25
  export declare const formatAddition: (prop: PropDelta) => string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatAddition = exports.formatDeletion = exports.formatPropDelta = exports.addedPrefixIfNoColor = exports.strikeThroughOrRemovedPrefix = exports.strikeThrough = exports.numberValue = exports.stringValue = exports.punctuation = exports.equals = exports.attrName = exports.bg = exports.fg = exports.colorValue = exports.colorEnabled = void 0;
3
+ exports.formatAddition = exports.formatDeletion = exports.formatPropChangeDelta = exports.formatPropDelta = exports.addedPrefixIfNoColor = exports.strikeThroughOrRemovedPrefix = exports.strikeThrough = exports.inlineAddition = exports.numberValue = exports.stringValue = exports.punctuation = exports.equals = exports.attrName = exports.bg = exports.fg = exports.colorValue = exports.colorEnabled = void 0;
4
4
  const renderer_1 = require("@remotion/renderer");
5
5
  const colorEnabled = () => renderer_1.RenderInternals.chalk.enabled();
6
6
  exports.colorEnabled = colorEnabled;
@@ -35,6 +35,8 @@ const stringValue = (str) => (0, exports.fg)(230, 219, 116, str);
35
35
  exports.stringValue = stringValue;
36
36
  const numberValue = (str) => (0, exports.fg)(174, 129, 255, str);
37
37
  exports.numberValue = numberValue;
38
+ const inlineAddition = (str) => (0, exports.fg)(166, 226, 46, str);
39
+ exports.inlineAddition = inlineAddition;
38
40
  const strikeThrough = (str) => `\u001b[9m\u001b[38;2;255;85;85m${stripAnsi(str)}\u001b[39m\u001b[29m`;
39
41
  exports.strikeThrough = strikeThrough;
40
42
  const strikeThroughOrRemovedPrefix = (str) => (0, exports.colorEnabled)() ? (0, exports.strikeThrough)(str) : 'removed: ' + str;
@@ -49,6 +51,25 @@ const formatSimpleProp = (key, value) => {
49
51
  const formatNestedProp = (parentKey, childKey, value) => {
50
52
  return `${(0, exports.attrName)(parentKey)}${(0, exports.equals)('=')}${(0, exports.punctuation)('{{')}${(0, exports.punctuation)(childKey)}${(0, exports.punctuation)(':')} ${(0, exports.colorValue)(value)}${(0, exports.punctuation)('}}')}`;
51
53
  };
54
+ const formatValueDelta = ({ oldValueString, newValueString, }) => {
55
+ const withoutUnchangedOptions = shortenUnchangedInterpolateOptions({
56
+ oldValueString,
57
+ newValueString,
58
+ });
59
+ const shortened = removeUnchangedInterpolateOptionProperties(withoutUnchangedOptions);
60
+ const inlineDelta = formatInlineAdditionOrRemoval(shortened);
61
+ if (inlineDelta !== null) {
62
+ return inlineDelta;
63
+ }
64
+ return `${(0, exports.colorValue)(shortened.oldValueString)} ${(0, exports.punctuation)('→')} ${(0, exports.colorValue)(shortened.newValueString)}`;
65
+ };
66
+ const formatSimplePropChange = ({ key, oldValueString, newValueString, }) => {
67
+ return `${(0, exports.attrName)(key)}${(0, exports.equals)('=')}${(0, exports.punctuation)('{')}${formatValueDelta({ oldValueString, newValueString })}${(0, exports.punctuation)('}')}`;
68
+ };
69
+ const formatNestedPropChange = ({ key, oldValueString, newValueString, }) => {
70
+ const dotIdx = key.indexOf('.');
71
+ return `${(0, exports.attrName)(key.slice(0, dotIdx))}${(0, exports.equals)('=')}${(0, exports.punctuation)('{{')}${(0, exports.punctuation)(key.slice(dotIdx + 1))}${(0, exports.punctuation)(':')} ${formatValueDelta({ oldValueString, newValueString })}${(0, exports.punctuation)('}}')}`;
72
+ };
52
73
  const formatPropDelta = ({ key, valueString }) => {
53
74
  const dotIdx = key.indexOf('.');
54
75
  if (dotIdx === -1) {
@@ -57,6 +78,14 @@ const formatPropDelta = ({ key, valueString }) => {
57
78
  return formatNestedProp(key.slice(0, dotIdx), key.slice(dotIdx + 1), valueString);
58
79
  };
59
80
  exports.formatPropDelta = formatPropDelta;
81
+ const formatPropChangeDelta = ({ key, oldValueString, newValueString, }) => {
82
+ const dotIdx = key.indexOf('.');
83
+ if (dotIdx === -1) {
84
+ return formatSimplePropChange({ key, oldValueString, newValueString });
85
+ }
86
+ return formatNestedPropChange({ key, oldValueString, newValueString });
87
+ };
88
+ exports.formatPropChangeDelta = formatPropChangeDelta;
60
89
  const formatDeletion = (prop) => {
61
90
  const formatted = (0, exports.formatPropDelta)(prop);
62
91
  return (0, exports.strikeThroughOrRemovedPrefix)(formatted);
@@ -67,3 +96,229 @@ const formatAddition = (prop) => {
67
96
  return (0, exports.addedPrefixIfNoColor)(formatted);
68
97
  };
69
98
  exports.formatAddition = formatAddition;
99
+ const callStart = 'interpolate(';
100
+ const normalizeArg = (arg) => {
101
+ return arg
102
+ .replace(/\s+/g, ' ')
103
+ .replace(/,(\s*[}\]])/g, '$1')
104
+ .trim();
105
+ };
106
+ const shortenUnchangedOptionProperties = new Set([
107
+ 'extrapolateLeft',
108
+ 'extrapolateRight',
109
+ ]);
110
+ const splitTopLevelArgs = (argsSource) => {
111
+ const args = [];
112
+ let depth = 0;
113
+ let quote = null;
114
+ let start = 0;
115
+ for (let i = 0; i < argsSource.length; i++) {
116
+ const char = argsSource[i];
117
+ const previous = argsSource[i - 1];
118
+ if (quote) {
119
+ if (char === quote && previous !== '\\') {
120
+ quote = null;
121
+ }
122
+ continue;
123
+ }
124
+ if (char === "'" || char === '"' || char === '`') {
125
+ quote = char;
126
+ continue;
127
+ }
128
+ if (char === '(' || char === '[' || char === '{') {
129
+ depth++;
130
+ continue;
131
+ }
132
+ if (char === ')' || char === ']' || char === '}') {
133
+ depth--;
134
+ continue;
135
+ }
136
+ if (char === ',' && depth === 0) {
137
+ args.push(argsSource.slice(start, i).trim());
138
+ start = i + 1;
139
+ }
140
+ }
141
+ args.push(argsSource.slice(start).trim());
142
+ return args;
143
+ };
144
+ const getCommonPrefixLength = (oldValueString, newValueString) => {
145
+ const maxLength = Math.min(oldValueString.length, newValueString.length);
146
+ let index = 0;
147
+ while (index < maxLength && oldValueString[index] === newValueString[index]) {
148
+ index++;
149
+ }
150
+ return index;
151
+ };
152
+ const getCommonSuffixLength = ({ oldValueString, newValueString, prefixLength, }) => {
153
+ const maxLength = Math.min(oldValueString.length, newValueString.length) - prefixLength;
154
+ let index = 0;
155
+ while (index < maxLength &&
156
+ oldValueString[oldValueString.length - 1 - index] ===
157
+ newValueString[newValueString.length - 1 - index]) {
158
+ index++;
159
+ }
160
+ return index;
161
+ };
162
+ const minSharedInlineDeltaChars = 12;
163
+ const formatInlineAdditionOrRemoval = ({ oldValueString, newValueString, }) => {
164
+ if (!(0, exports.colorEnabled)()) {
165
+ return null;
166
+ }
167
+ const prefixLength = getCommonPrefixLength(oldValueString, newValueString);
168
+ const suffixLength = getCommonSuffixLength({
169
+ oldValueString,
170
+ newValueString,
171
+ prefixLength,
172
+ });
173
+ if (prefixLength + suffixLength < minSharedInlineDeltaChars) {
174
+ return null;
175
+ }
176
+ const oldMiddleEnd = oldValueString.length - suffixLength;
177
+ const newMiddleEnd = newValueString.length - suffixLength;
178
+ const oldMiddle = oldValueString.slice(prefixLength, oldMiddleEnd);
179
+ const newMiddle = newValueString.slice(prefixLength, newMiddleEnd);
180
+ if (oldMiddle.length === 0 && newMiddle.length > 0) {
181
+ return `${newValueString.slice(0, prefixLength)}${(0, exports.inlineAddition)(newMiddle)}${newValueString.slice(newMiddleEnd)}`;
182
+ }
183
+ if (newMiddle.length === 0 && oldMiddle.length > 0) {
184
+ return `${oldValueString.slice(0, prefixLength)}${(0, exports.strikeThrough)(oldMiddle)}${oldValueString.slice(oldMiddleEnd)}`;
185
+ }
186
+ return null;
187
+ };
188
+ const getObjectPropertyKey = (propertySource) => {
189
+ const colonIndex = propertySource.indexOf(':');
190
+ if (colonIndex === -1) {
191
+ return null;
192
+ }
193
+ const key = propertySource.slice(0, colonIndex).trim();
194
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) {
195
+ return key;
196
+ }
197
+ if ((key.startsWith("'") && key.endsWith("'")) ||
198
+ (key.startsWith('"') && key.endsWith('"'))) {
199
+ return key.slice(1, -1);
200
+ }
201
+ return null;
202
+ };
203
+ const parseTopLevelObjectProperties = (objectSource) => {
204
+ const trimmed = objectSource.trim();
205
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
206
+ return null;
207
+ }
208
+ const properties = splitTopLevelArgs(trimmed.slice(1, -1))
209
+ .filter(Boolean)
210
+ .map((propertySource) => {
211
+ const key = getObjectPropertyKey(propertySource);
212
+ if (key === null) {
213
+ return null;
214
+ }
215
+ const colonIndex = propertySource.indexOf(':');
216
+ return {
217
+ key,
218
+ value: normalizeArg(propertySource.slice(colonIndex + 1)),
219
+ source: propertySource.trim(),
220
+ };
221
+ });
222
+ if (properties.some((property) => property === null)) {
223
+ return null;
224
+ }
225
+ return properties;
226
+ };
227
+ const parseInterpolateCall = (valueString) => {
228
+ const trimmed = valueString.trim();
229
+ if (!trimmed.startsWith(callStart) || !trimmed.endsWith(')')) {
230
+ return null;
231
+ }
232
+ let depth = 0;
233
+ let quote = null;
234
+ for (let i = 'interpolate'.length; i < trimmed.length; i++) {
235
+ const char = trimmed[i];
236
+ const previous = trimmed[i - 1];
237
+ if (quote) {
238
+ if (char === quote && previous !== '\\') {
239
+ quote = null;
240
+ }
241
+ continue;
242
+ }
243
+ if (char === "'" || char === '"' || char === '`') {
244
+ quote = char;
245
+ continue;
246
+ }
247
+ if (char === '(' || char === '[' || char === '{') {
248
+ depth++;
249
+ continue;
250
+ }
251
+ if (char === ')' || char === ']' || char === '}') {
252
+ depth--;
253
+ if (depth === 0 && i !== trimmed.length - 1) {
254
+ return null;
255
+ }
256
+ }
257
+ }
258
+ if (depth !== 0) {
259
+ return null;
260
+ }
261
+ return {
262
+ args: splitTopLevelArgs(trimmed.slice(callStart.length, -1)),
263
+ };
264
+ };
265
+ const shortenUnchangedInterpolateOptions = ({ oldValueString, newValueString, }) => {
266
+ const oldCall = parseInterpolateCall(oldValueString);
267
+ const newCall = parseInterpolateCall(newValueString);
268
+ if (!oldCall ||
269
+ !newCall ||
270
+ oldCall.args.length <= 3 ||
271
+ newCall.args.length <= 3) {
272
+ return { oldValueString, newValueString };
273
+ }
274
+ const oldOptions = oldCall.args.slice(3).map(normalizeArg);
275
+ const newOptions = newCall.args.slice(3).map(normalizeArg);
276
+ if (oldOptions.length !== newOptions.length ||
277
+ oldOptions.some((option, index) => option !== newOptions[index])) {
278
+ return { oldValueString, newValueString };
279
+ }
280
+ return {
281
+ oldValueString: `interpolate(${oldCall.args.slice(0, 3).join(', ')})`,
282
+ newValueString: `interpolate(${newCall.args.slice(0, 3).join(', ')})`,
283
+ };
284
+ };
285
+ const removeUnchangedInterpolateOptionProperties = ({ oldValueString, newValueString, }) => {
286
+ const oldCall = parseInterpolateCall(oldValueString);
287
+ const newCall = parseInterpolateCall(newValueString);
288
+ if (!oldCall ||
289
+ !newCall ||
290
+ oldCall.args.length <= 3 ||
291
+ newCall.args.length <= 3) {
292
+ return { oldValueString, newValueString };
293
+ }
294
+ const oldProperties = parseTopLevelObjectProperties(oldCall.args[3]);
295
+ const newProperties = parseTopLevelObjectProperties(newCall.args[3]);
296
+ if (!oldProperties || !newProperties) {
297
+ return { oldValueString, newValueString };
298
+ }
299
+ const propertiesToRemove = [...shortenUnchangedOptionProperties].filter((propertyName) => {
300
+ const oldProperty = oldProperties.find((property) => property.key === propertyName);
301
+ const newProperty = newProperties.find((property) => property.key === propertyName);
302
+ return (oldProperty !== undefined &&
303
+ newProperty !== undefined &&
304
+ oldProperty.value === newProperty.value);
305
+ });
306
+ if (propertiesToRemove.length === 0) {
307
+ return { oldValueString, newValueString };
308
+ }
309
+ const removeProperties = (call, properties) => {
310
+ const nextOptions = properties.filter((property) => !propertiesToRemove.includes(property.key));
311
+ const nextArgs = nextOptions.length === 0
312
+ ? [...call.args.slice(0, 3), ...call.args.slice(4)]
313
+ : [
314
+ ...call.args.slice(0, 3),
315
+ `{${nextOptions.map((property) => property.source).join(', ')}}`,
316
+ ...call.args.slice(4),
317
+ ];
318
+ return `interpolate(${nextArgs.join(', ')})`;
319
+ };
320
+ return {
321
+ oldValueString: removeProperties(oldCall, oldProperties),
322
+ newValueString: removeProperties(newCall, newProperties),
323
+ };
324
+ };
@@ -30,11 +30,17 @@ const updateEffectKeyframeSettingsHandler = ({ input: { fileName, sequenceNodePa
30
30
  updates: [
31
31
  {
32
32
  key,
33
- operation: {
34
- type: 'settings',
35
- clamping: settings.clamping,
36
- posterize: settings.posterize,
37
- },
33
+ operation: settings.type === 'settings'
34
+ ? {
35
+ type: 'settings',
36
+ clamping: settings.clamping,
37
+ posterize: settings.posterize,
38
+ }
39
+ : {
40
+ type: 'easing',
41
+ segmentIndex: settings.segmentIndex,
42
+ easing: settings.easing,
43
+ },
38
44
  },
39
45
  ],
40
46
  });
@@ -27,11 +27,17 @@ const updateSequenceKeyframeSettingsHandler = ({ input: { fileName, nodePath, ke
27
27
  updates: [
28
28
  {
29
29
  key,
30
- operation: {
31
- type: 'settings',
32
- clamping: settings.clamping,
33
- posterize: settings.posterize,
34
- },
30
+ operation: settings.type === 'settings'
31
+ ? {
32
+ type: 'settings',
33
+ clamping: settings.clamping,
34
+ posterize: settings.posterize,
35
+ }
36
+ : {
37
+ type: 'easing',
38
+ segmentIndex: settings.segmentIndex,
39
+ easing: settings.easing,
40
+ },
35
41
  },
36
42
  ],
37
43
  });
@@ -3,7 +3,7 @@ export interface UndoEntryDescription {
3
3
  undoMessage: string;
4
4
  redoMessage: string;
5
5
  }
6
- type UndoEntryType = 'visual-control' | 'default-props' | 'sequence-props' | 'effect-props' | 'keyframe-delete' | 'add-effect' | 'delete-effect' | 'paste-effects' | 'reorder-effect' | 'reorder-sequence' | 'delete-jsx-node' | 'duplicate-jsx-node' | 'insert-jsx-element' | 'delete-composition' | 'rename-composition' | 'duplicate-composition' | 'delete-folder' | 'rename-folder';
6
+ type UndoEntryType = 'visual-control' | 'default-props' | 'sequence-props' | 'effect-props' | 'keyframe-add' | 'keyframe-delete' | 'add-effect' | 'delete-effect' | 'paste-effects' | 'reorder-effect' | 'reorder-sequence' | 'delete-jsx-node' | 'duplicate-jsx-node' | 'insert-jsx-element' | 'delete-composition' | 'rename-composition' | 'duplicate-composition' | 'delete-folder' | 'rename-folder';
7
7
  type UndoEntrySnapshot = {
8
8
  filePath: string;
9
9
  oldContents: string;
@@ -27,6 +27,8 @@ type UndoEntry = {
27
27
  entryType: 'sequence-props';
28
28
  } | {
29
29
  entryType: 'effect-props';
30
+ } | {
31
+ entryType: 'keyframe-add';
30
32
  } | {
31
33
  entryType: 'keyframe-delete';
32
34
  } | {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/studio-server"
4
4
  },
5
5
  "name": "@remotion/studio-server",
6
- "version": "4.0.474",
6
+ "version": "4.0.475",
7
7
  "description": "Run a Remotion Studio with a server backend",
8
8
  "main": "dist",
9
9
  "scripts": {
@@ -27,11 +27,11 @@
27
27
  "@babel/parser": "7.24.1",
28
28
  "semver": "7.5.3",
29
29
  "prettier": "3.8.1",
30
- "remotion": "4.0.474",
30
+ "remotion": "4.0.475",
31
31
  "recast": "0.23.11",
32
- "@remotion/bundler": "4.0.474",
33
- "@remotion/renderer": "4.0.474",
34
- "@remotion/studio-shared": "4.0.474",
32
+ "@remotion/bundler": "4.0.475",
33
+ "@remotion/renderer": "4.0.475",
34
+ "@remotion/studio-shared": "4.0.475",
35
35
  "memfs": "3.4.3",
36
36
  "open": "8.4.2"
37
37
  },
@@ -39,7 +39,7 @@
39
39
  "ast-types": "0.16.1",
40
40
  "react": "19.2.3",
41
41
  "@types/semver": "7.5.3",
42
- "@remotion/eslint-config-internal": "4.0.474",
42
+ "@remotion/eslint-config-internal": "4.0.475",
43
43
  "eslint": "9.19.0",
44
44
  "@types/node": "20.12.14",
45
45
  "@typescript/native-preview": "7.0.0-dev.20260217.1"