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
4 changes: 3 additions & 1 deletion bun.lock
100755 → 100644

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.2",
"input-otp": "^1.4.2",
"lottie-web": "^5.13.0",
"lucide-react": "^0.525.0",
"next": "^16.1.1",
"next-auth": "^4.24.11",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
'use client';

import React from 'react';
import { Player, AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig } from 'motionforge';
import {
Player,
AbsoluteFill,
useCurrentFrame,
interpolate,
spring,
useVideoConfig,
Lottie,
Sequence
} from 'motionforge';

const HelloWorldComposition = () => {
const frame = useCurrentFrame();
Expand All @@ -25,12 +34,26 @@ const HelloWorldComposition = () => {
style={{
opacity,
transform: `scale(${scale}) translateY(${translateY}px)`,
textAlign: 'center'
textAlign: 'center',
zIndex: 10,
}}
>
<h1 className="text-7xl text-emerald-400 font-black mb-4">Hello MotionForge!</h1>
<p className="text-2xl text-emerald-600">Your programmatic video journey starts here.</p>
</div>

{/* Lottie Animation Example */}
<Sequence from={30} durationInFrames={120}>
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', opacity: 0.4 }}>
<div style={{ width: 600, height: 600 }}>
<Lottie
src="https://assets.lottiefiles.com/packages/lf20_u4j3X6.json"
playbackRate={0.5}
loop
/>
Comment on lines +49 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

External Lottie URL in a starter template is fragile and inconsistent with the demo.

This template hardcodes https://assets.lottiefiles.com/... while the DemoLottie files use https://assets10.lottiefiles.com/... (different subdomain). If either URL goes stale, users see a broken hello-world experience on first run.

Consider bundling a small inline JSON animation (like the simpleLottie object used in DemoLottie.tsx) instead of relying on an external URL for the starter template.

🤖 Prompt for AI Agents
In `@packages/create-motionforge/templates/hello-world/page.tsx.template` around
lines 49 - 53, The page.tsx.template currently uses an external Lottie URL in
the <Lottie> component which is fragile; replace that external src usage by
bundling an inline Lottie JSON (like the simpleLottie object defined in
DemoLottie.tsx) and pass it to the Lottie component (or import
DemoLottie/simpleLottie) so the starter template renders reliably offline and
matches the demo; update the <Lottie> usage in page.tsx.template to accept the
inlined JSON instead of the hardcoded "https://assets.lottiefiles.com/..." URL
and keep the existing playbackRate and loop props.

</div>
</AbsoluteFill>
</Sequence>
</AbsoluteFill>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"motionforge": "^1.2.0",
"lottie-web": "^5.13.0",
Comment on lines 12 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Template references motionforge ^1.2.0 but Lottie requires ^1.3.0.

The template now includes lottie-web and presumably uses the new <Lottie /> component, but the motionforge dependency is still pinned to ^1.2.0. Projects scaffolded from this template could resolve to 1.2.x (which lacks Lottie support) if 1.3.0 isn't yet published.

♻️ Suggested fix
-    "motionforge": "^1.2.0",
+    "motionforge": "^1.3.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"motionforge": "^1.2.0",
"lottie-web": "^5.13.0",
"motionforge": "^1.3.0",
"lottie-web": "^5.13.0",
🤖 Prompt for AI Agents
In `@packages/create-motionforge/templates/shared/package.json.template` around
lines 12 - 13, Update the package version for the "motionforge" dependency in
package.json.template from "^1.2.0" to "^1.3.0" so it is compatible with the
included "lottie-web" dependency; locate the "motionforge" entry in the shared
package.json.template and bump the version string to "^1.3.0" (ensure the
trailing comma and JSON formatting remain valid).

"next": "15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
12 changes: 12 additions & 0 deletions packages/motionforge/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to MotionForge will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2024-03-20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect changelog date.

The date 2024-03-20 appears to be incorrect — this PR was created on 2026-02-15. Update to the actual release date.

-## [1.3.0] - 2024-03-20
+## [1.3.0] - 2026-02-15
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## [1.3.0] - 2024-03-20
## [1.3.0] - 2026-02-15
🤖 Prompt for AI Agents
In `@packages/motionforge/CHANGELOG.md` at line 8, Update the release date in the
changelog header for version marker "## [1.3.0] - 2024-03-20" to the correct
release date (replace 2024-03-20 with 2026-02-15) so the header reads "##
[1.3.0] - 2026-02-15"; locate the exact header line containing "## [1.3.0]" in
CHANGELOG.md and change only the date portion.


### Added
- Full Lottie animation integration with `<Lottie />` component
- Deterministic frame synchronization with MotionForge timeline
- Support for both remote JSON URLs and local JSON objects
- Advanced Lottie controls: `frameStart`, `frameEnd`, `playbackRate`, `loop`
- Automatic relative frame mapping within `<Sequence />`
- Production-grade performance with memoization and instance cleanup
- CLI template updates with Lottie support and examples
- Comprehensive Lottie documentation in README

## [1.2.0] - 2024-01-15

### Added
Expand Down
56 changes: 56 additions & 0 deletions packages/motionforge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,62 @@ MotionForge uses a dark theme by default with emerald green accents. Customize c
}
```

## 🎭 Using Lottie in MotionForge

MotionForge provides first-class, production-grade Lottie support. Animations are synchronized with the frame system for deterministic, frame-perfect rendering.

### Basic Usage

```tsx
import { Lottie, AbsoluteFill } from 'motionforge';

const MyComposition = () => {
return (
<AbsoluteFill>
<Lottie
src="https://assets.lottiefiles.com/packages/lf20_u4j3X6.json"
width={400}
height={400}
/>
</AbsoluteFill>
);
};
```

### Advanced Configuration

The `Lottie` component supports several props to control playback and appearance:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `src` | `string \| object` | - | URL to JSON or imported JSON object |
| `frameStart` | `number` | `0` | The frame at which the Lottie starts |
| `frameEnd` | `number` | - | The frame at which the Lottie ends |
| `playbackRate` | `number` | `1` | Speed of the animation |
| `loop` | `boolean` | `false` | Whether to loop the animation |
| `width` | `number \| string` | `100%` | Width of the container |
| `height` | `number \| string` | `100%` | Height of the container |

### Syncing with Sequences

`Lottie` automatically detects if it's inside a `Sequence` and adjusts its internal timing to match the relative frame.

```tsx
<Sequence from={30} durationInFrames={120}>
<Lottie
src={myAnimationData}
playbackRate={1.5}
loop
/>
</Sequence>
```

### Performance Tips

1. **Pre-loading**: For imported JSON objects, Lottie initializes instantly. For URLs, it fetches the data once and memoizes the instance.
2. **SSR Support**: The component handles server-side rendering gracefully by only initializing the Lottie engine on the client.
3. **Memoization**: Internal animation instances are memoized and properly cleaned up to prevent memory leaks.

## 📖 Documentation

- [Getting Started](https://motionforge.dev/docs/getting-started)
Expand Down
6 changes: 5 additions & 1 deletion packages/motionforge/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/motionforge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "motionforge",
"version": "1.2.0",
"version": "1.3.0",
"description": "A React-based framework for creating videos programmatically. Build stunning videos with React components, spring animations, and frame-perfect control.",
"author": "MotionForge Team",
"license": "MIT",
Expand Down Expand Up @@ -73,6 +73,9 @@
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"dependencies": {
"lottie-web": "^5.13.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
Expand Down
182 changes: 182 additions & 0 deletions packages/motionforge/src/components/Lottie.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use client';

import React, { useEffect, useRef, useState, useMemo } from 'react';
import lottie, { AnimationItem } from 'lottie-web';
import { useCurrentFrame } from '../core/context';
import { useRelativeCurrentFrame } from './Sequence';

export interface LottieProps {
/**
* Source of the Lottie animation. Can be a URL to a JSON file or an imported JSON object.
*/
src: string | object;
/**
* The frame at which the Lottie animation should start playing.
* Default is 0.
*/
frameStart?: number;
/**
* The frame at which the Lottie animation should end playing.
* Default is the last frame of the Lottie animation.
*/
frameEnd?: number;
/**
* Playback rate of the Lottie animation.
* Default is 1.
*/
playbackRate?: number;
/**
* Whether the Lottie animation should loop.
* Default is false.
*/
loop?: boolean;
/**
* Width of the Lottie container.
*/
width?: number | string;
/**
* Height of the Lottie container.
*/
height?: number | string;
/**
* Style overrides for the Lottie container.
*/
style?: React.CSSProperties;
/**
* CSS class name for the Lottie container.
*/
className?: string;
}

/**
* Lottie Component for MotionForge.
*
* Provides production-grade Lottie support with deterministic frame synchronization.
* Synchronizes with the MotionForge frame system instead of using time-based playback.
*/
export const Lottie: React.FC<LottieProps> = ({
src,
frameStart = 0,
frameEnd,
playbackRate = 1,
loop = false,
width,
height,
style,
className,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<AnimationItem | null>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState<string | null>(null);

const absoluteFrame = useCurrentFrame();
const relativeFrame = useRelativeCurrentFrame();

// Use relative frame if inside a Sequence, otherwise use absolute frame
const currentFrame = relativeFrame !== null ? relativeFrame : absoluteFrame;

// Handle initialization
useEffect(() => {
if (typeof window === 'undefined' || !containerRef.current) return;

let isCancelled = false;

const params: any = {
container: containerRef.current,
renderer: 'svg',
loop: false,
autoplay: false,
rendererSettings: {
progressiveLoad: false,
hideOnTransparent: true,
}
};

if (typeof src === 'string') {
params.path = src;
} else {
params.animationData = src;
}

try {
const anim = lottie.loadAnimation(params);
animationRef.current = anim;

const onLoaded = () => {
if (!isCancelled) {
setIsLoaded(true);
}
};

// Both events can be useful depending on how Lottie is loaded
anim.addEventListener('DOMLoaded', onLoaded);
anim.addEventListener('data_ready', onLoaded);

return () => {
isCancelled = true;
anim.destroy();
animationRef.current = null;
};
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return () => {};
}
}, [src]);
Comment on lines +80 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Object src in dependency array causes re-initialization on every render.

When src is an inline object (e.g., <Lottie src={{ ... }} />), a new reference is created each render, causing useEffect to destroy and re-create the lottie instance in an infinite loop. This is the primary usage pattern shown in the demos (src={simpleLottie} is fine since it's module-scoped, but any consumer passing an inline object will hit this).

Additionally, the error state is never cleared when src changes, so after a failed load, switching to a valid src still shows the error UI.

Proposed fix
+  const srcRef = useRef(src);
+  const [srcKey, setSrcKey] = useState(0);
+
+  // Only re-trigger when src *actually* changes (deep-compare for objects)
+  useEffect(() => {
+    if (typeof src === 'string') {
+      if (src !== srcRef.current) {
+        srcRef.current = src;
+        setSrcKey(k => k + 1);
+      }
+    } else {
+      // For objects, JSON-serialize or use a stable reference from the caller
+      srcRef.current = src;
+    }
+  }, [src]);

   // Handle initialization
   useEffect(() => {
     if (typeof window === 'undefined' || !containerRef.current) return;

     let isCancelled = false;
+    setError(null);
+    setIsLoaded(false);
     // ... rest unchanged
-  }, [src]);
+  }, [srcKey]);

Alternatively, document that src must be a stable reference (module-level constant or useMemo'd) and add a note in the JSDoc. A simpler approach is to JSON.stringify the object src for the dep array:

+  const srcStable = useMemo(
+    () => (typeof src === 'string' ? src : JSON.stringify(src)),
+    [src]
+  );
   // ...
-  }, [src]);
+  }, [srcStable]);

But note JSON.stringify itself re-runs on every render for object src, so the caller should ideally stabilize the reference. At minimum, clear error/loaded state on re-init.

🧰 Tools
🪛 ESLint

[error] 122-122: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/packages/motionforge/src/components/Lottie.tsx:122:7
120 | };
121 | } catch (err) {

122 | setError(err instanceof Error ? err.message : String(err));
| ^^^^^^^^ Avoid calling setState() directly within an effect
123 | return () => {};
124 | }
125 | }, [src]);

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
In `@packages/motionforge/src/components/Lottie.tsx` around lines 80 - 125, The
effect currently re-initializes whenever the inline object `src` reference
changes, causing infinite re-creates and never clearing prior errors; update the
effect in the component that uses useEffect (the effect that references `src`,
`containerRef`, `animationRef`, `setIsLoaded`, and `setError`) to compute a
stable key for `src` (e.g., use string key = typeof src === 'string' ? src :
JSON.stringify(src)), track the previous key with a ref and bail out early if
the key hasn't changed, and when the key does change reset state by calling
setError(null) and setIsLoaded(false) before creating the new lottie instance;
then use that stable key (not the raw object) in the effect dependency array so
inline object props no longer force re-initialization.


// Handle frame synchronization
useEffect(() => {
if (!animationRef.current || !isLoaded) return;

const anim = animationRef.current;

// totalFrames is available once loaded
const totalLottieFrames = anim.totalFrames;

// Determine the range of frames to play
const start = frameStart;
const end = frameEnd ?? totalLottieFrames;
const duration = end - start;

if (duration <= 0) return;

// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;

if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}

// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;

// goToAndStop(value, isFrame) - second arg true means it's a frame number, not time
anim.goToAndStop(finalFrame, true);
Comment on lines +143 to +157
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Frame mapping doesn't clamp negative values, breaking with negative playbackRate or edge cases.

  • Line 145: currentFrame * playbackRate can be negative if playbackRate < 0 (not validated).
  • Line 148: JS % operator preserves the sign of the dividend, so a negative targetFrame % duration yields a negative result, causing finalFrame to fall below start.
  • Line 150: Non-loop path uses Math.min but no Math.max(0, ...), so negative values pass through unclamped.
Proposed fix
     let targetFrame = currentFrame * playbackRate;

     if (loop) {
-      targetFrame = targetFrame % duration;
+      targetFrame = ((targetFrame % duration) + duration) % duration;
     } else {
-      targetFrame = Math.min(targetFrame, duration - 0.01);
+      targetFrame = Math.max(0, Math.min(targetFrame, duration - 0.01));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;
if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}
// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
// goToAndStop(value, isFrame) - second arg true means it's a frame number, not time
anim.goToAndStop(finalFrame, true);
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;
if (loop) {
targetFrame = ((targetFrame % duration) + duration) % duration;
} else {
targetFrame = Math.max(0, Math.min(targetFrame, duration - 0.01));
}
// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
// goToAndStop(value, isFrame) - second arg true means it's a frame number, not time
anim.goToAndStop(finalFrame, true);
🤖 Prompt for AI Agents
In `@packages/motionforge/src/components/Lottie.tsx` around lines 143 - 157, The
mapping of currentFrame to Lottie frames can produce negative targetFrame when
playbackRate is negative and the modulo preserves sign; update the targetFrame
calculation in the Lottie component (symbols: currentFrame, playbackRate, loop,
duration, start, targetFrame, finalFrame, anim.goToAndStop) so that after
computing targetFrame = currentFrame * playbackRate you: for loop=true normalize
the modulo to a non-negative value (e.g. ((targetFrame % duration) + duration) %
duration) and for loop=false clamp to the [0, duration - 0.01] range (use
Math.max(0, ...) or equivalent), then compute finalFrame = start + targetFrame
and call anim.goToAndStop(finalFrame, true).

}, [currentFrame, isLoaded, frameStart, frameEnd, playbackRate, loop]);

const containerStyle: React.CSSProperties = useMemo(() => ({
width: width ?? '100%',
height: height ?? '100%',
...style,
}), [width, height, style]);

if (error) {
return (
<div style={{ color: 'red', border: '1px solid red', padding: '10px' }}>
<strong>Lottie Error:</strong> {error}
</div>
);
}

return (
<div
ref={containerRef}
style={containerStyle}
className={className}
data-lottie-loaded={isLoaded}
/>
);
};
2 changes: 1 addition & 1 deletion packages/motionforge/src/components/Media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { interpolate } from '../utils/animation';

// Absolute Fill - Container component
interface AbsoluteFillProps {
children: React.ReactNode;
children?: React.ReactNode;
style?: React.CSSProperties;
className?: string;
}
Expand Down
Loading