feat(videos): add LogoReveal and GitHubCTA components

This commit is contained in:
SnapOtter
2026-05-08 16:38:39 +08:00
parent 72b0077a57
commit 43fbb23f19
2 changed files with 243 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import type React from "react";
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
import { ClipReveal } from "@/components/ClipReveal";
import { COLOR } from "@/lib/colors";
import { FONT, TEXT } from "@/lib/fonts";
export const GitHubCTA: React.FC<{
labelFrame?: number;
logoFrame?: number;
taglineFrame?: number;
ctaFrame?: number;
urlFrame?: number;
}> = ({ labelFrame = 0, logoFrame = 15, taglineFrame = 30, ctaFrame = 50, urlFrame = 65 }) => {
const frame = useCurrentFrame();
const logoScale = interpolate(frame, [logoFrame, logoFrame + 15], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const glowRadius = interpolate(frame, [logoFrame, logoFrame + 15], [0, 200], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const borderOpacity = interpolate(Math.sin(frame * 0.06), [-1, 1], [0.3, 0.6]);
return (
<AbsoluteFill
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
border: `2px solid rgba(245, 158, 11, ${borderOpacity})`,
}}
>
<ClipReveal startFrame={labelFrame}>
<span
style={{
...TEXT.label,
color: COLOR.accent,
fontSize: 18,
}}
>
100% OPEN SOURCE
</span>
</ClipReveal>
<div style={{ height: 24 }} />
<div style={{ position: "relative" }}>
<div
style={{
position: "absolute",
width: glowRadius * 2,
height: glowRadius * 2,
borderRadius: "50%",
background: `radial-gradient(circle, ${COLOR.accent} 0%, transparent 70%)`,
opacity: 0.15,
left: 40 - glowRadius,
top: 40 - glowRadius,
}}
/>
<Img
src={staticFile("logo.png")}
style={{
width: 80,
height: 80,
transform: `scale(${logoScale})`,
}}
/>
</div>
<div style={{ height: 20 }} />
<ClipReveal startFrame={taglineFrame}>
<span style={{ ...TEXT.sectionTitle, fontSize: 48 }}>Free forever.</span>
</ClipReveal>
<div style={{ height: 16 }} />
<ClipReveal startFrame={ctaFrame}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
<span style={{ fontFamily: FONT.body, fontWeight: 400, fontSize: 20, color: "white" }}>
Star us on GitHub
</span>
</div>
</ClipReveal>
<div style={{ height: 8 }} />
<ClipReveal startFrame={urlFrame}>
<span style={{ fontFamily: FONT.mono, fontSize: 16, color: COLOR.accent }}>
github.com/snapotter-hq/SnapOtter
</span>
</ClipReveal>
</AbsoluteFill>
);
};
+140
View File
@@ -0,0 +1,140 @@
import type React from "react";
import {
AbsoluteFill,
Img,
interpolate,
random,
spring,
staticFile,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { ClipReveal } from "@/components/ClipReveal";
import { COLOR } from "@/lib/colors";
import { TEXT } from "@/lib/fonts";
import { SPRING } from "@/lib/motion";
const PARTICLE_COUNT = 25;
export const LogoReveal: React.FC<{
convergeFrame?: number;
burstFrame?: number;
logoFrame?: number;
textFrame?: number;
taglineFrame?: number;
logoSize?: number;
}> = ({
convergeFrame = 0,
burstFrame = 30,
logoFrame = 30,
textFrame = 50,
taglineFrame = 70,
logoSize = 80,
}) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const logoScale = spring({
frame: frame - logoFrame,
fps,
config: SPRING.popIn,
});
const glowRadius = interpolate(frame, [burstFrame, burstFrame + 15], [0, 200], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const glowOpacity = interpolate(frame, [burstFrame, burstFrame + 20], [0.2, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{Array.from({ length: PARTICLE_COUNT }).map((_, i) => {
const baseAngle = random(`particle-angle-${i}`) * Math.PI * 2;
const maxRadius = 300 + random(`particle-radius-${i}`) * 200;
const size = 2 + random(`particle-size-${i}`) * 3;
const rotSpeed = 0.03 + random(`particle-speed-${i}`) * 0.02;
const particleOpacity = 0.3 + random(`particle-opacity-${i}`) * 0.7;
const convergeProgress = interpolate(frame, [convergeFrame, burstFrame], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const angle = baseAngle + frame * rotSpeed;
const radius = maxRadius * convergeProgress;
const px = width / 2 + Math.cos(angle) * radius;
const py = height / 2 + Math.sin(angle) * radius;
const pOpacity = convergeProgress > 0.05 ? particleOpacity : 0;
return (
<div
key={`particle-${i}`}
style={{
position: "absolute",
left: px - size / 2,
top: py - size / 2,
width: size,
height: size,
borderRadius: "50%",
backgroundColor: COLOR.accent,
opacity: pOpacity,
}}
/>
);
})}
<div
style={{
position: "absolute",
width: glowRadius * 2,
height: glowRadius * 2,
borderRadius: "50%",
background: `radial-gradient(circle, ${COLOR.accent} 0%, transparent 70%)`,
opacity: glowOpacity,
left: width / 2 - glowRadius,
top: height / 2 - glowRadius,
}}
/>
<Img
src={staticFile("logo.png")}
style={{
width: logoSize,
height: logoSize,
transform: `scale(${logoScale})`,
}}
/>
<div style={{ height: 16 }} />
<ClipReveal startFrame={textFrame}>
<span style={{ ...TEXT.sectionTitle, fontSize: 48, color: "white" }}>SnapOtter</span>
</ClipReveal>
<div style={{ height: 8 }} />
<ClipReveal startFrame={taglineFrame}>
<span
style={{
fontFamily: TEXT.heroSub.fontFamily,
fontWeight: 500,
fontSize: 24,
color: COLOR.accent,
}}
>
Your images. Stay yours.
</span>
</ClipReveal>
</AbsoluteFill>
);
};