3. Effects
フィルタ
CSSプロパティやSVGの属性を時間にしたがって変化させることによって、さまざまなエフェクトを実現することができる。基本的な属性を変化させるだけでもいろいろできるほか、一見するとどうすれば実現できるかわからないようなエフェクトも、filterを使うと再現できたりする。
たとえば、次の例はテキストが細くなっ て消えるようなエフェクトを<feMorphology>を使って再現しようとしたもの。
- Video
- Code
Effects/Dropshadow.tsx
import {
AbsoluteFill,
useCurrentFrame,
interpolate,
Easing,
} from "remotion";
import React, { useMemo } from "react";
export const DropshadowText: React.FC = () => {
const frame = useCurrentFrame();
const inAmount = useMemo(() => {
return interpolate(
frame,
[0, 30],
[0, 1],
{
easing: Easing.bounce,
extrapolateRight: "clamp",
}
);
}, [frame]);
const dx = useMemo(() => {
return interpolate(
frame,
[10, 50],
[-12, 24],
{
easing: Easing.cubic,
extrapolateRight: "clamp",
}
);
}, [frame]);
const outAmount = useMemo(() => {
return interpolate(
frame,
[60, 90],
[0, 1],
{
easing: Easing.cubic,
extrapolateRight: "clamp",
}
);
}, [frame]);
return (
<>
<svg>
<defs>
<filter id="shadow">
<feMorphology operator="erode" radius={10 * outAmount} />
<feDropShadow
dx={dx}
dy={8}
stdDeviation={3}
floodOpacity={0.8}
floodColor="gold"
/>
</filter>
</defs>
</svg>
<AbsoluteFill
style={{
backgroundColor: "antiquewhite",
justifyContent: "center",
alignItems: "center",
}}
>
<h2
style={{
color: "firebrick",
textAlign: "center",
fontFamily: "sans-serif",
fontSize: 64,
filter: "url(#shadow)",
transform: `scale(${inAmount})`,
opacity: 1 - outAmount,
}}
>
Hello, Remotion!!
</h2>
</AbsoluteFill>
</>
);
};
また、次の例はTurbulent Displaceのような「滲み」をつくるエフェクトを再現しようとしたもの。
- Video
- Code
Effects/TurbulentDisplace.tsx
import {
makeCircle,
} from "@remotion/shapes";
import {
warpPath,
WarpPathFn,
} from "@remotion/paths";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
random,
interpolate,
Easing,
} from "remotion";
import React, { useMemo } from "react";
export const TurbulentDisplace: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames, width, height } = useVideoConfig();
const r = 72;
const scale = useMemo(() => {
return interpolate(
frame,
[0, 60, 61, durationInFrames],
[0, 1, 1.2, 10],
{
easing: Easing.out(Easing.circle),
}
);
}, [frame]);
const radius = r * scale;
const fn: WarpPathFn = ({ x, y }) => ({
x: x + random(scale > 1 ? 0 : frame) * 3,
y: y + random(scale > 1 ? 0 : frame) * 4,
});
const warpedPath = useMemo(() => {
const { path } = makeCircle({ radius: radius });
return warpPath(
path,
fn
);
}, [frame]);
return (
<AbsoluteFill style={{ backgroundColor: "whitesmoke" }}>
<svg
style={{
transformBox: "fill-box",
overflow: "visible"
}}
viewBox={`0 0 ${width} ${height}`}
transform={`translate(${width / 2 - radius}, ${height / 2 - radius})`}
>
<filter id="displacement">
<feTurbulence
type="fractalNoise" // "Turbulence"ではない
baseFrequency={0.04} // 周波数。大きくすると滲みが大きくなる
numOctaves={4} // 大きくすると滲みのディテールが増すが、計算量も増える
seed={1}
result="Noise"
/>
<feDisplacementMap
in2="Noise"
in="SourceGraphic"
scale={radius}
xChannelSelector="A"
yChannelSelector="A"
/>
</filter>
<path
d={warpedPath}
fill="midnightblue"
stroke="none"
filter="url(#displacement)"
/>
</svg>
</AbsoluteFill>
);
};
ここでは<feTurbulence>を使っている。この作例のように<feDisplacementMap>のscale
を徐々に大きくする場合、マッピングする元の画像も徐々に大きくしないと、滲みの端が途切れて見えてしまうので注意。この例でも、よく見るとちょっと途切れてしまっている。
パスを切り取る
パスはevolvePathを使うことで時間経過にしたがって伸ばすことができる。
- Video
- Code
Effects/TrimCircle.tsx
import {
makeCircle,
} from "@remotion/shapes";
import {
evolvePath,
translatePath,
} from "@remotion/paths";
import {
useCurrentFrame,
useVideoConfig,
interpolate,
interpolateColors,
Easing,
} from "remotion";
import React, { useMemo } from "react";
const Circle: React.FC<{ n: number }> = ({ n }) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const sw = 12;
const r = 100;
const delay = 5;
const { path } = makeCircle({ radius: r });
const evolved = useMemo(() => {
return new Array(n).fill(0).map((_, i) => {
const step = interpolate(
frame,
[i * delay, i * delay + fps],
[0, 1],
{
easing: Easing.out(Easing.poly(5)),
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}
);
return {
...evolvePath(step, path),
stroke: `${interpolateColors(i, [0, n], ["#c4ffea", "#ff7b00"])}`,
};
});
}, [frame]);
return (
<>
{evolved.map((p, i) => {
return (
<path
key={i}
d={translatePath(path, width / 2 - r, height / 2 - r)}
stroke={p.stroke}
strokeWidth={sw}
strokeDasharray={p.strokeDasharray}
strokeDashoffset={p.strokeDashoffset}
fill="none"
style={{
transformOrigin: "center",
}}
/>
)
})}
</>
);
};
export const TrimCircle: React.FC = () => {
const { width, height } = useVideoConfig();
const n = 9;
return (
<svg
viewBox={`0 0 ${width} ${height}`}
style={{
backgroundColor: "#f3eed5",
}}
>
<Circle n={n} />
</svg>
);
}