mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(videos): add OneCommand hero composition with terminal-to-app morph
Restore missing landing page compositions (FloatingTools, BrandGradient, FormatUniverse) lost during branch merge, and fix the @/ path alias in remotion webpack config to support all composition imports.
This commit is contained in:
@@ -1,6 +1,19 @@
|
|||||||
|
import path from "node:path";
|
||||||
import { Config } from "@remotion/cli/config";
|
import { Config } from "@remotion/cli/config";
|
||||||
import { enableTailwind } from "@remotion/tailwind";
|
import { enableTailwind } from "@remotion/tailwind";
|
||||||
|
|
||||||
Config.setVideoImageFormat("jpeg");
|
Config.setVideoImageFormat("jpeg");
|
||||||
Config.setOverwriteOutput(true);
|
Config.setOverwriteOutput(true);
|
||||||
Config.overrideWebpackConfig((config) => enableTailwind(config));
|
Config.overrideWebpackConfig((config) => {
|
||||||
|
const withTailwind = enableTailwind(config);
|
||||||
|
return {
|
||||||
|
...withTailwind,
|
||||||
|
resolve: {
|
||||||
|
...withTailwind.resolve,
|
||||||
|
alias: {
|
||||||
|
...withTailwind.resolve?.alias,
|
||||||
|
"@": path.resolve("./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { AbsoluteFill } from "remotion";
|
||||||
|
import { GradientBlob } from "@/components/GradientBlob";
|
||||||
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
|
import { COLOR } from "@/lib/colors";
|
||||||
|
|
||||||
|
const DURATION = 150;
|
||||||
|
|
||||||
|
const BLOBS = [
|
||||||
|
{
|
||||||
|
color: "#f59e0b",
|
||||||
|
radius: 180,
|
||||||
|
cx: 200,
|
||||||
|
cy: 150,
|
||||||
|
a: 2,
|
||||||
|
b: 3,
|
||||||
|
phaseX: 0,
|
||||||
|
phaseY: 0,
|
||||||
|
amplitudeX: 80,
|
||||||
|
amplitudeY: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#f97316",
|
||||||
|
radius: 150,
|
||||||
|
cx: 500,
|
||||||
|
cy: 350,
|
||||||
|
a: 3,
|
||||||
|
b: 2,
|
||||||
|
phaseX: 1.2,
|
||||||
|
phaseY: 0.8,
|
||||||
|
amplitudeX: 70,
|
||||||
|
amplitudeY: 90,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#3b82f6",
|
||||||
|
radius: 200,
|
||||||
|
cx: 600,
|
||||||
|
cy: 150,
|
||||||
|
a: 2,
|
||||||
|
b: 1,
|
||||||
|
phaseX: 2.4,
|
||||||
|
phaseY: 1.6,
|
||||||
|
amplitudeX: 90,
|
||||||
|
amplitudeY: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#14b8a6",
|
||||||
|
radius: 140,
|
||||||
|
cx: 350,
|
||||||
|
cy: 400,
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
phaseX: 3.6,
|
||||||
|
phaseY: 2.4,
|
||||||
|
amplitudeX: 60,
|
||||||
|
amplitudeY: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#8b5cf6",
|
||||||
|
radius: 160,
|
||||||
|
cx: 150,
|
||||||
|
cy: 350,
|
||||||
|
a: 3,
|
||||||
|
b: 1,
|
||||||
|
phaseX: 4.8,
|
||||||
|
phaseY: 3.2,
|
||||||
|
amplitudeX: 50,
|
||||||
|
amplitudeY: 70,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BrandGradient: React.FC = () => (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: COLOR.dark, overflow: "hidden" }}>
|
||||||
|
{BLOBS.map((blob) => (
|
||||||
|
<GradientBlob key={blob.color} config={blob} duration={DURATION} />
|
||||||
|
))}
|
||||||
|
<GrainOverlay opacity={0.04} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { AbsoluteFill, random, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
|
import { COLOR } from "@/lib/colors";
|
||||||
|
import { FONT } from "@/lib/fonts";
|
||||||
|
import { TOOLS } from "@/lib/tools";
|
||||||
|
|
||||||
|
const LAYERS = [
|
||||||
|
{ speed: 0.8, fontSize: 14, opacity: 0.06, count: 10 },
|
||||||
|
{ speed: 1.5, fontSize: 18, opacity: 0.12, count: 9 },
|
||||||
|
{ speed: 2.5, fontSize: 24, opacity: 0.2, count: 6 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const X_FREQ = (2 * Math.PI) / 120;
|
||||||
|
|
||||||
|
export const FloatingTools: React.FC = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
const wrapH = height + 100;
|
||||||
|
|
||||||
|
const [items] = useState(() => {
|
||||||
|
let toolIdx = 0;
|
||||||
|
return LAYERS.flatMap((layer, li) =>
|
||||||
|
Array.from({ length: layer.count }, (_, i) => {
|
||||||
|
const tool = TOOLS[toolIdx % TOOLS.length];
|
||||||
|
toolIdx++;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
layer,
|
||||||
|
x: random(`float-x-${li}-${i}`) * width,
|
||||||
|
startY: random(`float-y-${li}-${i}`) * wrapH,
|
||||||
|
phase: random(`float-phase-${li}-${i}`) * Math.PI * 2,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: COLOR.dark, overflow: "hidden" }}>
|
||||||
|
{items.map((item, idx) => {
|
||||||
|
const rawY = item.startY - frame * item.layer.speed;
|
||||||
|
const y = (((rawY % wrapH) + wrapH) % wrapH) - 50;
|
||||||
|
const xOffset = Math.sin(frame * X_FREQ + item.phase) * 3;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: item.x + xOffset,
|
||||||
|
top: y,
|
||||||
|
fontFamily: FONT.body,
|
||||||
|
fontSize: item.layer.fontSize,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: COLOR.category[item.tool.category] ?? COLOR.accent,
|
||||||
|
opacity: item.layer.opacity,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.tool.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<GrainOverlay opacity={0.02} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||||
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
|
import { COLOR } from "@/lib/colors";
|
||||||
|
import { FONT } from "@/lib/fonts";
|
||||||
|
|
||||||
|
const RINGS = [
|
||||||
|
{
|
||||||
|
formats: ["CR2", "NEF", "ARW", "RAF", "DNG"],
|
||||||
|
radius: 120,
|
||||||
|
color: "#f59e0b",
|
||||||
|
direction: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
formats: ["AVIF", "JXL", "HEIC", "WebP", "QOI"],
|
||||||
|
radius: 195,
|
||||||
|
color: "#14b8a6",
|
||||||
|
direction: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
formats: ["JPEG", "PNG", "TIFF", "BMP", "GIF", "PSD"],
|
||||||
|
radius: 270,
|
||||||
|
color: "#3b82f6",
|
||||||
|
direction: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DURATION = 180;
|
||||||
|
const SPEED = (2 * Math.PI) / DURATION;
|
||||||
|
|
||||||
|
const Badge: React.FC<{
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
angle: number;
|
||||||
|
radius: number;
|
||||||
|
}> = ({ label, color, angle, radius }) => {
|
||||||
|
const x = Math.cos(angle) * radius;
|
||||||
|
const y = Math.sin(angle) * radius * 0.55;
|
||||||
|
const z = Math.sin(angle);
|
||||||
|
const scale = interpolate(z, [-1, 1], [0.75, 1.0]);
|
||||||
|
const opacity = interpolate(z, [-1, 1], [0.4, 1.0]);
|
||||||
|
const zIndex = Math.round(interpolate(z, [-1, 1], [0, 10]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 400 + x,
|
||||||
|
top: 280 + y,
|
||||||
|
transform: `translate(-50%, -50%) scale(${scale})`,
|
||||||
|
opacity,
|
||||||
|
zIndex,
|
||||||
|
padding: "6px 14px",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: `${color}26`,
|
||||||
|
border: `1px solid ${color}66`,
|
||||||
|
boxShadow: `0 0 12px ${color}33`,
|
||||||
|
fontFamily: FONT.body,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
color,
|
||||||
|
whiteSpace: "nowrap" as const,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GhostBadge: React.FC<{
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
angle: number;
|
||||||
|
radius: number;
|
||||||
|
}> = ({ label, color, angle, radius }) => {
|
||||||
|
const x = Math.cos(angle) * radius;
|
||||||
|
const y = Math.sin(angle) * radius * 0.55;
|
||||||
|
const z = Math.sin(angle);
|
||||||
|
const scale = interpolate(z, [-1, 1], [0.75, 1.0]);
|
||||||
|
const zIndex = Math.round(interpolate(z, [-1, 1], [0, 10]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 400 + x,
|
||||||
|
top: 280 + y,
|
||||||
|
transform: `translate(-50%, -50%) scale(${scale})`,
|
||||||
|
opacity: 0.2,
|
||||||
|
zIndex,
|
||||||
|
padding: "6px 14px",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: `${color}26`,
|
||||||
|
border: `1px solid ${color}66`,
|
||||||
|
fontFamily: FONT.body,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
color,
|
||||||
|
whiteSpace: "nowrap" as const,
|
||||||
|
pointerEvents: "none" as const,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormatUniverse: React.FC = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const logoScale = Math.sin(frame * 0.06) * 0.03 + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: COLOR.dark, overflow: "hidden" }}>
|
||||||
|
{RINGS.map((ring) =>
|
||||||
|
ring.formats.map((fmt, fi) => {
|
||||||
|
const angleOffset = (fi / ring.formats.length) * Math.PI * 2;
|
||||||
|
const ghostAngle = angleOffset + (frame - 2) * SPEED * ring.direction;
|
||||||
|
return (
|
||||||
|
<GhostBadge
|
||||||
|
key={`ghost-${ring.radius}-${fmt}`}
|
||||||
|
label={fmt}
|
||||||
|
color={ring.color}
|
||||||
|
angle={ghostAngle}
|
||||||
|
radius={ring.radius}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{RINGS.map((ring) =>
|
||||||
|
ring.formats.map((fmt, fi) => {
|
||||||
|
const angleOffset = (fi / ring.formats.length) * Math.PI * 2;
|
||||||
|
const angle = angleOffset + frame * SPEED * ring.direction;
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={`badge-${ring.radius}-${fmt}`}
|
||||||
|
label={fmt}
|
||||||
|
color={ring.color}
|
||||||
|
angle={angle}
|
||||||
|
radius={ring.radius}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Img
|
||||||
|
src={staticFile("logo.png")}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
left: 400 - 40,
|
||||||
|
top: 280 - 40,
|
||||||
|
transform: `scale(${logoScale})`,
|
||||||
|
zIndex: 11,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 40,
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT.body,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 18,
|
||||||
|
color: "rgba(255, 255, 255, 0.7)",
|
||||||
|
zIndex: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
55+ formats. Every camera. Every device.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GrainOverlay opacity={0.03} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user