feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 20s

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:
soroush.asadi
2026-06-21 15:52:52 +03:30
parent b9b91397b0
commit 4f04f6bf75
137 changed files with 8942 additions and 135 deletions
@@ -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>
);
};