feat(videos): add RotatingTaglines and GradientMesh components

This commit is contained in:
SnapOtter
2026-05-08 16:38:03 +08:00
parent 1702492ef3
commit 41a3b9a97b
2 changed files with 124 additions and 0 deletions
@@ -0,0 +1,44 @@
import type React from "react";
import type { CSSProperties } from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
import { GradientBlob } from "@/components/GradientBlob";
interface BlobDef {
color: string;
radius: number;
cx: number;
cy: number;
a: number;
b: number;
phaseX: number;
phaseY: number;
amplitudeX: number;
amplitudeY: number;
}
export const GradientMesh: React.FC<{
blobs: BlobDef[];
duration: number;
baseOpacity?: number;
opacityRange?: [number, number];
opacityFrameRange?: [number, number];
style?: CSSProperties;
}> = ({ blobs, duration, baseOpacity, opacityRange, opacityFrameRange, style }) => {
const frame = useCurrentFrame();
let containerOpacity = baseOpacity ?? 1;
if (opacityRange && opacityFrameRange) {
containerOpacity = interpolate(frame, opacityFrameRange, opacityRange, {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
}
return (
<AbsoluteFill style={{ opacity: containerOpacity, ...style }}>
{blobs.map((blob, i) => (
<GradientBlob key={`mesh-blob-${i}`} config={blob} duration={duration} />
))}
</AbsoluteFill>
);
};
@@ -0,0 +1,80 @@
import type React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { COLOR } from "@/lib/colors";
import { FONT } from "@/lib/fonts";
import { EASE } from "@/lib/motion";
export const RotatingTaglines: React.FC<{
lines: string[];
startFrame: number;
framesPerLine: number;
fontSize?: number;
color?: string;
}> = ({ lines, startFrame, framesPerLine, fontSize = 36, color = COLOR.accent }) => {
const frame = useCurrentFrame();
const elapsed = frame - startFrame;
if (elapsed < 0) return null;
const lineIndex = Math.floor(elapsed / framesPerLine);
const lineProgress = (elapsed % framesPerLine) / framesPerLine;
if (lineIndex >= lines.length) return null;
const enterDuration = 0.2;
const holdEnd = 0.7;
const exitEnd = 1.0;
const opacity =
lineProgress < enterDuration
? interpolate(lineProgress, [0, enterDuration], [0, 1], { easing: EASE.enter })
: lineProgress < holdEnd
? 1
: interpolate(lineProgress, [holdEnd, exitEnd], [1, 0], { easing: EASE.exit });
const translateY =
lineProgress < enterDuration
? interpolate(lineProgress, [0, enterDuration], [30, 0], { easing: EASE.enter })
: lineProgress > holdEnd
? interpolate(lineProgress, [holdEnd, exitEnd], [0, -20], { easing: EASE.exit })
: 0;
const glowOpacity = interpolate(Math.sin(frame * 0.08), [-1, 1], [0.03, 0.08]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
position: "relative",
}}
>
<div
style={{
position: "absolute",
width: 400,
height: 100,
borderRadius: "50%",
background: `radial-gradient(ellipse, ${COLOR.accent} 0%, transparent 70%)`,
opacity: glowOpacity,
filter: "blur(40px)",
}}
/>
<div
style={{
fontFamily: FONT.body,
fontWeight: 500,
fontSize,
color,
opacity,
transform: `translateY(${translateY}px)`,
textAlign: "center",
}}
>
{lines[lineIndex]}
</div>
</div>
);
};