Recreating the Apple fireworks animation
Learn how to create an animation that Apple has on its website during the holiday season with this beginner-friendly Remotion tutorial!
Video version
This tutorial is also available as a video version:
Source code
Getting started
Start a new Remotion project and select the blank template:
- npm
- yarn
- pnpm
bash
npm init video --blank
bash
npm init video --blank
bash
pnpm create video --blank
bash
pnpm create video --blank
bash
yarn create video --blank
bash
yarn create video --blank
Composition setup
A <Composition>
defines the dimensions and duration of the video. In the src/Root.tsx
file, adjust the width and height to the following:
src/Root.tsxtsx
export constRemotionRoot :React .FC = () => {return (<><Composition id ="MyComp"component ={MyComposition }durationInFrames ={150}fps ={30}width ={1920}height ={1080}/></>);};
src/Root.tsxtsx
export constRemotionRoot :React .FC = () => {return (<><Composition id ="MyComp"component ={MyComposition }durationInFrames ={150}fps ={30}width ={1920}height ={1080}/></>);};
Create a background
Create a new file src/Background.tsx
and return a background with linear gradient:
src/Background.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constBackground :React .FC = () => {return (<AbsoluteFill style ={{background : "linear-gradient(to bottom, #000021, #010024)",}}/>);};
src/Background.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constBackground :React .FC = () => {return (<AbsoluteFill style ={{background : "linear-gradient(to bottom, #000021, #010024)",}}/>);};
Add the created background to the <MyComposition/>
component, which can be found in the file src/Composition.tsx
. This file will contain all the components that you create in this tutorial.
src/Composition.tsxtsx
export constMyComposition :React .FC = () => {return (<AbsoluteFill ><Background /></AbsoluteFill >);};
src/Composition.tsxtsx
export constMyComposition :React .FC = () => {return (<AbsoluteFill ><Background /></AbsoluteFill >);};
This results in the following:
Render a dot
Render a white dot by creating a new file src/Dot.tsx
and return a centered circle.
src/Dot.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constDot :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",}}><div style ={{height : 14,width : 14,borderRadius : 14 / 2,backgroundColor : "#ccc",}}/></AbsoluteFill >);};
src/Dot.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constDot :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",}}><div style ={{height : 14,width : 14,borderRadius : 14 / 2,backgroundColor : "#ccc",}}/></AbsoluteFill >);};
Add the <Dot>
in your main composition src/Composition.tsx
:
src/Composition.tsxtsx
export constMyComposition :React .FC = () => {return (<AbsoluteFill ><Background /><Dot /></AbsoluteFill >);};
src/Composition.tsxtsx
export constMyComposition :React .FC = () => {return (<AbsoluteFill ><Background /><Dot /></AbsoluteFill >);};
Now we got a white dot on top of our background:
Animate the dot
Let's apply some animation to the white dot we created above. We create another component called <Shrinking>
in a new file src/Shrinking.tsx
, which then wraps the dot in the main composition src/Composition.tsx
.
src/Shrinking.tsxtsx
import React from "react";import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";export const Shrinking: React.FC<{children: React.ReactNode;}> = ({ children }) => {const frame = useCurrentFrame();return (<AbsoluteFillstyle={{scale: String(interpolate(frame, [60, 90], [1, 0], {extrapolateLeft: "clamp",extrapolateRight: "clamp",}),),}}>{children}</AbsoluteFill>);};
src/Shrinking.tsxtsx
import React from "react";import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";export const Shrinking: React.FC<{children: React.ReactNode;}> = ({ children }) => {const frame = useCurrentFrame();return (<AbsoluteFillstyle={{scale: String(interpolate(frame, [60, 90], [1, 0], {extrapolateLeft: "clamp",extrapolateRight: "clamp",}),),}}>{children}</AbsoluteFill>);};
Add the <Shrinking>
component in your main composition src/Composition.tsx
:
tsx
export constMyComposition :React .FC = () => {return (<AbsoluteFill ><Background /><Shrinking ><Dot /></Shrinking ></AbsoluteFill >);};
tsx
export constMyComposition :React .FC = () => {return (<AbsoluteFill ><Background /><Shrinking ><Dot /></Shrinking ></AbsoluteFill >);};
Now, you have some action to show. By using <Shrinking>
in your main composition you have created a scale out effect:
Move the dot
Next, create a component called <Move>
. This component has a spring animation, which by default goes from zero to one, and has a duration of four seconds (durationInFrames: 120
) in the code snippet below:
src/Move.tsxtsx
importReact from "react";import {AbsoluteFill ,interpolate ,spring ,useCurrentFrame ,useVideoConfig ,} from "remotion";export constMove :React .FC <{children :React .ReactNode ;}> = ({children }) => {const {fps } =useVideoConfig ();constframe =useCurrentFrame ();constdown =spring ({fps ,frame ,config : {damping : 200,},durationInFrames : 120,});consty =interpolate (down , [0, 1], [0, -400]);return (<AbsoluteFill style ={{translate : `0 ${y }px`,}}>{children }</AbsoluteFill >);};
src/Move.tsxtsx
importReact from "react";import {AbsoluteFill ,interpolate ,spring ,useCurrentFrame ,useVideoConfig ,} from "remotion";export constMove :React .FC <{children :React .ReactNode ;}> = ({children }) => {const {fps } =useVideoConfig ();constframe =useCurrentFrame ();constdown =spring ({fps ,frame ,config : {damping : 200,},durationInFrames : 120,});consty =interpolate (down , [0, 1], [0, -400]);return (<AbsoluteFill style ={{translate : `0 ${y }px`,}}>{children }</AbsoluteFill >);};
Add the <Move>
component to your composition src/Composition.tsx
. You get a nice animation by combining the effect of moving and shrinking by surrounding the shrinking dot in the <Move>
component:
src/Composition.tsxtsx
export constMyComposition = () => {return (<AbsoluteFill ><Background /><Move ><Shrinking ><Dot /></Shrinking ></Move ></AbsoluteFill >);};
src/Composition.tsxtsx
export constMyComposition = () => {return (<AbsoluteFill ><Background /><Move ><Shrinking ><Dot /></Shrinking ></Move ></AbsoluteFill >);};
And up goes the dot:
Duplicate the moving dot
Here it gets a little bit trickier, but the following steps are going to make your animation a lot more entertaining. First, you add a delay
prop into the <Move>
component and then change the frame
parameter of your spring()
function.
src/Move.tsxtsx
importReact from "react";import {AbsoluteFill ,interpolate ,spring ,useCurrentFrame ,useVideoConfig ,} from "remotion";export constMove :React .FC <{children :React .ReactNode ;delay : number;}> = ({children ,delay }) => {const {fps } =useVideoConfig ();constframe =useCurrentFrame ();constdown =spring ({fps ,frame :frame -delay ,config : {damping : 200,},durationInFrames : 120,});consty =interpolate (down , [0, 1], [0, -400]);return (<AbsoluteFill style ={{translate : `0 ${y }px`,}}>{children }</AbsoluteFill >);};
src/Move.tsxtsx
importReact from "react";import {AbsoluteFill ,interpolate ,spring ,useCurrentFrame ,useVideoConfig ,} from "remotion";export constMove :React .FC <{children :React .ReactNode ;delay : number;}> = ({children ,delay }) => {const {fps } =useVideoConfig ();constframe =useCurrentFrame ();constdown =spring ({fps ,frame :frame -delay ,config : {damping : 200,},durationInFrames : 120,});consty =interpolate (down , [0, 1], [0, -400]);return (<AbsoluteFill style ={{translate : `0 ${y }px`,}}>{children }</AbsoluteFill >);};
Now, let's create a <Trail>
component. It takes some React children and duplicates them. The component adds a delay to each subsequent dot so they don't start all at once. Each dot will have a scale applied to it, so that each dot is smaller than the previous one.
Put the previously created <Move>
component within the <Trail>
component.
The order is crucial here. Things are done from inside out:
- Apply a scale so that the dots become smaller over time.
- Apply the move animation.
- Apply a delay between the animation start of each dot by using Remotion's
<Sequence>
component.
src/Trail.tsxtsx
importReact from "react";import {AbsoluteFill ,Sequence } from "remotion";import {Move } from "./Move";export constTrail :React .FC <{amount : number;children :React .ReactNode ;}> = ({amount ,children }) => {return (<AbsoluteFill >{newArray (amount ).fill (true).map ((a ,i ) => {return (<Sequence from ={i * 3}><AbsoluteFill ><Move delay ={0}><AbsoluteFill style ={{scale :String (1 -i /amount ),}}>{children }</AbsoluteFill ></Move ></AbsoluteFill ></Sequence >);})}</AbsoluteFill >);};
src/Trail.tsxtsx
importReact from "react";import {AbsoluteFill ,Sequence } from "remotion";import {Move } from "./Move";export constTrail :React .FC <{amount : number;children :React .ReactNode ;}> = ({amount ,children }) => {return (<AbsoluteFill >{newArray (amount ).fill (true).map ((a ,i ) => {return (<Sequence from ={i * 3}><AbsoluteFill ><Move delay ={0}><AbsoluteFill style ={{scale :String (1 -i /amount ),}}>{children }</AbsoluteFill ></Move ></AbsoluteFill ></Sequence >);})}</AbsoluteFill >);};
In your main component, you now replace the <Move>
component with the <Trail>
component:
src/Composition.tsxtsx
export constMyComposition = () => {return (<AbsoluteFill ><Background /><Trail amount ={4}><Shrinking ><Dot /></Shrinking ></Trail ></AbsoluteFill >);};
src/Composition.tsxtsx
export constMyComposition = () => {return (<AbsoluteFill ><Background /><Trail amount ={4}><Shrinking ><Dot /></Shrinking ></Trail ></AbsoluteFill >);};
And this is how your animation with the duplicated dots should look like:
Duplicating markup and arranging it in a circle
Now let's create a <Explosion>
component. It takes children and renders them for example 10 times and applies a rotation to each instance. It's worth mentioning here that a full rotation amounts to 2π, while (i/AMOUNT)
represents a factor between 0 and 1.
src/Explosion.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";constAMOUNT = 10;export constExplosion :React .FC <{children :React .ReactNode ;}> = ({children }) => {return (<AbsoluteFill >{newArray (AMOUNT ).fill (true).map ((_ ,i ) => {return (<AbsoluteFill style ={{rotate : (i /AMOUNT ) * (2 *Math .PI ) + "rad",}}>{children }</AbsoluteFill >);})}</AbsoluteFill >);};
src/Explosion.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";constAMOUNT = 10;export constExplosion :React .FC <{children :React .ReactNode ;}> = ({children }) => {return (<AbsoluteFill >{newArray (AMOUNT ).fill (true).map ((_ ,i ) => {return (<AbsoluteFill style ={{rotate : (i /AMOUNT ) * (2 *Math .PI ) + "rad",}}>{children }</AbsoluteFill >);})}</AbsoluteFill >);};
<Trail>
gets put inside the <Explosion>
component. Your main component (src/Composition.tsx
) looks like this:
src/Composition.tsxtsx
export const MyComposition = () => {return (<AbsoluteFill> <Background /><Explosion><Trailamount={4}> <Shrinking><Dot /></Shrinking></Trail></Explosion></AbsoluteFill> );};
src/Composition.tsxtsx
export const MyComposition = () => {return (<AbsoluteFill> <Background /><Explosion><Trailamount={4}> <Shrinking><Dot /></Shrinking></Trail></Explosion></AbsoluteFill> );};
The animated explosion should look like this:
Cleanup
You have created a bunch of files until now, let's put most of them together in one file called src/Dots.tsx
. Extract <Explosion>
and it's children into a new separate component called Dots.
src/Dots.tsxtsx
importReact from "react";import {Sequence } from "remotion";import {Dot } from "./Dot";import {Explosion } from "./Explosion";import {Shrinking } from "./Shrinking";import {Trail } from "./Trail";export constDots :React .FC = () => {return (<Explosion ><Trail amount ={4}><Shrinking ><Sequence from ={5}><Dot /></Sequence ></Shrinking ></Trail ></Explosion >);};
src/Dots.tsxtsx
importReact from "react";import {Sequence } from "remotion";import {Dot } from "./Dot";import {Explosion } from "./Explosion";import {Shrinking } from "./Shrinking";import {Trail } from "./Trail";export constDots :React .FC = () => {return (<Explosion ><Trail amount ={4}><Shrinking ><Sequence from ={5}><Dot /></Sequence ></Shrinking ></Trail ></Explosion >);};
Replace the <Explosion>
with the new <Dots>
component:
tsx
export constMyComposition = () => {return (<AbsoluteFill ><Background /><Dots /></AbsoluteFill >);};
tsx
export constMyComposition = () => {return (<AbsoluteFill ><Background /><Dots /></AbsoluteFill >);};
Nothing has changed on the animation itself:
Adding hearts and stars
To make the animation more exciting, let's add some stars and hearts in different colors. To do this, we need to basically repeat the previous steps. Besides the <Dots>
component, we'll add three more components in the next few steps.
Let's start with red hearts. First you render a red heart by creating a new file src/RedHeart.tsx
and return a centered red heart emoji.
src/RedHeart.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constRedHeart :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",}}>❤️</AbsoluteFill >);};
src/RedHeart.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constRedHeart :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",}}>❤️</AbsoluteFill >);};
Effects like <Shrinking>
, <Move>
and <Explosion>
need to be applied to that red heart. We do this in a new component called RedHearts.
Consider s that we need to add an offset to <RedHearts>
, otherwise they would be positioned the same as the <Dots>
.
We change the position by giving the red hearts a bigger radius than the dots, and apply a 100px
translation. Also, we add a short delay of 5 frames to the <Move>
component:
src/RedHearts.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Explosion } from "./Explosion";import {Move } from "./Move";import {RedHeart } from "./RedHeart";import {Shrinking } from "./Shrinking";export constRedHearts :React .FC = () => {return (<Explosion ><Move delay ={5}><AbsoluteFill style ={{transform : `translateY(-100px)` }}><Shrinking ><RedHeart /></Shrinking ></AbsoluteFill ></Move ></Explosion >);};
src/RedHearts.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Explosion } from "./Explosion";import {Move } from "./Move";import {RedHeart } from "./RedHeart";import {Shrinking } from "./Shrinking";export constRedHearts :React .FC = () => {return (<Explosion ><Move delay ={5}><AbsoluteFill style ={{transform : `translateY(-100px)` }}><Shrinking ><RedHeart /></Shrinking ></AbsoluteFill ></Move ></Explosion >);};
We do the same to get some yellow hearts in our animation:
src/YellowHeart.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constYellowHeart :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",}}>💛</AbsoluteFill >);};
src/YellowHeart.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constYellowHeart :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",}}>💛</AbsoluteFill >);};
For the yellow hearts we are going to change the position by applying a translation of 50px
and adding a delay of 20 frames to the <Move>
component:
src/YellowHearts.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Explosion } from "./Explosion";import {Move } from "./Move";import {Shrinking } from "./Shrinking";import {YellowHeart } from "./YellowHeart";export constYellowHearts :React .FC = () => {return (<AbsoluteFill style ={{rotate : "0.3rad",}}><Explosion ><Move delay ={20}><AbsoluteFill style ={{transform : `translateY(-50px)`,}}><Shrinking ><YellowHeart /></Shrinking ></AbsoluteFill ></Move ></Explosion ></AbsoluteFill >);};
src/YellowHearts.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Explosion } from "./Explosion";import {Move } from "./Move";import {Shrinking } from "./Shrinking";import {YellowHeart } from "./YellowHeart";export constYellowHearts :React .FC = () => {return (<AbsoluteFill style ={{rotate : "0.3rad",}}><Explosion ><Move delay ={20}><AbsoluteFill style ={{transform : `translateY(-50px)`,}}><Shrinking ><YellowHeart /></Shrinking ></AbsoluteFill ></Move ></Explosion ></AbsoluteFill >);};
Your main composition should look like this:
In addition to the dots and hearts let's also add stars.
Create a new file src/Star.tsx
and return a centered star emoji.
src/Star.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constStar :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",fontSize : 14,}}>⭐</AbsoluteFill >);};
src/Star.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";export constStar :React .FC = () => {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",fontSize : 14,}}>⭐</AbsoluteFill >);};
Consider that we need to change the positioning of the stars, otherwise they would be on top of the <Dots>
.
Let's give <Trail>
an extraOffset
prop, so the stars can start more outwards than the dots.
An extraOffset
of 100 for the stars leads to the same circumference at the beginning and end as the red hearts have. Here is the adjusted <Trail>
:
src/Trail.tsxtsx
importReact from "react";import {AbsoluteFill ,Sequence } from "remotion";import {Move } from "./Move";export constTrail :React .FC <{amount : number;extraOffset : number;children :React .ReactNode ;}> = ({amount ,extraOffset ,children }) => {return (<AbsoluteFill >{newArray (amount ).fill (true).map ((a ,i ) => {return (<Sequence from ={i * 3}><AbsoluteFill style ={{translate : `0 ${-extraOffset }px`,}}><Move delay ={0}><AbsoluteFill style ={{scale :String (1 -i /amount ),}}>{children }</AbsoluteFill ></Move ></AbsoluteFill ></Sequence >);})}</AbsoluteFill >);};
src/Trail.tsxtsx
importReact from "react";import {AbsoluteFill ,Sequence } from "remotion";import {Move } from "./Move";export constTrail :React .FC <{amount : number;extraOffset : number;children :React .ReactNode ;}> = ({amount ,extraOffset ,children }) => {return (<AbsoluteFill >{newArray (amount ).fill (true).map ((a ,i ) => {return (<Sequence from ={i * 3}><AbsoluteFill style ={{translate : `0 ${-extraOffset }px`,}}><Move delay ={0}><AbsoluteFill style ={{scale :String (1 -i /amount ),}}>{children }</AbsoluteFill ></Move ></AbsoluteFill ></Sequence >);})}</AbsoluteFill >);};
Effects like <Shrinking>
, the new <Trail>
and <Explosion>
need to be applied to the star we created above. Additionally we also add some rotation. We do all of this in a new component called Stars:
src/Stars.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Explosion } from "./Explosion";import {Shrinking } from "./Shrinking";import {Star } from "./Star";import {Trail } from "./Trail";export constStars :React .FC = () => {return (<AbsoluteFill style ={{rotate : "0.3rad",}}><Explosion ><Trail extraOffset ={100}amount ={4}><Shrinking ><Star /></Shrinking ></Trail ></Explosion ></AbsoluteFill >);};
src/Stars.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Explosion } from "./Explosion";import {Shrinking } from "./Shrinking";import {Star } from "./Star";import {Trail } from "./Trail";export constStars :React .FC = () => {return (<AbsoluteFill style ={{rotate : "0.3rad",}}><Explosion ><Trail extraOffset ={100}amount ={4}><Shrinking ><Star /></Shrinking ></Trail ></Explosion ></AbsoluteFill >);};
Here is how the almost complete firework should look like:
Slow motion effect
Lastly let's apply a slow motion effect to the firework. For this, create a new file src/SlowedTrail.tsx
. It should contain a component called Slowed and a helper function remapSpeed()
which will apply different speed levels to the firework. In the code snippet below a speed of 1.5 is applied until frame 20, afterwards the speed slows down to 0.5.
src/SlowedTrail.tsxtsx
importReact from "react";import {Freeze ,interpolate ,useCurrentFrame } from "remotion";// remapSpeed() is a helper function for the component <Slowed> that takes a frame number and a speedconstremapSpeed = ({frame ,speed ,}: {frame : number;speed : (fr : number) => number;}) => {letframesPassed = 0;for (leti = 0;i <=frame ;i ++) {framesPassed +=speed (i );}returnframesPassed ;};export constSlowed :React .FC <{children :React .ReactNode ;}> = ({children }) => {constframe =useCurrentFrame ();constremappedFrame =remapSpeed ({frame ,speed : (f ) =>interpolate (f , [0, 20, 21], [1.5, 1.5, 0.5], {extrapolateRight : "clamp",}),});return <Freeze frame ={remappedFrame }>{children }</Freeze >;};
src/SlowedTrail.tsxtsx
importReact from "react";import {Freeze ,interpolate ,useCurrentFrame } from "remotion";// remapSpeed() is a helper function for the component <Slowed> that takes a frame number and a speedconstremapSpeed = ({frame ,speed ,}: {frame : number;speed : (fr : number) => number;}) => {letframesPassed = 0;for (leti = 0;i <=frame ;i ++) {framesPassed +=speed (i );}returnframesPassed ;};export constSlowed :React .FC <{children :React .ReactNode ;}> = ({children }) => {constframe =useCurrentFrame ();constremappedFrame =remapSpeed ({frame ,speed : (f ) =>interpolate (f , [0, 20, 21], [1.5, 1.5, 0.5], {extrapolateRight : "clamp",}),});return <Freeze frame ={remappedFrame }>{children }</Freeze >;};
In the main component, wrap all moving dots, hearts and stars in the component <Slowed>
. As you sure can tell by now, everything is very composable:
src/Composition.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Background } from "./Background";import {Dots } from "./Dots";import {RedHearts } from "./RedHearts";import {Slowed } from "./SlowedTrail";import {Stars } from "./Stars";import {YellowHearts } from "./YellowHearts";export constMyComposition = () => {return (<AbsoluteFill ><Background /><Slowed ><Dots /><RedHearts /><YellowHearts /><Stars /></Slowed ></AbsoluteFill >);};
src/Composition.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Background } from "./Background";import {Dots } from "./Dots";import {RedHearts } from "./RedHearts";import {Slowed } from "./SlowedTrail";import {Stars } from "./Stars";import {YellowHearts } from "./YellowHearts";export constMyComposition = () => {return (<AbsoluteFill ><Background /><Slowed ><Dots /><RedHearts /><YellowHearts /><Stars /></Slowed ></AbsoluteFill >);};
Your final firework should look like this:
Adding your animoji
As the final step of this tutorial, we add your animoji on top of the firework. For the animoji you need to have an iPhone and a Mac. This is how you get it: On your iPhone in iMessage, record an animoji of yourself and send it to a friend. After you've done that, it will also appear in the Messages app on your Mac. Download your animoji there by right-clicking. Once you have done that, create a transparent version of your animoji. Just follow these points:
- Right-click your downloaded animoji
- Select "Services"
- Select "Encode Selected Video Files"
- Choose "Apple ProRes" in the settings dropdown
- Tick the box that says "Preserve Transparency".
A new encoded file of your animoji will be created. Give it a simple name like animoji.mov.
In addition to the src
folder in your Remotion project, create a new one called public
. Put your encoded video in this folder. You can then use FFmpeg to turn the encoded video into a series of frames:
- Change the current working directory to
public
:cd public
- Use this command:
ffmpeg -i animoji.mov -pix_fmt rgba -start_number 0 frame%03d.png
Only assets that are being used by Remotion need to be in the public
folder. You don't need the encoded video, so you can delete it after the frames have been extracted.
Here is a screenshot right before creating the series of frames:
Alright, so far you've prepared the animoji to be used in a new component called Animoji. You can import this series of frames by using the staticFile() API. The file name of each frame will help you to determine the current frame number.
src/Animoji.tsxtsx
importReact from "react";import {AbsoluteFill ,Img ,staticFile ,useCurrentFrame } from "remotion";export constAnimoji :React .FC = () => {constframe =useCurrentFrame ();constsrc = `frame${(frame * 2).toString ().padStart (3, "0")}.png`;return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",marginTop : 80,}}><Img style ={{height : 800,}}src ={staticFile (src )}/></AbsoluteFill >);};
src/Animoji.tsxtsx
importReact from "react";import {AbsoluteFill ,Img ,staticFile ,useCurrentFrame } from "remotion";export constAnimoji :React .FC = () => {constframe =useCurrentFrame ();constsrc = `frame${(frame * 2).toString ().padStart (3, "0")}.png`;return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",marginTop : 80,}}><Img style ={{height : 800,}}src ={staticFile (src )}/></AbsoluteFill >);};
Render the <Animoji>
component in your main composition:
src/Composition.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Animoji } from "./Animoji";import {Background } from "./Background";import {Dots } from "./Dots";import {RedHearts } from "./RedHearts";import {Slowed } from "./SlowedTrail";import {Stars } from "./Stars";import {YellowHearts } from "./YellowHearts";export constMyComposition = () => {return (<AbsoluteFill ><Background /><Slowed ><Dots /><RedHearts /><YellowHearts /><Stars /></Slowed ><Animoji /></AbsoluteFill >);};
src/Composition.tsxtsx
importReact from "react";import {AbsoluteFill } from "remotion";import {Animoji } from "./Animoji";import {Background } from "./Background";import {Dots } from "./Dots";import {RedHearts } from "./RedHearts";import {Slowed } from "./SlowedTrail";import {Stars } from "./Stars";import {YellowHearts } from "./YellowHearts";export constMyComposition = () => {return (<AbsoluteFill ><Background /><Slowed ><Dots /><RedHearts /><YellowHearts /><Stars /></Slowed ><Animoji /></AbsoluteFill >);};
By doing all of this you have imported a transparent version of your animoji into your composition. You can run npm run build
to export your video as MP4. Which should look like this:
Congrats on your programmatically generated video! 🎉