From 41a3b9a97bb95e4dedc3b50ffb93232a5c1ee1af Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 16:38:03 +0800 Subject: [PATCH] feat(videos): add RotatingTaglines and GradientMesh components --- apps/videos/src/components/GradientMesh.tsx | 44 ++++++++++ .../src/components/RotatingTaglines.tsx | 80 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 apps/videos/src/components/GradientMesh.tsx create mode 100644 apps/videos/src/components/RotatingTaglines.tsx diff --git a/apps/videos/src/components/GradientMesh.tsx b/apps/videos/src/components/GradientMesh.tsx new file mode 100644 index 00000000..33c80021 --- /dev/null +++ b/apps/videos/src/components/GradientMesh.tsx @@ -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 ( + + {blobs.map((blob, i) => ( + + ))} + + ); +}; diff --git a/apps/videos/src/components/RotatingTaglines.tsx b/apps/videos/src/components/RotatingTaglines.tsx new file mode 100644 index 00000000..f7801894 --- /dev/null +++ b/apps/videos/src/components/RotatingTaglines.tsx @@ -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 ( +
+
+
+ {lines[lineIndex]} +
+
+ ); +};