feat(remotion): +LogoMotion3D template (Tech/3D cinematic logo reveal)
First template built through the new flow (brief → quality-gate approval → build → seed → deploy). Tech/3D logo motion: a 3D metallic card + radial light rays + lens flare + bloom (genuine @remotion/three), with the user's uploaded logo composited on the card as a reliable HTML <Img> (renders any SVG/PNG/data-URI; static camera keeps it aligned), brand text + tagline, grain. Falls back to a branded play-mark when no logo is set. Re-flows across 16:9/1:1/9:16. - LogoMotion3D.tsx registered per aspect in Root.tsx. - Seeded as fr-logo-motion-3d: text fields (brandText, tagline) + a logoUrl image upload field + the dark-tech palette (light text) + per-aspect previews. - 3 thumbnails + 3 preview MP4s rendered, deployed; detail page + assets serve 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { Three3DTest } from "./compositions/Three3DTest";
|
||||
import { AssetSheet } from "./compositions/AssetSheet";
|
||||
import { StoryScenes, STORY_SCENES_DURATION } from "./compositions/StoryScenes";
|
||||
import { FlexStory, flexStorySchema, flexStoryDefaults, calcFlexStoryMetadata } from "./compositions/FlexStory";
|
||||
import { LogoMotion3D, logoMotion3DSchema, logoMotion3DDefaults } from "./compositions/LogoMotion3D";
|
||||
import { CHARACTER_JOURNEY } from "./scenes/presets";
|
||||
import {
|
||||
IlluminatedCircles,
|
||||
@@ -107,6 +108,21 @@ export const RemotionRoot: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Tech/3D logo motion — quality-preview composition */}
|
||||
{ASPECTS.map((a) => (
|
||||
<Composition
|
||||
key={`LogoMotion3D-${a.id}`}
|
||||
id={`LogoMotion3D-${a.id}`}
|
||||
component={LogoMotion3D}
|
||||
durationInFrames={5 * FPS}
|
||||
fps={FPS}
|
||||
width={a.width}
|
||||
height={a.height}
|
||||
schema={logoMotion3DSchema}
|
||||
defaultProps={logoMotion3DDefaults}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 3D feasibility test */}
|
||||
<Composition
|
||||
id="Three3DTest"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import React from "react";
|
||||
import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { z } from "zod";
|
||||
import { ThreeCanvas } from "@remotion/three";
|
||||
import { Environment, Lightformer } from "@react-three/drei";
|
||||
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
|
||||
import { AdditiveBlending } from "three";
|
||||
import { colorSchema } from "../lib/branding";
|
||||
import { FONT } from "../lib/fonts";
|
||||
import { useLayout } from "../lib/aspect";
|
||||
import { hexToRgba, rand } from "../lib/anim";
|
||||
|
||||
// ── Tech/3D logo reveal: a 3D metallic card + radial light rays + lens flare +
|
||||
// bloom (genuine @remotion/three), with the user's logo composited on the card as
|
||||
// a reliable HTML <Img> (renders any SVG/PNG/data-URI). Static camera keeps the
|
||||
// flat logo aligned to the card.
|
||||
export const logoMotion3DSchema = z.object({
|
||||
logoUrl: z.string(), // uploaded logo (SVG/PNG/data-URI); "" → branded placeholder mark
|
||||
brandText: z.string(),
|
||||
tagline: z.string(),
|
||||
...colorSchema,
|
||||
});
|
||||
type Props = z.infer<typeof logoMotion3DSchema>;
|
||||
|
||||
export const logoMotion3DDefaults: Props = {
|
||||
logoUrl: "",
|
||||
brandText: "فلترندر",
|
||||
tagline: "موشن، ساده و حرفهای",
|
||||
accentColor: "#38bdf8",
|
||||
secondaryColor: "#818cf8",
|
||||
backgroundColor: "#0a0e1a",
|
||||
textColor: "#f8fafc",
|
||||
};
|
||||
|
||||
const resolveSrc = (u: string) => (/^https?:\/\//.test(u) || u.startsWith("data:") ? u : staticFile(u));
|
||||
|
||||
const Rays: React.FC<{ frame: number; color: string; op: number }> = ({ frame, color, op }) => {
|
||||
const n = 16;
|
||||
const pulse = 1 + Math.sin(frame / 11) * 0.08;
|
||||
return (
|
||||
<group rotation={[0, 0, frame * 0.004]} position={[0, 0, -1.4]}>
|
||||
{Array.from({ length: n }).map((_, i) => (
|
||||
<mesh key={i} rotation={[0, 0, (i / n) * Math.PI * 2]}>
|
||||
<planeGeometry args={[0.05 + (i % 2) * 0.03, 7]} />
|
||||
<meshBasicMaterial color={color} transparent opacity={0.1 * pulse * op} blending={AdditiveBlending} depthWrite={false} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const Flare: React.FC<{ core: number; streak: number; accent: string }> = ({ core, streak, accent }) => (
|
||||
<group position={[0, 0, -0.6]}>
|
||||
<mesh><circleGeometry args={[0.55, 40]} /><meshBasicMaterial color="#ffffff" transparent opacity={core} blending={AdditiveBlending} depthWrite={false} /></mesh>
|
||||
<mesh><circleGeometry args={[1.1, 40]} /><meshBasicMaterial color={accent} transparent opacity={core * 0.4} blending={AdditiveBlending} depthWrite={false} /></mesh>
|
||||
<mesh><planeGeometry args={[9, 0.1]} /><meshBasicMaterial color="#ffffff" transparent opacity={streak} blending={AdditiveBlending} depthWrite={false} /></mesh>
|
||||
<mesh><planeGeometry args={[0.1, 5]} /><meshBasicMaterial color={accent} transparent opacity={streak * 0.7} blending={AdditiveBlending} depthWrite={false} /></mesh>
|
||||
</group>
|
||||
);
|
||||
|
||||
const Card: React.FC<{ scale: number; accent: string }> = ({ scale, accent }) => (
|
||||
<group scale={scale}>
|
||||
<mesh>
|
||||
<boxGeometry args={[1.7, 1.7, 0.2]} />
|
||||
<meshStandardMaterial color={accent} metalness={1} roughness={0.16} envMapIntensity={2} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
|
||||
export const LogoMotion3D: React.FC<Props> = (props) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
const L = useLayout();
|
||||
const accent = props.accentColor;
|
||||
|
||||
const popIn = spring({ frame: frame - 6, fps, config: { damping: 11, stiffness: 130 } });
|
||||
const scale = interpolate(popIn, [0, 1], [0.2, 1]);
|
||||
const raysOp = interpolate(frame, [4, 24], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const core = interpolate(frame, [8, 20, 40], [0, 1, 0.32], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const streak = interpolate(frame, [10, 22, 44], [0, 0.9, 0.18], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const brandOp = interpolate(frame, [30, 48], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const brandY = interpolate(popIn, [0, 1], [L.vmin(28), 0]);
|
||||
const tagOp = interpolate(frame, [46, 64], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
|
||||
// The 3D card (1.7 units) projects to ≈43% of the frame height — match the HTML
|
||||
// logo to it so it sits exactly on the card.
|
||||
const cardPx = height * 0.43;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: props.backgroundColor, fontFamily: FONT }}>
|
||||
<ThreeCanvas width={width} height={height} camera={{ position: [0, 0, 5.4], fov: 40 }} style={{ position: "absolute", inset: 0 }}>
|
||||
<ambientLight intensity={0.3} />
|
||||
<pointLight position={[4, 3, 4]} intensity={40} color="#ffffff" />
|
||||
<pointLight position={[-4, -1, 2]} intensity={24} color={props.secondaryColor} />
|
||||
<Environment resolution={128}>
|
||||
<Lightformer intensity={2.2} position={[0, 3, 2]} scale={[7, 3, 1]} color={accent} />
|
||||
<Lightformer intensity={1.6} position={[-4, 1, 2]} scale={[3, 5, 1]} color="#ffffff" />
|
||||
<Lightformer intensity={1.6} position={[4, 1, 2]} scale={[3, 5, 1]} color={props.secondaryColor} />
|
||||
</Environment>
|
||||
<Rays frame={frame} color={accent} op={raysOp} />
|
||||
<Flare core={core} streak={streak} accent={accent} />
|
||||
<Card scale={scale} accent={accent} />
|
||||
{Array.from({ length: 14 }).map((_, i) => {
|
||||
const x = (rand(i) - 0.5) * 9;
|
||||
const y = (rand(i + 3) - 0.5) * 5 + Math.sin((frame + i * 20) / 36) * 0.2;
|
||||
const z = -1 - rand(i + 6) * 1.6;
|
||||
return <mesh key={i} position={[x, y, z]}><circleGeometry args={[0.02 + rand(i) * 0.04, 12]} /><meshBasicMaterial color={accent} transparent opacity={0.5} blending={AdditiveBlending} depthWrite={false} /></mesh>;
|
||||
})}
|
||||
<EffectComposer>
|
||||
<Bloom intensity={1.5} luminanceThreshold={0.35} luminanceSmoothing={0.4} mipmapBlur />
|
||||
<Vignette eskil={false} offset={0.3} darkness={0.72} />
|
||||
</EffectComposer>
|
||||
</ThreeCanvas>
|
||||
|
||||
{/* Logo on the card (centered, scales in with the card). Any format renders. */}
|
||||
<AbsoluteFill style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<div style={{ width: cardPx, height: cardPx, transform: `scale(${scale})`, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
{props.logoUrl ? (
|
||||
<Img src={resolveSrc(props.logoUrl)} style={{ width: "74%", height: "74%", objectFit: "contain", filter: `drop-shadow(0 0 ${L.vmin(20)}px ${hexToRgba(accent, 0.45)})` }} />
|
||||
) : (
|
||||
<div style={{ width: 0, height: 0, borderTop: `${cardPx * 0.18}px solid transparent`, borderBottom: `${cardPx * 0.18}px solid transparent`, borderLeft: `${cardPx * 0.3}px solid #ffffff`, marginLeft: cardPx * 0.06, filter: `drop-shadow(0 0 ${L.vmin(24)}px ${hexToRgba("#ffffff", 0.6)})` }} />
|
||||
)}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
|
||||
{/* Brand + tagline below the card */}
|
||||
<AbsoluteFill style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-end", paddingBottom: L.pick(height * 0.1, height * 0.08, height * 0.16) }}>
|
||||
<div style={{ direction: "rtl", opacity: brandOp, transform: `translateY(${brandY}px)`, fontWeight: 900, fontSize: L.pick(L.vmin(80), L.vmin(74), L.vmin(70)), color: props.textColor, letterSpacing: -1, textShadow: `0 0 ${L.vmin(40)}px ${hexToRgba(accent, 0.5)}` }}>
|
||||
{props.brandText}
|
||||
</div>
|
||||
<div style={{ direction: "rtl", opacity: tagOp, marginTop: L.vmin(12), fontWeight: 400, fontSize: L.pick(L.vmin(31), L.vmin(30), L.vmin(28)), color: hexToRgba(props.textColor, 0.62), letterSpacing: 2 }}>
|
||||
{props.tagline}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
|
||||
<AbsoluteFill style={{ pointerEvents: "none", opacity: 0.06, mixBlendMode: "overlay", backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")", backgroundSize: "160px 160px" }} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user