Skip to main content

重新创建苹果烟花动画

· 22 min read
乔尼·伯格
首席黑客

学习如何使用这个适合初学者的 Remotion 教程创建苹果在节假日网站上展示的动画!

视频版本

此教程也有视频版本可供参考:

源代码

GitHub

入门指南

开始一个新的 Remotion 项目并选择空白模板:

bash
npm init video --blank
bash
npm init video --blank

组合设置

<Composition> 定义视频的尺寸和持续时间。在 src/Root.tsx 文件中,将宽度和高度调整为以下内容:

src/Root.tsx
tsx
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="MyComp"
component={MyComposition}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
</>
);
};
src/Root.tsx
tsx
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="MyComp"
component={MyComposition}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
</>
);
};

创建背景

创建一个新文件 src/Background.tsx 并返回一个带有线性渐变的背景:

src/Background.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const Background: React.FC = () => {
return (
<AbsoluteFill
style={{
background: "linear-gradient(to bottom, #000021, #010024)",
}}
/>
);
};
src/Background.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const Background: React.FC = () => {
return (
<AbsoluteFill
style={{
background: "linear-gradient(to bottom, #000021, #010024)",
}}
/>
);
};

将创建的背景添加到 <MyComposition/> 组件中,该组件可以在文件 src/Composition.tsx 中找到。此文件将包含您在本教程中创建的所有组件。

src/Composition.tsx
tsx
export const MyComposition: React.FC = () => {
return (
<AbsoluteFill>
<Background />
</AbsoluteFill>
);
};
src/Composition.tsx
tsx
export const MyComposition: React.FC = () => {
return (
<AbsoluteFill>
<Background />
</AbsoluteFill>
);
};

这将产生以下效果:

html
<img src="/img/apple-wow-tutorial/Background.png"/>
## 渲染一个点
通过创建一个新文件 `src/Dot.tsx` 并返回一个居中的圆来渲染一个白色的点。
```tsx twoslash title="src/Dot.tsx"
import React from "react";
import { AbsoluteFill } from "remotion";
export const Dot: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
height: 14,
width: 14,
borderRadius: 14 / 2,
backgroundColor: "#ccc",
}}
/>
</AbsoluteFill>
);
};
html
<img src="/img/apple-wow-tutorial/Background.png"/>
## 渲染一个点
通过创建一个新文件 `src/Dot.tsx` 并返回一个居中的圆来渲染一个白色的点。
```tsx twoslash title="src/Dot.tsx"
import React from "react";
import { AbsoluteFill } from "remotion";
export const Dot: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
height: 14,
width: 14,
borderRadius: 14 / 2,
backgroundColor: "#ccc",
}}
/>
</AbsoluteFill>
);
};

在你的主合成 src/Composition.tsx 中添加 <Dot>

src/Composition.tsx
tsx
export const MyComposition: React.FC = () => {
return (
<AbsoluteFill>
<Background />
<Dot />
</AbsoluteFill>
);
};
src/Composition.tsx
tsx
export const MyComposition: React.FC = () => {
return (
<AbsoluteFill>
<Background />
<Dot />
</AbsoluteFill>
);
};

现在我们在背景上有一个白色的点:

为点添加动画

让我们对上面创建的白色点应用一些动画。我们在一个新文件 src/Shrinking.tsx 中创建另一个组件 <Shrinking>,然后将该组件包裹在主合成 src/Composition.tsx 中。

src/Shrinking.tsx
tsx
import React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
export const Shrinking: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const frame = useCurrentFrame();
return (
<AbsoluteFill
style={{
scale: String(
interpolate(frame, [60, 90], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
),
}}
>
{children}
</AbsoluteFill>
);
};
src/Shrinking.tsx
tsx
import React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
export const Shrinking: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const frame = useCurrentFrame();
return (
<AbsoluteFill
style={{
scale: String(
interpolate(frame, [60, 90], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
),
}}
>
{children}
</AbsoluteFill>
);
};

在你的主合成 src/Composition.tsx 中添加 <Shrinking> 组件:

tsx
export const MyComposition: React.FC = () => {
return (
<AbsoluteFill>
<Background />
<Shrinking>
<Dot />
</Shrinking>
</AbsoluteFill>
);
};
tsx
export const MyComposition: React.FC = () => {
return (
<AbsoluteFill>
<Background />
<Shrinking>
<Dot />
</Shrinking>
</AbsoluteFill>
);
};

现在,你有一些动作可以展示了。通过在主合成中使用 <Shrinking>,你已经创建了一个缩放效果:

移动点

接下来,创建一个名为 <Move> 的组件。该组件具有一个弹簧动画,默认情况下从零到一,并且在下面的代码片段中持续四秒钟(durationInFrames: 120):

src/Move.tsx
tsx
import React from "react";
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
 
export const Move: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
 
const down = spring({
fps,
frame,
config: {
damping: 200,
},
durationInFrames: 120,
});
 
const y = interpolate(down, [0, 1], [0, -400]);
 
return (
<AbsoluteFill
style={{
translate: `0 ${y}px`,
}}
>
{children}
</AbsoluteFill>
);
};
src/Move.tsx
tsx
import React from "react";
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
 
export const Move: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
 
const down = spring({
fps,
frame,
config: {
damping: 200,
},
durationInFrames: 120,
});
 
const y = interpolate(down, [0, 1], [0, -400]);
 
return (
<AbsoluteFill
style={{
translate: `0 ${y}px`,
}}
>
{children}
</AbsoluteFill>
);
};

<Move> 组件添加到你的合成 src/Composition.tsx 中。通过将缩小的点包裹在 <Move> 组件中,你可以通过结合移动和缩小的效果获得一个不错的动画:

src/Composition.tsx
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Move>
<Shrinking>
<Dot />
</Shrinking>
</Move>
</AbsoluteFill>
);
};
src/Composition.tsx
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Move>
<Shrinking>
<Dot />
</Shrinking>
</Move>
</AbsoluteFill>
);
};

点就这样飞起来了:

复制移动的点

这里变得有点棘手,但以下步骤将使你的动画更加有趣。首先,将一个 delay 属性添加到 <Move> 组件中,然后更改 spring() 函数的 frame 参数。

src/Move.tsx
tsx
import React from "react";
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
 
export const Move: React.FC<{
children: React.ReactNode;
delay: number;
}> = ({ children, delay }) => {
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
 
const down = spring({
fps,
frame: frame - delay,
config: {
damping: 200,
},
durationInFrames: 120,
});
 
const y = interpolate(down, [0, 1], [0, -400]);
 
return (
<AbsoluteFill
style={{
translate: `0 ${y}px`,
}}
>
{children}
</AbsoluteFill>
);
};
src/Move.tsx
tsx
import React from "react";
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
 
export const Move: React.FC<{
children: React.ReactNode;
delay: number;
}> = ({ children, delay }) => {
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
 
const down = spring({
fps,
frame: frame - delay,
config: {
damping: 200,
},
durationInFrames: 120,
});
 
const y = interpolate(down, [0, 1], [0, -400]);
 
return (
<AbsoluteFill
style={{
translate: `0 ${y}px`,
}}
>
{children}
</AbsoluteFill>
);
};

现在,让我们创建一个<Trail>组件。它接受一些React子元素并对它们进行复制。该组件会为每个后续的点添加延迟,以便它们不会同时开始。每个点都会应用一个比前一个点小的比例。

将之前创建的<Move>组件放在<Trail>组件中。 这里的顺序至关重要。操作是从内到外进行的:

  1. 应用一个比例,使点随时间变小。
  2. 应用移动动画。
  3. 使用Remotion的<Sequence>组件在每个点的动画开始之间应用延迟。
src/Trail.tsx
tsx
import React from "react";
import { AbsoluteFill, Sequence } from "remotion";
import { Move } from "./Move";
 
export const Trail: React.FC<{
amount: number;
children: React.ReactNode;
}> = ({ amount, children }) => {
return (
<AbsoluteFill>
{new Array(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.tsx
tsx
import React from "react";
import { AbsoluteFill, Sequence } from "remotion";
import { Move } from "./Move";
 
export const Trail: React.FC<{
amount: number;
children: React.ReactNode;
}> = ({ amount, children }) => {
return (
<AbsoluteFill>
{new Array(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>
);
};

在您的主组件中,现在用<Trail>组件替换<Move>组件:

src/Composition.tsx
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Trail amount={4}>
<Shrinking>
<Dot />
</Shrinking>
</Trail>
</AbsoluteFill>
);
};
src/Composition.tsx
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Trail amount={4}>
<Shrinking>
<Dot />
</Shrinking>
</Trail>
</AbsoluteFill>
);
};

这是带有复制点的动画应该看起来的样子:

复制标记并将其排列成一个圆

现在让我们创建一个<Explosion>组件。它接受子元素并将它们渲染例如10次,并对每个实例应用旋转。值得一提的是,完整的旋转量为2π,而(i/AMOUNT)表示0到1之间的因子。

src/Explosion.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
const AMOUNT = 10;
 
export const Explosion: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<AbsoluteFill>
{new Array(AMOUNT).fill(true).map((_, i) => {
return (
<AbsoluteFill
style={{
rotate: (i / AMOUNT) * (2 * Math.PI) + "rad",
}}
>
{children}
</AbsoluteFill>
);
})}
</AbsoluteFill>
);
};
src/Explosion.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
const AMOUNT = 10;
 
export const Explosion: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<AbsoluteFill>
{new Array(AMOUNT).fill(true).map((_, i) => {
return (
<AbsoluteFill
style={{
rotate: (i / AMOUNT) * (2 * Math.PI) + "rad",
}}
>
{children}
</AbsoluteFill>
);
})}
</AbsoluteFill>
);
};

<Trail>放在<Explosion>组件中。您的主组件(src/Composition.tsx)如下所示:

src/Composition.tsx
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Explosion>
<Trail amount={4}>
<Shrinking>
<Dot />
</Shrinking>
</Trail>
</Explosion>
</AbsoluteFill>
);
};
src/Composition.tsx
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Explosion>
<Trail amount={4}>
<Shrinking>
<Dot />
</Shrinking>
</Trail>
</Explosion>
</AbsoluteFill>
);
};

动画爆炸应该看起来像这样:

清理

到目前为止,您已经创建了一堆文件,让我们将大部分文件放在一个名为src/Dots.tsx的文件中。将<Explosion>和其子元素提取到一个名为Dots的新独立组件中。

src/Dots.tsx
tsx
import React from "react";
import { Sequence } from "remotion";
import { Dot } from "./Dot";
import { Explosion } from "./Explosion";
import { Shrinking } from "./Shrinking";
import { Trail } from "./Trail";
 
export const Dots: React.FC = () => {
return (
<Explosion>
<Trail amount={4}>
<Shrinking>
<Sequence from={5}>
<Dot />
</Sequence>
</Shrinking>
</Trail>
</Explosion>
);
};
src/Dots.tsx
tsx
import React from "react";
import { Sequence } from "remotion";
import { Dot } from "./Dot";
import { Explosion } from "./Explosion";
import { Shrinking } from "./Shrinking";
import { Trail } from "./Trail";
 
export const Dots: React.FC = () => {
return (
<Explosion>
<Trail amount={4}>
<Shrinking>
<Sequence from={5}>
<Dot />
</Sequence>
</Shrinking>
</Trail>
</Explosion>
);
};

用新的<Dots>组件替换<Explosion>

tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Dots />
</AbsoluteFill>
);
};
tsx
export const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Dots />
</AbsoluteFill>
);
};

动画本身没有任何变化:

添加心形和星星

为了使动画更加精彩,让我们添加一些不同颜色的星星和心形。为此,我们基本上需要重复之前的步骤。除了 <Dots> 组件外,在接下来的几个步骤中,我们将添加另外三个组件。

让我们从红色心形开始。首先,通过创建一个新文件 src/RedHeart.tsx 并返回一个居中的红色心形表情来渲染一个红色心形。

src/RedHeart.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const RedHeart: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
❤️
</AbsoluteFill>
);
};
src/RedHeart.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const RedHeart: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
❤️
</AbsoluteFill>
);
};

需要将 <Shrinking><Move><Explosion> 这些效果应用到这个红色心形上。我们在一个名为 RedHearts 的新组件中实现这一点。

考虑到我们需要给 <RedHearts> 添加一个偏移量,否则它们会与 <Dots> 的位置相同。

我们通过给红色心形一个比点更大的半径,并应用 100px 的平移来改变位置。此外,我们给 <Move> 组件添加了一个短暂的 5 帧延迟:

src/RedHearts.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
import { Explosion } from "./Explosion";
import { Move } from "./Move";
import { RedHeart } from "./RedHeart";
import { Shrinking } from "./Shrinking";
 
export const RedHearts: React.FC = () => {
return (
<Explosion>
<Move delay={5}>
<AbsoluteFill style={{ transform: `translateY(-100px)` }}>
<Shrinking>
<RedHeart />
</Shrinking>
</AbsoluteFill>
</Move>
</Explosion>
);
};
src/RedHearts.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
import { Explosion } from "./Explosion";
import { Move } from "./Move";
import { RedHeart } from "./RedHeart";
import { Shrinking } from "./Shrinking";
 
export const RedHearts: React.FC = () => {
return (
<Explosion>
<Move delay={5}>
<AbsoluteFill style={{ transform: `translateY(-100px)` }}>
<Shrinking>
<RedHeart />
</Shrinking>
</AbsoluteFill>
</Move>
</Explosion>
);
};

我们做同样的事情来获得一些黄色心形在我们的动画中:

src/YellowHeart.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const YellowHeart: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
💛
</AbsoluteFill>
);
};
src/YellowHeart.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const YellowHeart: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
💛
</AbsoluteFill>
);
};

对于黄色心形,我们将通过应用 50px 的平移并给 <Move> 组件添加一个 20 帧的延迟来改变位置:

src/YellowHearts.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
import { Explosion } from "./Explosion";
import { Move } from "./Move";
import { Shrinking } from "./Shrinking";
import { YellowHeart } from "./YellowHeart";
 
export const YellowHearts: 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.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
import { Explosion } from "./Explosion";
import { Move } from "./Move";
import { Shrinking } from "./Shrinking";
import { YellowHeart } from "./YellowHeart";
 
export const YellowHearts: 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/Star.tsx 并返回一个居中的星星表情。

src/Star.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const Star: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
fontSize: 14,
}}
>
</AbsoluteFill>
);
};
src/Star.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
 
export const Star: React.FC = () => {
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
fontSize: 14,
}}
>
</AbsoluteFill>
);
};

请注意,我们需要更改星星的定位,否则它们会在 <Dots> 的上方。

让我们给 <Trail> 一个 extraOffset 属性,这样星星就可以比点更向外开始。

星星的 extraOffset 为 100 导致了与红色心形相同的开始和结束的圆周。这是调整后的 <Trail>

src/Trail.tsx
tsx
import React from "react";
import { AbsoluteFill, Sequence } from "remotion";
import { Move } from "./Move";
 
export const Trail: React.FC<{
amount: number;
extraOffset: number;
children: React.ReactNode;
}> = ({ amount, extraOffset, children }) => {
return (
<AbsoluteFill>
{new Array(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.tsx
tsx
import React from "react";
import { AbsoluteFill, Sequence } from "remotion";
import { Move } from "./Move";
 
export const Trail: React.FC<{
amount: number;
extraOffset: number;
children: React.ReactNode;
}> = ({ amount, extraOffset, children }) => {
return (
<AbsoluteFill>
{new Array(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.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
import { Explosion } from "./Explosion";
import { Shrinking } from "./Shrinking";
import { Star } from "./Star";
import { Trail } from "./Trail";
 
export const Stars: React.FC = () => {
return (
<AbsoluteFill
style={{
rotate: "0.3rad",
}}
>
<Explosion>
<Trail extraOffset={100} amount={4}>
<Shrinking>
<Star />
</Shrinking>
</Trail>
</Explosion>
</AbsoluteFill>
);
};
src/Stars.tsx
tsx
import React from "react";
import { AbsoluteFill } from "remotion";
import { Explosion } from "./Explosion";
import { Shrinking } from "./Shrinking";
import { Star } from "./Star";
import { Trail } from "./Trail";
 
export const Stars: React.FC = () => {
return (
<AbsoluteFill
style={{
rotate: "0.3rad",
}}
>
<Explosion>
<Trail extraOffset={100} amount={4}>
<Shrinking>
<Star />
</Shrinking>
</Trail>
</Explosion>
</AbsoluteFill>
);
};

这是几乎完成的烟花效果:

Slow motion effect

最后让我们给烟花应用一个慢动作效果。为此,创建一个名为 src/SlowedTrail.tsx 的新文件。它应该包含一个名为 Slowed 的组件和一个名为 remapSpeed() 的辅助函数,该函数将为烟花应用不同的速度级别。在下面的代码片段中,速度为 1.5 被应用到第 20 帧,之后速度减慢到 0.5。

src/SlowedTrail.tsx
tsx
import React from "react";
import { Freeze, interpolate, useCurrentFrame } from "remotion";
 
// remapSpeed() is a helper function for the component <Slowed> that takes a frame number and a speed
const remapSpeed = ({
frame,
speed,
}: {
frame: number;
speed: (fr: number) => number;
}) => {
let framesPassed = 0;
for (let i = 0; i <= frame; i++) {
framesPassed += speed(i);
}
 
return framesPassed;
};
 
export const Slowed: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const frame = useCurrentFrame();
const remappedFrame = 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.tsx
tsx
import React from "react";
import { Freeze, interpolate, useCurrentFrame } from "remotion";
 
// remapSpeed() is a helper function for the component <Slowed> that takes a frame number and a speed
const remapSpeed = ({
frame,
speed,
}: {
frame: number;
speed: (fr: number) => number;
}) => {
let framesPassed = 0;
for (let i = 0; i <= frame; i++) {
framesPassed += speed(i);
}
 
return framesPassed;
};
 
export const Slowed: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const frame = useCurrentFrame();
const remappedFrame = remapSpeed({
frame,
speed: (f) =>
interpolate(f, [0, 20, 21], [1.5, 1.5, 0.5], {
extrapolateRight: "clamp",
}),
});
 
return <Freeze frame={remappedFrame}>{children}</Freeze>;
};

在主组件中,将所有移动的点、心形和星星包装在组件 <Slowed> 中。正如你现在肯定已经注意到的那样,一切都是非常可组合的:

src/Composition.tsx
tsx
import React 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 const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Slowed>
<Dots />
<RedHearts />
<YellowHearts />
<Stars />
</Slowed>
</AbsoluteFill>
);
};
src/Composition.tsx
tsx
import React 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 const MyComposition = () => {
return (
<AbsoluteFill>
<Background />
<Slowed>
<Dots />
<RedHearts />
<YellowHearts />
<Stars />
</Slowed>
</AbsoluteFill>
);
};

你的最终烟花应该是这样的:

添加你的 Animoji

作为本教程的最后一步,我们将在烟花上添加你的 Animoji。对于 Animoji,你需要拥有一部 iPhone 和一台 Mac。这是如何做到的: 在 iMessage 中的 iPhone 上,录制一个 Animoji 并将其发送给朋友。完成后,它也会出现在 Mac 上的 Messages 应用中。通过右键单击在那里下载你的 Animoji。完成后,创建你的 Animoji 的透明版本。只需按照以下步骤操作:

  1. 右键单击下载的 Animoji
  2. 选择 "Services"
  3. 选择 "Encode Selected Video Files"
  4. 在设置下拉菜单中选择 "Apple ProRes"
  5. 选中 "Preserve Transparency" 复选框。

将创建一个新的编码文件,给它一个简单的名称,比如 animoji.mov。

除了 Remotion 项目中的 src 文件夹外,创建一个名为 public 的新文件夹。将编码后的视频放入此文件夹。然后,你可以使用 FFmpeg 将编码后的视频转换为一系列帧:

  1. 将当前工作目录更改为 public: cd public

  2. 使用此命令: ffmpeg -i animoji.mov -pix_fmt rgba -start_number 0 frame%03d.png