From 9a4be3115487eeadeb5035001c8b921980858b0c Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 17:15:28 +0800 Subject: [PATCH] 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. --- apps/videos/remotion.config.ts | 15 +- .../compositions/ambient/BrandGradient.tsx | 79 ++++++++ .../compositions/ambient/FloatingTools.tsx | 68 +++++++ .../compositions/features/FormatUniverse.tsx | 178 ++++++++++++++++++ 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 apps/videos/src/compositions/ambient/BrandGradient.tsx create mode 100644 apps/videos/src/compositions/ambient/FloatingTools.tsx create mode 100644 apps/videos/src/compositions/features/FormatUniverse.tsx diff --git a/apps/videos/remotion.config.ts b/apps/videos/remotion.config.ts index 0c4bc42c..f14d9396 100644 --- a/apps/videos/remotion.config.ts +++ b/apps/videos/remotion.config.ts @@ -1,6 +1,19 @@ +import path from "node:path"; import { Config } from "@remotion/cli/config"; import { enableTailwind } from "@remotion/tailwind"; Config.setVideoImageFormat("jpeg"); 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"), + }, + }, + }; +}); diff --git a/apps/videos/src/compositions/ambient/BrandGradient.tsx b/apps/videos/src/compositions/ambient/BrandGradient.tsx new file mode 100644 index 00000000..1928d628 --- /dev/null +++ b/apps/videos/src/compositions/ambient/BrandGradient.tsx @@ -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 = () => ( + + {BLOBS.map((blob) => ( + + ))} + + +); diff --git a/apps/videos/src/compositions/ambient/FloatingTools.tsx b/apps/videos/src/compositions/ambient/FloatingTools.tsx new file mode 100644 index 00000000..60db8607 --- /dev/null +++ b/apps/videos/src/compositions/ambient/FloatingTools.tsx @@ -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 ( + + {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 ( + + {item.tool.name} + + ); + })} + + + ); +}; diff --git a/apps/videos/src/compositions/features/FormatUniverse.tsx b/apps/videos/src/compositions/features/FormatUniverse.tsx new file mode 100644 index 00000000..fd50cd30 --- /dev/null +++ b/apps/videos/src/compositions/features/FormatUniverse.tsx @@ -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 ( +
+ {label} +
+ ); +}; + +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 ( +
+ {label} +
+ ); +}; + +export const FormatUniverse: React.FC = () => { + const frame = useCurrentFrame(); + const logoScale = Math.sin(frame * 0.06) * 0.03 + 1; + + return ( + + {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 ( + + ); + }), + )} + + {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 ( + + ); + }), + )} + + + +
+ 55+ formats. Every camera. Every device. +
+ + +
+ ); +};