#7607·refine

`spring` returns `NaN` and breaks physics simulation when `config` has `undefined` properties

Author: codeCraft-RitikCreated Sep 16, 2026Updated Sep 17, 2026
Labelsbug

Describe the bug

Describe the bug

When calling spring() with a config object containing undefined properties (common when passing optional React props: { damping: props.damping, stiffness: props.stiffness }), springCalculation merges the configuration with object spread:

In packages/core/src/spring/spring-utils.ts:

typescript
config: {
  ...defaultSpringConfig,
  ...config,
}

If stiffness or mass is undefined, it overrides defaultSpringConfig's value with undefined. In advance(), Math.sqrt(k / m) calculates Math.sqrt(undefined / 1) = NaN, causing spring() to return NaN for every frame.

If damping is undefined, zeta evaluates to NaN. Because NaN < 1 is false, it bypasses underDampedPosition and silently forces the spring into criticallyDampedPosition, breaking underdamped spring physics without error.

Steps To Reproduce

Steps To Reproduce

  1. Call spring with an optional property explicitly set to undefined:
typescript
import { spring } from 'remotion';

const value = spring({
  frame: 10,
  fps: 30,
  config: {
    damping: 10,
    stiffness: undefined,
  },
});

### Expected behavior

### Expected behavior
`springCalculation` should fall back to `defaultSpringConfig` values using nullish coalescing (`config.stiffness ?? defaultSpringConfig.stiffness`), gracefully handling partial and optional configuration objects.

### Suggested Fix
```diff
--- a/packages/core/src/spring/spring-utils.ts
+++ b/packages/core/src/spring/spring-utils.ts
@@ -14,7 +14,7 @@ export type SpringConfig = {
 	overshootClamping: boolean;
 };
 
-const defaultSpringConfig: SpringConfig = {
+export const defaultSpringConfig: SpringConfig = {
 	damping: 10,
 	mass: 1,
 	stiffness: 100,
@@ -118,12 +118,18 @@ export function springCalculation({
 	const from = 0;
 	const to = 1;
+	const resolvedConfig: SpringConfig = {
+		damping: config.damping ?? defaultSpringConfig.damping,
+		mass: config.mass ?? defaultSpringConfig.mass,
+		stiffness: config.stiffness ?? defaultSpringConfig.stiffness,
+		overshootClamping:
+			config.overshootClamping ?? defaultSpringConfig.overshootClamping,
+	};
 	const cacheKey = [
 		frame,
 		fps,
-		config.damping,
-		config.mass,
-		config.overshootClamping,
-		config.stiffness,
+		resolvedConfig.damping,
+		resolvedConfig.mass,
+		resolvedConfig.overshootClamping,
+		resolvedConfig.stiffness,
 	].join('-');
 	if (calculationCache[cacheKey]) {
 		return calculationCache[cacheKey];
@@ -144,10 +150,7 @@ export function springCalculation({
 		animation = advance({
 			animation,
 			now: time,
-			config: {
-				...defaultSpringConfig,
-				...config,
-			},
+			config: resolvedConfig,
 		});
 	}
 
@@ -156,10 +159,7 @@ export function springCalculation({
 		animation = advance({
 			animation,
 			now: (frameClamped / fps) * 1000,
-			config: {
-				...defaultSpringConfig,
-				...config,
-			},
+			config: resolvedConfig,
 		});
 	}

Packages

Packages

@remotion/core

Additional Context

No response