mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
chore(videos): merge base workspace files into promo videos branch
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -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));
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { registerRoot } from "remotion";
|
||||
import { RemotionRoot } from "./Root";
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user