chore(videos): merge base workspace files into promo videos branch

This commit is contained in:
SnapOtter
2026-05-08 17:10:00 +08:00
19 changed files with 1233 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+6
View File
@@ -0,0 +1,6 @@
import { Config } from "@remotion/cli/config";
import { enableTailwind } from "@remotion/tailwind";
Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);
Config.overrideWebpackConfig((config) => enableTailwind(config));
+36
View File
@@ -0,0 +1,36 @@
import type React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { EASE } from "@/lib/motion";
export const ClipReveal: React.FC<{
children: React.ReactNode;
startFrame: number;
duration?: number;
direction?: "up" | "down";
}> = ({ children, startFrame, duration = 15, direction = "up" }) => {
const frame = useCurrentFrame();
const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: EASE.enter,
});
const translateY =
direction === "up"
? interpolate(progress, [0, 1], [40, 0])
: interpolate(progress, [0, 1], [-40, 0]);
return (
<div style={{ overflow: "hidden", display: "inline-block" }}>
<div
style={{
transform: `translateY(${translateY}px)`,
opacity: interpolate(progress, [0, 0.3], [0, 1], {
extrapolateRight: "clamp",
}),
}}
>
{children}
</div>
</div>
);
};
+21
View File
@@ -0,0 +1,21 @@
import type React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { EASE } from "@/lib/motion";
export const Counter: React.FC<{
from: number;
to: number;
startFrame: number;
duration: number;
style?: React.CSSProperties;
format?: (n: number) => string;
}> = ({ from, to, startFrame, duration, style, format }) => {
const frame = useCurrentFrame();
const raw = interpolate(frame, [startFrame, startFrame + duration], [from, to], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: EASE.enter,
});
const value = Math.floor(raw);
return <span style={style}>{format ? format(value) : String(value)}</span>;
};
@@ -0,0 +1,41 @@
import type React from "react";
import { useCurrentFrame } from "remotion";
interface BlobConfig {
color: string;
radius: number;
cx: number;
cy: number;
a: number;
b: number;
phaseX: number;
phaseY: number;
amplitudeX: number;
amplitudeY: number;
}
export const GradientBlob: React.FC<{
config: BlobConfig;
duration: number;
}> = ({ config, duration }) => {
const frame = useCurrentFrame();
const t = (frame / duration) * Math.PI * 2;
const x = config.cx + config.amplitudeX * Math.sin(config.a * t + config.phaseX);
const y = config.cy + config.amplitudeY * Math.sin(config.b * t + config.phaseY);
return (
<div
style={{
position: "absolute",
width: config.radius * 2,
height: config.radius * 2,
left: x - config.radius,
top: y - config.radius,
background: `radial-gradient(circle, ${config.color} 0%, transparent 70%)`,
filter: "blur(60px)",
opacity: 0.5,
mixBlendMode: "screen",
}}
/>
);
};
@@ -0,0 +1,23 @@
import type React from "react";
import { AbsoluteFill, useCurrentFrame } from "remotion";
export const GrainOverlay: React.FC<{ opacity?: number }> = ({ opacity = 0.03 }) => {
const frame = useCurrentFrame();
return (
<AbsoluteFill style={{ opacity, mixBlendMode: "overlay", pointerEvents: "none" }}>
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<filter id={`grain-${frame}`}>
<feTurbulence
type="fractalNoise"
baseFrequency={0.65}
numOctaves={3}
seed={frame}
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
</filter>
<rect width="100%" height="100%" filter={`url(#grain-${frame})`} />
</svg>
</AbsoluteFill>
);
};
@@ -0,0 +1,20 @@
import type React from "react";
export const PhotoPlaceholder: React.FC<{
width: number;
height: number;
hue?: number;
style?: React.CSSProperties;
}> = ({ width, height, hue = 30, style }) => (
<div
style={{
width,
height,
borderRadius: 8,
background: `linear-gradient(135deg, hsl(${hue}, 60%, 65%) 0%, hsl(${hue + 30}, 50%, 55%) 100%)`,
border: "1.5px solid rgba(255,255,255,0.15)",
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
...style,
}}
/>
);
+28
View File
@@ -0,0 +1,28 @@
import type React from "react";
import { COLOR } from "@/lib/colors";
import { TEXT } from "@/lib/fonts";
export const ToolPill: React.FC<{
name: string;
category: string;
style?: React.CSSProperties;
}> = ({ name, category, style }) => {
const color = COLOR.category[category] ?? COLOR.accent;
return (
<div
style={{
display: "inline-flex",
alignItems: "center",
padding: "4px 12px",
borderRadius: 6,
backgroundColor: color,
color: "white",
...TEXT.toolPill,
whiteSpace: "nowrap",
...style,
}}
>
{name}
</div>
);
};
+55
View File
@@ -0,0 +1,55 @@
import type React from "react";
import { useCurrentFrame } from "remotion";
import { FONT } from "@/lib/fonts";
interface Segment {
text: string;
color: string;
}
export const TypeWriter: React.FC<{
segments: Segment[];
startFrame: number;
speed?: number;
style?: React.CSSProperties;
}> = ({ segments, startFrame, speed = 2, style }) => {
const frame = useCurrentFrame();
const elapsed = Math.max(0, frame - startFrame);
const charIndex = Math.floor(elapsed / speed);
const fullText = segments.map((s) => s.text).join("");
const visibleChars = Math.min(charIndex, fullText.length);
let rendered = 0;
const nodes: React.ReactNode[] = [];
for (const seg of segments) {
const segStart = rendered;
const visible = Math.max(0, Math.min(visibleChars - segStart, seg.text.length));
if (visible > 0) {
nodes.push(
<span key={segStart} style={{ color: seg.color }}>
{seg.text.slice(0, visible)}
</span>,
);
}
rendered += seg.text.length;
if (rendered >= visibleChars) break;
}
const showCursor = visibleChars < fullText.length && Math.floor(frame / 8) % 2 === 0;
return (
<span style={{ fontFamily: FONT.mono, ...style }}>
{nodes}
{showCursor && (
<span
style={{
backgroundColor: "#7ee787",
width: 10,
height: 20,
display: "inline-block",
}}
/>
)}
</span>
);
};
@@ -0,0 +1,21 @@
import type React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
import { EASE } from "@/lib/motion";
export const WipeTransition: React.FC<{
children: React.ReactNode;
startFrame: number;
duration?: number;
direction?: "left" | "right";
}> = ({ children, startFrame, duration = 18, direction = "left" }) => {
const frame = useCurrentFrame();
const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 100], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: EASE.enter,
});
const clipPath =
direction === "left" ? `inset(0 ${100 - progress}% 0 0)` : `inset(0 0 0 ${100 - progress}%)`;
return <AbsoluteFill style={{ clipPath }}>{children}</AbsoluteFill>;
};
+4
View File
@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
+48
View File
@@ -0,0 +1,48 @@
export const COLOR = {
accent: "#f59e0b",
accentHover: "#d97706",
dark: "#0c0a09",
darkAlt: "#1a1a2e",
light: "#ffffff",
lightAlt: "#fafaf9",
warmGradientFrom: "#f59e0b",
warmGradientVia: "#f97316",
warmGradientTo: "#ef4444",
category: {
essentials: "#3B82F6",
optimization: "#10B981",
adjustments: "#8B5CF6",
watermark: "#EF4444",
utilities: "#6366F1",
layout: "#EC4899",
format: "#14B8A6",
ai: "#F59E0B",
} as Record<string, string>,
safe: "#22c55e",
danger: "#ef4444",
muted: "#737373",
cloudRed: "#fef2f2",
localGreen: "#f0fdf4",
};
export const CATEGORY_ORDER = [
"essentials",
"optimization",
"adjustments",
"ai",
"watermark",
"utilities",
"layout",
"format",
] as const;
export const CATEGORY_LABELS: Record<string, string> = {
essentials: "Essentials",
optimization: "Optimization",
adjustments: "Adjustments",
ai: "AI Tools",
watermark: "Watermark",
utilities: "Utilities",
layout: "Layout",
format: "Format",
};
+67
View File
@@ -0,0 +1,67 @@
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { loadFont as loadJetBrainsMono } from "@remotion/google-fonts/JetBrainsMono";
import { loadFont as loadNunito } from "@remotion/google-fonts/Nunito";
const { fontFamily: nunito } = loadNunito();
const { fontFamily: inter } = loadInter();
const { fontFamily: mono } = loadJetBrainsMono();
export const FONT = { heading: nunito, body: inter, mono };
export const TEXT = {
heroHeadline: {
fontFamily: nunito,
fontWeight: 800,
fontSize: 72,
letterSpacing: "-0.02em",
lineHeight: 1.1,
color: "white",
},
heroSub: {
fontFamily: inter,
fontWeight: 500,
fontSize: 28,
letterSpacing: "0em",
lineHeight: 1.4,
color: "white",
},
sectionTitle: {
fontFamily: nunito,
fontWeight: 700,
fontSize: 48,
letterSpacing: "-0.01em",
color: "white",
},
label: {
fontFamily: inter,
fontWeight: 600,
fontSize: 18,
letterSpacing: "0.04em",
textTransform: "uppercase" as const,
},
body: {
fontFamily: inter,
fontWeight: 400,
fontSize: 20,
lineHeight: 1.5,
},
mono: {
fontFamily: mono,
fontWeight: 400,
fontSize: 16,
lineHeight: 1.6,
},
toolPill: {
fontFamily: inter,
fontWeight: 600,
fontSize: 14,
letterSpacing: "0.01em",
},
counter: {
fontFamily: nunito,
fontWeight: 800,
fontSize: 96,
letterSpacing: "-0.03em",
color: "white",
},
} as const;
+30
View File
@@ -0,0 +1,30 @@
import { Easing } from "remotion";
export const EASE = {
enter: Easing.bezier(0.16, 1, 0.3, 1),
exit: Easing.bezier(0.55, 0, 1, 0.45),
emphasis: Easing.bezier(0.34, 1.56, 0.64, 1),
smooth: Easing.bezier(0.37, 0, 0.63, 1),
snap: Easing.bezier(0.22, 1, 0.36, 1),
};
export const SPRING = {
snappy: { damping: 200, stiffness: 100, mass: 0.5 },
natural: { damping: 15, stiffness: 80, mass: 1 },
popIn: { damping: 12, stiffness: 200, mass: 0.6 },
heavy: { damping: 20, stiffness: 60, mass: 2 },
settle: { damping: 18, stiffness: 150, mass: 0.8 },
};
export const TIMING = {
fps: 30,
staggerFrames: 2,
holdShort: 30,
holdMedium: 60,
holdLong: 90,
fadeIn: 12,
fadeOut: 8,
wipe: 18,
sectionGap: 12,
anticipation: 3,
};
+68
View File
@@ -0,0 +1,68 @@
export interface ToolDef {
name: string;
category: string;
}
export const TOOLS: ToolDef[] = [
// Essentials
{ name: "Resize", category: "essentials" },
{ name: "Crop", category: "essentials" },
{ name: "Rotate & Flip", category: "essentials" },
{ name: "Convert", category: "essentials" },
{ name: "Compress", category: "essentials" },
// Optimization
{ name: "Optimize for Web", category: "optimization" },
{ name: "Remove Metadata", category: "optimization" },
{ name: "Edit Metadata", category: "optimization" },
{ name: "Bulk Rename", category: "optimization" },
{ name: "Image to PDF", category: "optimization" },
{ name: "Favicon Generator", category: "optimization" },
// Adjustments
{ name: "Adjust Colors", category: "adjustments" },
{ name: "Sharpening", category: "adjustments" },
{ name: "Replace & Invert Color", category: "adjustments" },
{ name: "Color Blindness Simulation", category: "adjustments" },
// AI Tools
{ name: "Remove Background", category: "ai" },
{ name: "Image Upscaling", category: "ai" },
{ name: "Object Eraser", category: "ai" },
{ name: "OCR / Text Extraction", category: "ai" },
{ name: "Face / PII Blur", category: "ai" },
{ name: "Smart Crop", category: "ai" },
{ name: "Image Enhancement", category: "ai" },
{ name: "Face Enhancement", category: "ai" },
{ name: "AI Colorization", category: "ai" },
{ name: "Noise Removal", category: "ai" },
{ name: "Red Eye Removal", category: "ai" },
{ name: "Photo Restoration", category: "ai" },
{ name: "Passport Photo", category: "ai" },
{ name: "Content-Aware Resize", category: "ai" },
{ name: "PNG Transparency Fixer", category: "ai" },
// Watermark & Overlay
{ name: "Text Watermark", category: "watermark" },
{ name: "Image Watermark", category: "watermark" },
{ name: "Text Overlay", category: "watermark" },
{ name: "Image Composition", category: "watermark" },
// Utilities
{ name: "Image Info", category: "utilities" },
{ name: "Image Compare", category: "utilities" },
{ name: "Find Duplicates", category: "utilities" },
{ name: "Color Palette", category: "utilities" },
{ name: "QR Code Generator", category: "utilities" },
{ name: "Barcode Reader", category: "utilities" },
{ name: "Image to Base64", category: "utilities" },
// Layout & Composition
{ name: "Collage / Grid", category: "layout" },
{ name: "Stitch / Combine", category: "layout" },
{ name: "Image Splitting", category: "layout" },
{ name: "Border & Frame", category: "layout" },
// Format & Conversion
{ name: "SVG to Raster", category: "format" },
{ name: "Image to SVG", category: "format" },
{ name: "GIF Tools", category: "format" },
{ name: "PDF to Image", category: "format" },
];
export function getToolsByCategory(category: string): ToolDef[] {
return TOOLS.filter((t) => t.category === category);
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+13
View File
@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
fontFamily: {
heading: ["Nunito", "sans-serif"],
body: ["Inter", "sans-serif"],
mono: ["JetBrains Mono", "monospace"],
},
},
},
};
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"noEmit": true,
"lib": ["ES2022", "DOM"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "remotion.config.ts"]
}