2. Layout
p5.jsとの違い
この資料では、次のWebサイトに掲載されているアニメーションを参考にした作例を掲載する。
このうち、一つめのWebサイトは、p5.jsを使ったアニメーションを実装している。たとえば、イージングという章には、次のようなコードが載せられている。ここでlerp()
は、引数a, b, t
を受け取ってa + (b - a) * t
を返す、線形補間をするための関数である。
let prevR, nextR, d, t;
function setup() {
createCanvas(windowWidth, windowHeight);
d = 200;
reset();
}
function reset() {
prevR = d;
nextR = random(20, 400);
t = 0;
}
function draw() {
t += 0.01;
// イージング後の余韻を持たせるため、すぐにはリセットしない
if (t >= 1.4) {
reset();
return;
}
// t = 1.0 でイージング終了なので、t > 1.0 の場合は画面を更新しない
if (t > 1.0) {
return;
}
clear();
d = lerp(prevR, nextR, easeInOutBack(t));
circle(width / 2, height / 2, d);
}
function easeInOutBack(t) {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return t < 0.5 ? (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 : (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
p5.jsは、アニメーションループのなかでdraw()
が繰り返し呼び出され、Canvas要素を更新することで描画をおこなう仕組みになっている。一方で、Remotionにおいては、これと同じようなアニメーションループが存在しないため、RemotionのCompositionに渡されるReactコンポーネントでは、副作用がある書き方はできない点に注意が必要だ。
この点に気をつけながら、上のコードをRemotion向けに書き直すと、 たとえば、次のように書けるだろう。
- Video
- Code
import {
AbsoluteFill,
Sequence,
useCurrentFrame,
useVideoConfig,
random,
interpolate,
Easing,
} from "remotion";
import React, { useMemo } from "react";
export const ResizingCircle: React.FC<{
startRadius: number;
endRadius: number;
cx: number;
cy: number;
}> = ({ startRadius, endRadius, cx, cy }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const currentRadius = useMemo(() => {
return interpolate(
frame,
[0, fps * 2 - 10],
[startRadius, endRadius],
{
easing: Easing.inOut(Easing.back(3)),
extrapolateRight: "clamp",
}
);
}, [frame]);
return (
<circle cx={cx} cy={cy} r={currentRadius} fill="whitesmoke" />
);
};
export const ResizeCircle: React.FC = () => {
const { fps, durationInFrames, width, height } = useVideoConfig();
const numChanges = 4 + 1;
const radii = new Array(numChanges).fill(0).map((_, i) => random(`${i}th radius`) * 200);
return (
<>
{radii.slice(1, numChanges).map((endRadius, i) => (
<Sequence
key={i}
from={i * fps * 2}
durationInFrames={i != numChanges - 2 ? fps * 2 : durationInFrames - i * fps * 2}
>
<AbsoluteFill style={{ backgroundColor: "#292a33" }}>
<svg width={width} height={height} >
<ResizingCircle
startRadius={radii[i]}
endRadius={endRadius}
cx={width / 2}
cy={height / 2}
/>
</svg>
</AbsoluteFill>
</Sequence>
))}
</>
);
};
繰り返す・並べる
Remotionでアニメーションを実装する際には、上の例のように、適当な粒度のアニメーションごとにコンポーネントを切り分けるようにすると扱いやすい。そうして用意したコンポーネントをフレームに応じて出し分けるには、Sequenceや、Series、TransitionSeriesを使うことができる。
一方で、同じようなアニメーションを繰り返したい場合には、次の例のように、Loopを使うのが便利だ。
- Video
- Code
import {
AbsoluteFill,
Loop,
useCurrentFrame,
useVideoConfig,
random,
interpolate,
Easing,
} from "remotion";
import React, { useMemo } from "react";
import { ResizingCircle } from "./ResizeCircle";
const Circles: React.FC = () => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const margin = 32;
const cols = 15;
const rows = 12;
const opacity = useMemo(() => {
return interpolate(
frame,
[0, fps, fps * 2 - 10, fps * 2],
[0, 1, 1, 0],
{
easing: Easing.ease,
}
);
}, [frame]);
const loop = Loop.useLoop();
const iteration = useMemo(() => {
return loop ? loop.iteration : 0;
}, [loop]);
return (
<svg width={width} height={height} style={{ overflow: "visible", opacity }}>
{new Array(cols).fill(0).map((_, i) => {
return new Array(rows).fill(0).map((__, j) => {
const key = `${i}-${j}`;
const r = random(`${key}-${iteration}`) * margin / 2;
const x = i * (width + margin) / cols;
const y = j * (height + margin) / rows;
return (
<ResizingCircle
key={key}
startRadius={0}
endRadius={r}
cx={x}
cy={y}
/>
);
})
})
}
</svg>
);
};
export const RepeatCircle: React.FC = () => {
const { durationInFrames } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: "#292a33" }}>
<Loop durationInFrames={durationInFrames / 4}>
<Circles />
</Loop>
</AbsoluteFill>
);
};
ちなみに、Remotionで描画する要素はSVGでなくてもかまわない。JSXとして描画できるものであれば大抵のものは描画できるので、もちろん、ふつうのdiv要素を並べることもできる。
- Video
- Code
import { noise2D } from "@remotion/noise";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
} from "remotion";
import React, { useMemo } from "react";
const Shapes: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const margin = 24;
const n = margin - 2;
const time = useMemo(() => frame / fps * 1.2, [frame]);
return (
<>
{new Array(n).fill(0).map((_, i) => {
const noise = noise2D(i, margin, time);
const l = 12 + margin * Math.abs(noise);
const r = margin * interpolate(noise, [-1, 1], [0, 1], { easing: Easing.cubic })
return (
<div
key={i}
style={{
width: l,
height: l,
borderRadius: r,
backgroundColor: "whitesmoke",
}}
>
</div>
);
})
}
</>
);
};
export const RepeatDiv: React.FC = () => {
return (
<AbsoluteFill
style={{
backgroundColor: "#292a33",
justifyContent: "space-evenly",
alignItems: "center",
flexFlow: "row wrap",
}}
>
<Shapes />
</AbsoluteFill>
);
};
重ねる・ずらす
JSX(HTML)なので、コードとして下に書かれている要素ほど、上に重なるように描画される。
- Video
- Code
import {
useCurrentFrame,
useVideoConfig,
random,
interpolate,
Easing,
} from "remotion";
import React, { useMemo } from "react";
export const OverlapCircle: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames, width, height } = useVideoConfig();
const numCircles = 40;
const circles = useMemo(() => {
const step = interpolate(
frame,
[0, durationInFrames - 30],
[0, numCircles],
{
easing: Easing.out(Easing.cubic),
}
);
return new Array(numCircles)
.fill(0)
.map((_, i) => i * 3)
.slice(0, Math.round(step));
}, [frame]);
return (
<svg
style={{
position: "absolute",
transformBox: "fill-box",
backgroundColor: "#f3eed5",
}}
viewBox={`0 0 ${width} ${height}`}
>
{circles.map((r, i) => {
return (
<circle
key={i}
cx={r}
cy={r}
r={r}
fill="none"
stroke="#e5af9b"
strokeWidth={3 * random(i)}
transform={`translate(${r}, ${r})`}
/>
);
})}
</svg>
);
};
mix-blend-modeを使うと、重なり方に変化を付けることができて面白い。また、よくある小ワザとして、要素の位置に応じて、ぼかしをかけたり、アニメーションの量に強弱を付けたりすると、遠近感を出すことができる。
- Video
- Code
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
random,
interpolate,
interpolateColors,
Easing,
} from "remotion";
import React, { useMemo } from "react";
export const OverlapDiv: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames, width, height } = useVideoConfig();
const numCircles = 70;
const props = useMemo(() =>
new Array(numCircles)
.fill(0)
.map((_, i) => {
const offset = interpolate(
frame,
[0, durationInFrames - 10],
[-50, 150],
{
easing: Easing.linear
}
) * random(i) * 2.4;
return {
r: interpolate(random(i), [0, 1], [20, 100]),
top: interpolate(random(i * height), [0, 1], [0, height]),
left: interpolate(random(i * width), [0, 1], [-50, width + 150]) - offset,
borderColor: interpolateColors(random(i), [0, 1], ["#FF0000", "#00FF00",]),
borderStyle: i % 2 ? "outset" : "inset",
borderWidth: random(i) * 16,
blurAmount: interpolate(
numCircles - i,
[0, numCircles],
[0, 1.4],
{
easing: Easing.cubic
}
),
};
}),
[frame]
);
return (
<AbsoluteFill
style={{
backgroundColor: "#f3eed5",
isolation: "isolate",
}}
>
{props.map(({ r, top, left, borderColor, borderStyle, borderWidth, blurAmount }, i) => {
return (
<div
key={i}
style={{
position: "absolute",
top,
left,
width: r,
height: r,
borderRadius: r,
borderStyle,
borderColor,
borderWidth,
mixBlendMode: "exclusion",
filter: `blur(${blurAmount}px)`
}}
></div>
);
})}
</AbsoluteFill>
)
};