feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
Render engine - Add Remotion (code-based) as a 2nd render engine alongside After Effects. node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props, renders native then ffmpeg-scales to the quality tier (aspect-preserving). - content.projects.render_engine + render_remotion_comp (migration 32); render-svc claim resolves engine and routes (skips .aep for Remotion). - Admin TemplatesAdmin gains an engine picker + Remotion composition id field. Template pack (services/remotion) - 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in 3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro, Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown, GlitterReveal (editable logo image), NowruzGreeting (animated characters), and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D, Birthday3D, Promo3D) with reflections + bloom/DOF/vignette. - scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors. Pricing - Rewrite /pricing to the seconds-based model (charge = length x resolution), data-driven from /v1/plans, Toman, broker checkout. Coming-soon - Persian experimental-build overlay on all pages (launch date + countdown). Fixes - middleware matcher bypasses all static asset paths; catalog mapping passes cover image + preview video so real thumbnails render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
spring,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
import { ThreeCanvas } from "@remotion/three";
|
||||
import { Environment, Lightformer, MeshReflectorMaterial, RoundedBox } from "@react-three/drei";
|
||||
import { EffectComposer, Bloom, DepthOfField, Vignette } from "@react-three/postprocessing";
|
||||
import * as THREE from "three";
|
||||
import { z } from "zod";
|
||||
import { colorSchema } from "../lib/branding";
|
||||
import { FONT } from "../lib/fonts";
|
||||
import { useLayout } from "../lib/aspect";
|
||||
import { hexToRgba, rand } from "../lib/anim";
|
||||
|
||||
export const nowruz3DSchema = z.object({
|
||||
greeting: z.string(),
|
||||
subtitle: z.string(),
|
||||
message: z.string(),
|
||||
...colorSchema,
|
||||
});
|
||||
|
||||
type Props = z.infer<typeof nowruz3DSchema>;
|
||||
|
||||
const GOLD = "#f5c542";
|
||||
const RED = "#e23b3b";
|
||||
const SKIN = "#f0b486";
|
||||
const GREEN = "#4fb84f";
|
||||
|
||||
// ── Stylized 3D Haji Firuz (primitive-built, clay-render look) ────────────────
|
||||
const HajiFiruz3D: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const enter = spring({ frame: frame - 20, fps, config: { damping: 14, stiffness: 60 } });
|
||||
const bob = Math.abs(Math.sin(frame / 7)) * 0.12 * enter;
|
||||
const sway = Math.sin(frame / 7) * 0.08 * enter;
|
||||
const armSwing = Math.sin(frame / 3.2) * 0.5;
|
||||
const y = -0.5 + bob;
|
||||
|
||||
return (
|
||||
<group position={[0, y, 0]} rotation={[0, sway, 0]} scale={enter}>
|
||||
{/* tunic (tapered) */}
|
||||
<mesh position={[0, 0.55, 0]} castShadow>
|
||||
<cylinderGeometry args={[0.32, 0.6, 1.1, 32]} />
|
||||
<meshStandardMaterial color={RED} roughness={0.55} metalness={0.05} />
|
||||
</mesh>
|
||||
{/* gold hem + sash */}
|
||||
<mesh position={[0, 0.05, 0]}>
|
||||
<cylinderGeometry args={[0.6, 0.62, 0.12, 32]} />
|
||||
<meshStandardMaterial color={GOLD} roughness={0.25} metalness={0.85} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0.55, 0.0]}>
|
||||
<torusGeometry args={[0.42, 0.05, 12, 32]} />
|
||||
<meshStandardMaterial color={GOLD} roughness={0.25} metalness={0.85} />
|
||||
</mesh>
|
||||
{/* buttons */}
|
||||
{[0.75, 0.6, 0.45].map((by, i) => (
|
||||
<mesh key={i} position={[0, by, 0.46 - i * 0.02]}>
|
||||
<sphereGeometry args={[0.04, 16, 16]} />
|
||||
<meshStandardMaterial color={GOLD} metalness={0.9} roughness={0.2} />
|
||||
</mesh>
|
||||
))}
|
||||
{/* head */}
|
||||
<mesh position={[0, 1.32, 0]} castShadow>
|
||||
<sphereGeometry args={[0.33, 32, 32]} />
|
||||
<meshStandardMaterial color={SKIN} roughness={0.6} />
|
||||
</mesh>
|
||||
{/* eyes */}
|
||||
{[-0.12, 0.12].map((ex, i) => (
|
||||
<mesh key={i} position={[ex, 1.36, 0.3]}>
|
||||
<sphereGeometry args={[0.045, 16, 16]} />
|
||||
<meshStandardMaterial color="#2a2030" roughness={0.4} />
|
||||
</mesh>
|
||||
))}
|
||||
{/* smile */}
|
||||
<mesh position={[0, 1.24, 0.3]} rotation={[0, 0, 0]}>
|
||||
<torusGeometry args={[0.1, 0.018, 12, 24, Math.PI]} />
|
||||
<meshStandardMaterial color="#7a3a30" roughness={0.5} />
|
||||
</mesh>
|
||||
{/* hat (cone) + band + tip */}
|
||||
<mesh position={[0, 1.95, 0]} castShadow>
|
||||
<coneGeometry args={[0.34, 0.8, 32]} />
|
||||
<meshStandardMaterial color={RED} roughness={0.5} metalness={0.05} />
|
||||
</mesh>
|
||||
<mesh position={[0, 1.62, 0]}>
|
||||
<cylinderGeometry args={[0.35, 0.35, 0.1, 32]} />
|
||||
<meshStandardMaterial color={GOLD} roughness={0.25} metalness={0.85} />
|
||||
</mesh>
|
||||
<mesh position={[0, 2.36, 0]}>
|
||||
<sphereGeometry args={[0.07, 16, 16]} />
|
||||
<meshStandardMaterial color={GOLD} metalness={0.9} roughness={0.2} />
|
||||
</mesh>
|
||||
{/* right arm (down, swings) */}
|
||||
<group position={[0.45, 0.95, 0]} rotation={[0, 0, -0.5 + armSwing * 0.3]}>
|
||||
<mesh position={[0, -0.3, 0]} castShadow>
|
||||
<capsuleGeometry args={[0.1, 0.5, 8, 16]} />
|
||||
<meshStandardMaterial color={RED} roughness={0.55} />
|
||||
</mesh>
|
||||
<mesh position={[0, -0.62, 0]}>
|
||||
<sphereGeometry args={[0.12, 16, 16]} />
|
||||
<meshStandardMaterial color={SKIN} roughness={0.6} />
|
||||
</mesh>
|
||||
</group>
|
||||
{/* left arm raised with tambourine */}
|
||||
<group position={[-0.45, 1.0, 0.05]} rotation={[0, 0, 0.9 + armSwing * 0.4]}>
|
||||
<mesh position={[0, 0.28, 0]} castShadow>
|
||||
<capsuleGeometry args={[0.1, 0.5, 8, 16]} />
|
||||
<meshStandardMaterial color={RED} roughness={0.55} />
|
||||
</mesh>
|
||||
<group position={[0, 0.6, 0]} rotation={[Math.PI / 2, 0, armSwing]}>
|
||||
<mesh>
|
||||
<torusGeometry args={[0.26, 0.05, 16, 32]} />
|
||||
<meshStandardMaterial color={GOLD} metalness={0.85} roughness={0.25} />
|
||||
</mesh>
|
||||
<mesh>
|
||||
<circleGeometry args={[0.24, 32]} />
|
||||
<meshStandardMaterial color="#fff3d6" roughness={0.4} side={THREE.DoubleSide} transparent opacity={0.85} />
|
||||
</mesh>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<mesh key={i} position={[Math.cos((i / 8) * Math.PI * 2) * 0.26, Math.sin((i / 8) * Math.PI * 2) * 0.26, 0]}>
|
||||
<sphereGeometry args={[0.035, 12, 12]} />
|
||||
<meshStandardMaterial color={GOLD} metalness={0.9} roughness={0.2} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
</group>
|
||||
{/* legs */}
|
||||
{[-0.16, 0.16].map((lx, i) => (
|
||||
<mesh key={i} position={[lx, -0.2, 0]} castShadow>
|
||||
<capsuleGeometry args={[0.1, 0.3, 8, 16]} />
|
||||
<meshStandardMaterial color="#2a2f45" roughness={0.6} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Haft-Sin props ───────────────────────────────────────────────────────────
|
||||
const Candle: React.FC<{ x: number; z: number }> = ({ x, z }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const flick = 1 + Math.sin(frame / 4) * 0.12;
|
||||
return (
|
||||
<group position={[x, -0.5, z]}>
|
||||
<mesh castShadow>
|
||||
<cylinderGeometry args={[0.09, 0.1, 0.5, 24]} />
|
||||
<meshStandardMaterial color="#fbf0d8" roughness={0.6} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0.34, 0]} scale={[1, flick, 1]}>
|
||||
<coneGeometry args={[0.05, 0.18, 16]} />
|
||||
<meshStandardMaterial color="#ffd27a" emissive="#ffae3b" emissiveIntensity={4} toneMapped={false} />
|
||||
</mesh>
|
||||
<pointLight position={[0, 0.4, 0]} intensity={2.2 * flick} color="#ffb14d" distance={3} />
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const Egg: React.FC<{ x: number; z: number; color: string }> = ({ x, z, color }) => (
|
||||
<mesh position={[x, -0.42, z]} scale={[1, 1.3, 1]} castShadow>
|
||||
<sphereGeometry args={[0.14, 24, 24]} />
|
||||
<meshStandardMaterial color={color} roughness={0.35} metalness={0.1} />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
const FishBowl3D: React.FC<{ x: number; z: number }> = ({ x, z }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const fishX = Math.sin(frame / 20) * 0.12;
|
||||
return (
|
||||
<group position={[x, -0.25, z]}>
|
||||
{/* water */}
|
||||
<mesh position={[0, -0.02, 0]}>
|
||||
<sphereGeometry args={[0.27, 32, 32]} />
|
||||
<meshStandardMaterial color="#5bc8f0" roughness={0.1} metalness={0.2} transparent opacity={0.55} />
|
||||
</mesh>
|
||||
{/* fish */}
|
||||
<mesh position={[fishX, -0.02, 0]} scale={[0.13, 0.08, 0.05]}>
|
||||
<sphereGeometry args={[1, 16, 16]} />
|
||||
<meshStandardMaterial color="#ff5a2c" roughness={0.4} emissive="#ff4a1a" emissiveIntensity={0.2} />
|
||||
</mesh>
|
||||
{/* glass */}
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.32, 32, 32]} />
|
||||
<meshPhysicalMaterial color="#ffffff" roughness={0.05} metalness={0} transmission={0.0} transparent opacity={0.18} ior={1.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const Sabzeh3D: React.FC<{ x: number; z: number }> = ({ x, z }) => {
|
||||
const frame = useCurrentFrame();
|
||||
return (
|
||||
<group position={[x, -0.5, z]}>
|
||||
<mesh position={[0, 0.05, 0]} castShadow>
|
||||
<cylinderGeometry args={[0.26, 0.22, 0.18, 24]} />
|
||||
<meshStandardMaterial color="#caa06a" roughness={0.7} />
|
||||
</mesh>
|
||||
{Array.from({ length: 30 }).map((_, i) => {
|
||||
const a = rand(i) * Math.PI * 2;
|
||||
const r = rand(i + 5) * 0.22;
|
||||
const sway = Math.sin(frame / 18 + i) * 0.08;
|
||||
return (
|
||||
<mesh key={i} position={[Math.cos(a) * r, 0.28, Math.sin(a) * r]} rotation={[sway, 0, sway]}>
|
||||
<coneGeometry args={[0.012, 0.3 + rand(i + 2) * 0.2, 5]} />
|
||||
<meshStandardMaterial color={i % 2 ? GREEN : "#3da53d"} roughness={0.7} />
|
||||
</mesh>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const Petals3D: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
return (
|
||||
<group>
|
||||
{Array.from({ length: 30 }).map((_, i) => {
|
||||
const x = (rand(i) - 0.5) * 9;
|
||||
const z = (rand(i + 3) - 0.5) * 4 - 1;
|
||||
const fall = 4 - ((frame * (0.01 + rand(i) * 0.02) + rand(i + 7) * 6) % 7);
|
||||
const rot = frame * 0.03 * (1 + rand(i));
|
||||
return (
|
||||
<mesh key={i} position={[x + Math.sin(frame / 30 + i) * 0.4, fall, z]} rotation={[rot, rot * 0.7, rot * 0.3]}>
|
||||
<circleGeometry args={[0.06 + rand(i + 1) * 0.05, 8]} />
|
||||
<meshStandardMaterial color={["#ffd1e8", "#ffc1dd", "#fff0f6"][i % 3]} side={THREE.DoubleSide} roughness={0.6} />
|
||||
</mesh>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Scene ────────────────────────────────────────────────────────────────────
|
||||
const Scene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const orbit = Math.sin(frame / 110) * 0.22;
|
||||
return (
|
||||
<group rotation={[0, orbit, 0]}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[4, 8, 4]} intensity={2.4} color="#fff3e0" castShadow shadow-mapSize={[1024, 1024]} />
|
||||
<pointLight position={[-4, 2, 3]} intensity={20} color="#ff8a5c" />
|
||||
<pointLight position={[4, 1, -2]} intensity={16} color={GOLD} />
|
||||
|
||||
<Environment resolution={256}>
|
||||
<Lightformer intensity={2} position={[0, 4, -3]} scale={[10, 5, 1]} color="#fff0d8" />
|
||||
<Lightformer intensity={1.2} position={[-4, 2, 2]} scale={[4, 6, 1]} color="#ffb98a" />
|
||||
<Lightformer intensity={1.2} position={[4, 2, 2]} scale={[4, 6, 1]} color="#9ad8e8" />
|
||||
</Environment>
|
||||
|
||||
{/* reflective floor */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.62, 0]} receiveShadow>
|
||||
<planeGeometry args={[40, 40]} />
|
||||
<MeshReflectorMaterial
|
||||
blur={[300, 80]}
|
||||
resolution={1024}
|
||||
mixBlur={1}
|
||||
mixStrength={45}
|
||||
roughness={0.85}
|
||||
depthScale={1}
|
||||
minDepthThreshold={0.4}
|
||||
maxDepthThreshold={1.2}
|
||||
color="#2a2236"
|
||||
metalness={0.5}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
<HajiFiruz3D />
|
||||
<FishBowl3D x={1.5} z={0.3} />
|
||||
<Sabzeh3D x={-1.5} z={0.2} />
|
||||
<Candle x={0.9} z={0.9} />
|
||||
<Candle x={-0.9} z={0.9} />
|
||||
<Egg x={0.5} z={1.1} color={RED} />
|
||||
<Egg x={-0.4} z={1.15} color="#5bc8f0" />
|
||||
<Egg x={1.0} z={-0.4} color={GOLD} />
|
||||
<Petals3D />
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export const Nowruz3D: React.FC<Props> = ({
|
||||
greeting,
|
||||
subtitle,
|
||||
message,
|
||||
accentColor,
|
||||
secondaryColor,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
const L = useLayout();
|
||||
|
||||
const gSpring = spring({ frame: frame - 120, fps: 30, config: { damping: 13, stiffness: 90 } });
|
||||
const gScale = interpolate(gSpring, [0, 1], [0.6, 1]);
|
||||
const gOp = interpolate(frame, [120, 140], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const subOp = interpolate(frame, [142, 162], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const msgOp = interpolate(frame, [156, 176], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor }}>
|
||||
<AbsoluteFill style={{ background: `radial-gradient(circle at 50% 35%, ${hexToRgba(accentColor, 0.22)} 0%, ${hexToRgba(secondaryColor, 0.08)} 38%, ${backgroundColor} 72%)` }} />
|
||||
|
||||
<ThreeCanvas
|
||||
width={width}
|
||||
height={height}
|
||||
camera={{ position: [0, 2.0, 5.0], fov: 50 }}
|
||||
shadows
|
||||
style={{ position: "absolute", inset: 0 }}
|
||||
gl={{ toneMapping: THREE.ACESFilmicToneMapping, antialias: true }}
|
||||
>
|
||||
<Scene />
|
||||
<EffectComposer>
|
||||
<Bloom intensity={0.75} luminanceThreshold={0.62} luminanceSmoothing={0.3} mipmapBlur />
|
||||
<DepthOfField focusDistance={0.013} focalLength={0.045} bokehScale={3} />
|
||||
<Vignette eskil={false} offset={0.32} darkness={0.55} />
|
||||
</EffectComposer>
|
||||
</ThreeCanvas>
|
||||
|
||||
{/* Greeting overlay */}
|
||||
<AbsoluteFill style={{ fontFamily: FONT, direction: "rtl", alignItems: "center", justifyContent: "flex-start", paddingTop: height * 0.05 }}>
|
||||
<div style={{ transform: `scale(${gScale})`, opacity: gOp, fontWeight: 900, fontSize: L.vmin(96), color: textColor, textShadow: `0 ${L.vmin(3)}px ${L.vmin(6)}px ${hexToRgba("#1a0a00", 0.7)}, 0 0 ${L.vmin(30)}px ${hexToRgba(accentColor, 0.7)}` }}>
|
||||
{greeting}
|
||||
</div>
|
||||
<div style={{ marginTop: L.vmin(12), opacity: subOp, fontWeight: 700, fontSize: L.vmin(32), color: textColor, textShadow: `0 ${L.vmin(2)}px ${L.vmin(6)}px ${hexToRgba("#1a0a00", 0.8)}` }}>
|
||||
{subtitle}
|
||||
</div>
|
||||
<div style={{ marginTop: L.vmin(8), opacity: msgOp, fontWeight: 600, fontSize: L.vmin(26), color: hexToRgba(textColor, 0.9) }}>
|
||||
{message}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user