feat(remotion): Instagram channel-promo template + taste system + design-quality kit
The reference-round workflow, run end to end for a real template: Taste system (how we learn the user's taste, persisted): - references/TASTE_PROFILE.md (living design contract) + references/README.md (the daily loop) + a "reference round" stage in docs/TEMPLATE_BRIEF.md (provide refs or I suggest+mock directions). Design-quality before/after: - HeroDemo — the fix recipe vs the faint default: layered-depth background, a proper big video type scale, and a bold composed focal object. (Backgrounds were naked, text too small, scenes had no objects.) - YaldaSofreh3D + IGPromoDirections + IGProfileMock — reference-match proofs (low-poly 3D, 3 IG-promo style directions, the realistic IG-light page). Instagram channel-promo template (the deliverable — a flexible 5-scene FlexStory): - igkit + 5 blocks: IGIntro, IGProfile (realistic IG-light profile, scales to all aspects), IGFeed (post grid), IGStats (animated count-up), IGFollowCTA (Follow taps to "Following"). - FlexStory gains a `finish` toggle so the IG-light scenes render clean (no brand grade). INSTAGRAM_PROMO preset + 3 aspect comps in Root. Verified: a still of every scene at 9:16 renders clean; full preview MP4 rendering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ export const flexStorySchema = z.object({
|
||||
music: z.string().optional(), // path/url of the music bed; "" = silent
|
||||
musicVolume: z.number().optional(),
|
||||
sfx: z.boolean().optional(), // transition whoosh + outro chime
|
||||
finish: z.boolean().optional(), // cinematic grade + FinishPass (default on; off = clean/light)
|
||||
...colorSchema,
|
||||
});
|
||||
type Props = z.infer<typeof flexStorySchema>;
|
||||
@@ -67,6 +68,7 @@ export const FlexStory: React.FC<Props> = (props) => {
|
||||
const music = props.music === undefined ? "audio/music-ambient.mp3" : props.music;
|
||||
const musicVolume = props.musicVolume ?? 0.6;
|
||||
const sfx = props.sfx ?? true;
|
||||
const finish = props.finish ?? true;
|
||||
|
||||
// Precompute each scene's start frame + duration (shared by visuals + SFX).
|
||||
const starts: number[] = [];
|
||||
@@ -79,7 +81,7 @@ export const FlexStory: React.FC<Props> = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: colors.backgroundColor, fontFamily: FONT, filter: GRADE_FILTER }}>
|
||||
<AbsoluteFill style={{ backgroundColor: colors.backgroundColor, fontFamily: FONT, filter: finish ? GRADE_FILTER : "saturate(1.02)" }}>
|
||||
{music ? <Audio src={resolveAudio(music)} loop volume={musicVolume} /> : null}
|
||||
|
||||
{scenes.map((sc, i) => {
|
||||
@@ -103,8 +105,8 @@ export const FlexStory: React.FC<Props> = (props) => {
|
||||
))
|
||||
: null}
|
||||
|
||||
{/* Cinematic finish over every scene — the shared quality floor. */}
|
||||
<FinishPass colors={colors} />
|
||||
{/* Cinematic finish over every scene — the shared quality floor (off = clean/light). */}
|
||||
{finish ? <FinishPass colors={colors} /> : null}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { useLayout } from "../lib/aspect";
|
||||
import { FONT } from "../lib/fonts";
|
||||
import { hexToRgba, mixHex } from "../lib/anim";
|
||||
import { Grain, Vignette } from "../scenes/chrome";
|
||||
|
||||
/**
|
||||
* HeroDemo — the "after" for the design-quality fix: a DESIGNED hero, not text on
|
||||
* a void. Three deliberate fixes vs the calm default:
|
||||
* 1. Background = layered depth (gradient + big soft glows + a bold patterned panel)
|
||||
* with real atmosphere, instead of 2 ghost blobs.
|
||||
* 2. Type = a proper video scale — title huge, subtitle big + high-contrast.
|
||||
* 3. A bold composed focal OBJECT (the play disc + orbit + mini cards), thick-outline
|
||||
* saturated, arranged by thirds — so the frame reads as a composition.
|
||||
*/
|
||||
const C = { kicker: "فلترندر", title: "ویدیوهای حرفهای بسازید", subtitle: "قالبهای آمادهٔ فارسی، رندر ابری در چند دقیقه" };
|
||||
const COL = { accent: "#ff5a3c", secondary: "#7c5cff", bg: "#11132a", text: "#ffffff" };
|
||||
|
||||
export const HeroDemo: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const L = useLayout();
|
||||
const ink = mixHex(COL.bg, "#000010", 0.55);
|
||||
const stroke = mixHex(COL.bg, "#05060f", 0.6);
|
||||
|
||||
const titleSp = spring({ frame: frame - 4, fps, config: { damping: 18, stiffness: 110 } });
|
||||
const titleY = interpolate(titleSp, [0, 1], [L.vmin(50), 0]);
|
||||
const subOp = interpolate(frame, [16, 34], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const objSp = spring({ frame: frame - 2, fps, config: { damping: 14, stiffness: 90 } });
|
||||
const float = Math.sin(frame / 26) * L.vmin(10);
|
||||
|
||||
// RTL editorial split: bold object on the LEFT, right-aligned text on the RIGHT.
|
||||
const objSize = L.pick(L.vmin(470), L.vmin(620), L.vmin(680));
|
||||
const objLeft = L.pick(L.vmin(120), L.vmin(190), L.vmin(150));
|
||||
const objTop = L.pick(L.vmin(305), L.vmin(150), L.vmin(1100));
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ fontFamily: FONT, background: `linear-gradient(150deg, ${mixHex(COL.bg, COL.secondary, 0.12)} 0%, ${ink} 60%, ${mixHex(COL.bg, "#000010", 0.7)} 100%)` }}>
|
||||
{/* 1 — background depth: big soft glows + a faint bold dot-grid panel */}
|
||||
<div style={{ position: "absolute", left: "-12%", top: "-18%", width: "55%", height: "70%", borderRadius: "50%", background: hexToRgba(COL.accent, 0.5), filter: `blur(${L.vmin(150)}px)` }} />
|
||||
<div style={{ position: "absolute", right: "-10%", bottom: "-22%", width: "55%", height: "75%", borderRadius: "50%", background: hexToRgba(COL.secondary, 0.45), filter: `blur(${L.vmin(160)}px)` }} />
|
||||
<AbsoluteFill style={{ opacity: 0.12, backgroundImage: `radial-gradient(${hexToRgba(COL.text, 0.9)} ${L.vmin(2.4)}px, transparent ${L.vmin(2.6)}px)`, backgroundSize: `${L.vmin(46)}px ${L.vmin(46)}px`, maskImage: "radial-gradient(80% 80% at 30% 60%, black, transparent 75%)", WebkitMaskImage: "radial-gradient(80% 80% at 30% 60%, black, transparent 75%)" }} />
|
||||
|
||||
{/* 2 — bold composed focal object: glow + orbit + play disc + two mini template cards */}
|
||||
<div style={{ position: "absolute", left: objLeft, top: objTop + float, width: objSize, height: objSize, transform: `scale(${interpolate(objSp, [0, 1], [0.7, 1])})` }}>
|
||||
<div style={{ position: "absolute", inset: "8%", borderRadius: "50%", background: hexToRgba(COL.accent, 0.5), filter: `blur(${L.vmin(70)}px)` }} />
|
||||
<svg width={objSize} height={objSize} viewBox="0 0 100 100" style={{ position: "absolute", inset: 0, filter: `drop-shadow(0 ${L.vmin(24)}px ${L.vmin(40)}px ${hexToRgba("#000010", 0.5)})` }}>
|
||||
<circle cx="50" cy="50" r="40" fill="none" stroke={hexToRgba(COL.text, 0.22)} strokeWidth="1.4" strokeDasharray="5 5" />
|
||||
<circle cx="50" cy="50" r="31" fill={COL.accent} stroke={stroke} strokeWidth="2.4" />
|
||||
<circle cx="50" cy="50" r="31" fill="none" stroke={hexToRgba(COL.text, 0.5)} strokeWidth="1" transform="translate(-1.2,-1.6)" />
|
||||
<path d="M43 38 L65 50 L43 62 Z" fill={COL.text} stroke={stroke} strokeWidth="2.2" strokeLinejoin="round" />
|
||||
<circle cx="86" cy="34" r="3.4" fill={COL.secondary} stroke={stroke} strokeWidth="1.6" />
|
||||
<g transform="rotate(15 16 30)"><path d="M14 26 l2.4 5 5 2.4 -5 2.4 -2.4 5 -2.4 -5 -5 -2.4 5 -2.4 z" fill={COL.text} stroke={stroke} strokeWidth="1.4" strokeLinejoin="round" /></g>
|
||||
</svg>
|
||||
{/* mini "template" cards floating on the disc */}
|
||||
<div style={{ position: "absolute", right: "-4%", top: "8%", width: "34%", height: "26%", background: COL.secondary, borderRadius: L.vmin(16), border: `${L.vmin(5)}px solid ${stroke}`, boxShadow: `0 ${L.vmin(16)}px ${L.vmin(30)}px ${hexToRgba("#000010", 0.45)}`, transform: "rotate(8deg)", padding: L.vmin(14) }}>
|
||||
<div style={{ width: "70%", height: L.vmin(10), background: hexToRgba(COL.text, 0.85), borderRadius: 999 }} />
|
||||
<div style={{ width: "45%", height: L.vmin(10), background: hexToRgba(COL.text, 0.5), borderRadius: 999, marginTop: L.vmin(10) }} />
|
||||
</div>
|
||||
<div style={{ position: "absolute", left: "-2%", bottom: "6%", width: "30%", height: "23%", background: mixHex(COL.accent, "#ffffff", 0.15), borderRadius: L.vmin(16), border: `${L.vmin(5)}px solid ${stroke}`, boxShadow: `0 ${L.vmin(16)}px ${L.vmin(30)}px ${hexToRgba("#000010", 0.45)}`, transform: "rotate(-10deg)" }} />
|
||||
</div>
|
||||
|
||||
{/* 3 — type: right-aligned RTL editorial block, big + high-contrast */}
|
||||
<AbsoluteFill style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", justifyContent: "center", textAlign: "right", direction: "rtl", padding: L.pick(`0 ${L.vmin(110)}px 0 42%`, `${L.vmin(120)}px ${L.vmin(80)}px`, `0 ${L.vmin(70)}px ${L.vmin(140)}px`) }}>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: L.vmin(12), background: COL.accent, color: "#fff", fontWeight: 800, fontSize: L.vmin(30), padding: `${L.vmin(12)}px ${L.vmin(26)}px`, borderRadius: 999, border: `${L.vmin(4)}px solid ${stroke}`, boxShadow: `0 ${L.vmin(10)}px ${L.vmin(22)}px ${hexToRgba(COL.accent, 0.4)}` }}>
|
||||
<span style={{ width: L.vmin(12), height: L.vmin(12), borderRadius: 999, background: "#fff" }} />
|
||||
{C.kicker}
|
||||
</div>
|
||||
<div style={{ marginTop: L.vmin(30), fontWeight: 900, fontSize: L.pick(L.vmin(146), L.vmin(150), L.vmin(132)), lineHeight: 1.0, letterSpacing: -2, color: COL.text, transform: `translateY(${titleY}px)`, textShadow: `0 ${L.vmin(8)}px ${L.vmin(30)}px ${hexToRgba("#000010", 0.45)}`, maxWidth: L.pick(L.vmin(1000), L.vmin(900), L.vmin(960)) }}>
|
||||
ویدیوهای <span style={{ color: COL.accent }}>حرفهای</span> بسازید
|
||||
</div>
|
||||
<div style={{ marginTop: L.vmin(28), fontWeight: 500, fontSize: L.pick(L.vmin(50), L.vmin(48), L.vmin(46)), lineHeight: 1.5, color: hexToRgba(COL.text, 0.9), opacity: subOp, maxWidth: L.vmin(1050) }}>
|
||||
{C.subtitle}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
|
||||
<Vignette />
|
||||
<Grain />
|
||||
<AbsoluteFill style={{ pointerEvents: "none", filter: "contrast(1.06) saturate(1.12)" }} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from "react";
|
||||
import { AbsoluteFill } from "remotion";
|
||||
import { FONT } from "../lib/fonts";
|
||||
import { hexToRgba } from "../lib/anim";
|
||||
|
||||
/**
|
||||
* IGProfileMock — an authentic Instagram profile page (LIGHT theme) inside a phone,
|
||||
* as the centrepiece of a "follow our page" promo. Real IG chrome: status bar, the
|
||||
* username header, avatar + stats, bio, Follow/Message buttons, story highlights,
|
||||
* the grid tabs and the posts grid. Everything here is an editable field later.
|
||||
*/
|
||||
const IGLOGO = "linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888)";
|
||||
const BLUE = "#0095f6";
|
||||
const HANDLE = "flat.studio";
|
||||
const NAME = "استودیو فلت";
|
||||
const CAT = "هنر و طراحی";
|
||||
const BIO = ["هر روز یک طرح تازه ✨", "آموزش، قالب و الهام برای طراحان", "سفارش و دانلود 👇"];
|
||||
const LINK = "flat.studio/shop";
|
||||
const HILITES = ["جدید", "قالبها", "آموزش", "نمونهکار"];
|
||||
const POSTS = ["#ff5a3c", "#7c5cff", "#16b5a0", "#ffb23c", "#ef5da8", "#3aa0ff", "#ff7a59", "#4cd4b0", "#a06bff", "#ff5a3c", "#3aa0ff", "#ffb23c"];
|
||||
|
||||
const IgCamera: React.FC<{ s: number }> = ({ s }) => (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="url(#ig)" strokeWidth="2">
|
||||
<defs><linearGradient id="ig" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stopColor="#f09433" /><stop offset="0.5" stopColor="#dc2743" /><stop offset="1" stopColor="#bc1888" /></linearGradient></defs>
|
||||
<rect x="2" y="2" width="20" height="20" rx="6" /><circle cx="12" cy="12" r="5" /><circle cx="17.5" cy="6.5" r="1.2" fill="url(#ig)" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Stat: React.FC<{ n: string; l: string }> = ({ n, l }) => (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div style={{ fontWeight: 800, fontSize: 38, color: "#000" }}>{n}</div>
|
||||
<div style={{ fontWeight: 400, fontSize: 27, color: "#262626" }}>{l}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IGProfileMock: React.FC = () => {
|
||||
const S = 770; // phone screen inner width
|
||||
const cell = (S - 12) / 3;
|
||||
return (
|
||||
<AbsoluteFill style={{ fontFamily: FONT, background: "linear-gradient(165deg,#fdfcfb,#f3edf5)" }}>
|
||||
<div style={{ position: "absolute", left: "-12%", top: "-6%", width: "55%", height: "34%", borderRadius: "50%", background: hexToRgba("#dc2743", 0.16), filter: "blur(150px)" }} />
|
||||
<div style={{ position: "absolute", right: "-14%", bottom: "2%", width: "58%", height: "36%", borderRadius: "50%", background: hexToRgba("#7c5cff", 0.16), filter: "blur(160px)" }} />
|
||||
|
||||
{/* promo header: IG logo + headline */}
|
||||
<div style={{ position: "absolute", top: 96, left: 0, right: 0, display: "flex", flexDirection: "column", alignItems: "center", gap: 18 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<IgCamera s={64} />
|
||||
<div style={{ fontWeight: 800, fontSize: 52, background: IGLOGO, WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent" }}>Instagram</div>
|
||||
</div>
|
||||
<div style={{ direction: "rtl", fontWeight: 900, fontSize: 64, color: "#15151a" }}>صفحهٔ ما را دنبال کنید</div>
|
||||
</div>
|
||||
|
||||
{/* phone */}
|
||||
<div style={{ position: "absolute", left: "50%", top: 300, transform: "translateX(-50%)", width: S + 36, borderRadius: 64, background: "#0c0c0f", padding: 18, boxShadow: "0 50px 100px rgba(20,12,30,0.4)" }}>
|
||||
<div style={{ width: S, borderRadius: 48, background: "#fff", overflow: "hidden" }}>
|
||||
{/* status bar */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px 34px 6px", fontSize: 26, fontWeight: 700, color: "#000" }}>
|
||||
<span>۹:۴۱</span><span style={{ display: "flex", gap: 10, alignItems: "center", fontSize: 24 }}>▮▮▮ <span style={{ fontSize: 22 }}>WiFi</span> <span style={{ border: "2px solid #000", borderRadius: 5, padding: "2px 6px", fontSize: 18 }}>۸۴٪</span></span>
|
||||
</div>
|
||||
{/* username header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 28px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, direction: "ltr" }}>
|
||||
<span style={{ fontSize: 26 }}>🔒</span><span style={{ fontWeight: 800, fontSize: 36, color: "#000" }}>{HANDLE}</span><span style={{ fontSize: 24, color: "#000" }}>▾</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 26, fontSize: 42, color: "#000" }}><span>+</span><span>☰</span></div>
|
||||
</div>
|
||||
{/* profile: avatar + stats */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 30, padding: "12px 34px" }}>
|
||||
<div style={{ width: 176, height: 176, borderRadius: "50%", background: IGLOGO, padding: 6 }}>
|
||||
<div style={{ width: "100%", height: "100%", borderRadius: "50%", background: "#eee", border: "5px solid #fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 78 }}>🎨</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: "flex", justifyContent: "space-around" }}>
|
||||
<Stat n="۳۲۰" l="پست" /><Stat n="۲۴٫۸ هزار" l="دنبالکننده" /><Stat n="۱۸۰" l="دنبالشده" />
|
||||
</div>
|
||||
</div>
|
||||
{/* name + bio */}
|
||||
<div style={{ direction: "rtl", padding: "6px 34px 0", color: "#000" }}>
|
||||
<div style={{ fontWeight: 800, fontSize: 32 }}>{NAME}</div>
|
||||
<div style={{ fontSize: 27, color: "#737373", marginTop: 2 }}>{CAT}</div>
|
||||
{BIO.map((b, i) => <div key={i} style={{ fontSize: 28, marginTop: 4 }}>{b}</div>)}
|
||||
<div style={{ fontSize: 28, color: "#00376b", fontWeight: 600, marginTop: 4, direction: "ltr", textAlign: "right" }}>{LINK}</div>
|
||||
</div>
|
||||
{/* buttons */}
|
||||
<div style={{ display: "flex", gap: 12, padding: "20px 34px 8px" }}>
|
||||
<div style={{ flex: 1, height: 76, borderRadius: 12, background: BLUE, color: "#fff", fontWeight: 800, fontSize: 30, display: "flex", alignItems: "center", justifyContent: "center" }}>دنبال کردن</div>
|
||||
<div style={{ flex: 1, height: 76, borderRadius: 12, background: "#efefef", color: "#000", fontWeight: 700, fontSize: 30, display: "flex", alignItems: "center", justifyContent: "center" }}>پیام</div>
|
||||
<div style={{ width: 76, height: 76, borderRadius: 12, background: "#efefef", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 34 }}>👤</div>
|
||||
</div>
|
||||
{/* highlights */}
|
||||
<div style={{ display: "flex", gap: 28, padding: "16px 34px", direction: "rtl" }}>
|
||||
{HILITES.map((h, i) => (
|
||||
<div key={i} style={{ textAlign: "center" }}>
|
||||
<div style={{ width: 120, height: 120, borderRadius: "50%", border: "2px solid #dbdbdb", background: i % 2 ? "#f3e9df" : "#e9eef5", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 46 }}>{["✨", "🗂️", "🎓", "🖼️"][i]}</div>
|
||||
<div style={{ fontSize: 24, color: "#000", marginTop: 8 }}>{h}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* tabs */}
|
||||
<div style={{ display: "flex", borderTop: "1px solid #dbdbdb", marginTop: 6 }}>
|
||||
{["▦", "▶", "𓏬"].map((t, i) => (
|
||||
<div key={i} style={{ flex: 1, textAlign: "center", padding: "18px 0", fontSize: 38, color: i === 0 ? "#000" : "#b3b3b3", borderTop: i === 0 ? "2px solid #000" : "none", marginTop: -1 }}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
{/* posts grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: `repeat(3, ${cell}px)`, gap: 6 }}>
|
||||
{POSTS.map((c, i) => (
|
||||
<div key={i} style={{ width: cell, height: cell, background: c, display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
|
||||
<div style={{ width: cell * 0.36, height: cell * 0.36, borderRadius: 12, background: hexToRgba("#fff", 0.4) }} />
|
||||
{i % 4 === 0 ? <span style={{ position: "absolute", top: 10, right: 12, color: "#fff", fontSize: 30 }}>▶</span> : i % 4 === 1 ? <span style={{ position: "absolute", top: 10, right: 12, color: "#fff", fontSize: 26 }}>⧉</span> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { AbsoluteFill, useVideoConfig } from "remotion";
|
||||
import { FONT } from "../lib/fonts";
|
||||
import { hexToRgba } from "../lib/anim";
|
||||
import { Grain, Vignette } from "../scenes/chrome";
|
||||
|
||||
/**
|
||||
* IGPromoDirections — the reference round for an Instagram channel promo. Three
|
||||
* named style directions rendered as real stills so the look can be PICKED before
|
||||
* we build the full template. 9:16 (Instagram-native).
|
||||
* A — Device / phone-first (clean, trustworthy product look)
|
||||
* B — Bold kinetic (energetic, on-trend, matches the bold taste)
|
||||
* C — Premium glass (elegant, agency / dark)
|
||||
*/
|
||||
export const igPromoSchema = z.object({ variant: z.enum(["A", "B", "C"]) });
|
||||
|
||||
const IG = "linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888)";
|
||||
const C = { handle: "@flat.studio", name: "استودیو فلت", tagline: "هر روز یک طرح تازه", followers: "۲۴٫۸ هزار", cta: "دنبال کنید" };
|
||||
const GRID = ["#ff5a3c", "#7c5cff", "#16b5a0", "#ffb23c", "#ef5da8", "#3aa0ff", "#ff7a59", "#4cd4b0", "#a06bff"];
|
||||
|
||||
const StatCol: React.FC<{ n: string; l: string; dark?: boolean }> = ({ n, l, dark }) => (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div style={{ fontWeight: 800, fontSize: 40, color: dark ? "#fff" : "#15151a" }}>{n}</div>
|
||||
<div style={{ fontWeight: 500, fontSize: 26, color: dark ? hexToRgba("#fff", 0.6) : hexToRgba("#15151a", 0.55) }}>{l}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FollowBtn: React.FC<{ w?: number; big?: boolean }> = ({ w = 360, big }) => (
|
||||
<div style={{ width: w, height: big ? 108 : 92, borderRadius: 999, background: IG, display: "flex", alignItems: "center", justifyContent: "center", gap: 14, color: "#fff", fontWeight: 800, fontSize: big ? 46 : 40, boxShadow: `0 18px 40px ${hexToRgba("#dc2743", 0.45)}`, border: "5px solid #ffffff22" }}>
|
||||
<span style={{ fontSize: big ? 48 : 42 }}>+</span>{C.cta}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Avatar: React.FC<{ size: number }> = ({ size }) => (
|
||||
<div style={{ width: size, height: size, borderRadius: "50%", background: IG, padding: size * 0.05, boxShadow: `0 10px 30px ${hexToRgba("#cc2366", 0.4)}` }}>
|
||||
<div style={{ width: "100%", height: "100%", borderRadius: "50%", background: "#2a2440", border: "5px solid #fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: size * 0.42 }}>📷</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Grid: React.FC<{ cell: number; gap: number; radius?: number }> = ({ cell, gap, radius = 14 }) => (
|
||||
<div style={{ display: "grid", gridTemplateColumns: `repeat(3, ${cell}px)`, gap }}>
|
||||
{GRID.map((c, i) => (
|
||||
<div key={i} style={{ width: cell, height: cell, borderRadius: radius, background: c, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<div style={{ width: cell * 0.34, height: cell * 0.34, borderRadius: 8, background: hexToRgba("#fff", 0.35) }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── A — phone-first ──────────────────────────────────────────────────────────
|
||||
const VariantA: React.FC = () => (
|
||||
<AbsoluteFill style={{ fontFamily: FONT, background: "linear-gradient(160deg,#fbf6f1,#f4e9f2)" }}>
|
||||
<div style={{ position: "absolute", left: "-15%", top: "-8%", width: "60%", height: "40%", borderRadius: "50%", background: hexToRgba("#dc2743", 0.22), filter: "blur(160px)" }} />
|
||||
<div style={{ position: "absolute", right: "-15%", bottom: "4%", width: "60%", height: "40%", borderRadius: "50%", background: hexToRgba("#7c5cff", 0.2), filter: "blur(170px)" }} />
|
||||
<div style={{ direction: "rtl", position: "absolute", top: 150, left: 0, right: 0, textAlign: "center" }}>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 12, background: IG, color: "#fff", fontWeight: 800, fontSize: 30, padding: "12px 28px", borderRadius: 999 }}>اینستاگرام</div>
|
||||
<div style={{ fontWeight: 900, fontSize: 96, color: "#15151a", marginTop: 22, letterSpacing: -2 }}>ما را دنبال کنید</div>
|
||||
</div>
|
||||
<div style={{ position: "absolute", left: "50%", top: 430, transform: "translateX(-50%)", width: 620, height: 1180, borderRadius: 70, background: "#15151a", padding: 18, boxShadow: "0 50px 90px rgba(20,10,30,0.4)" }}>
|
||||
<div style={{ width: "100%", height: "100%", borderRadius: 56, background: "#fff", direction: "rtl", padding: "40px 34px", display: "flex", flexDirection: "column", alignItems: "center", gap: 26 }}>
|
||||
<Avatar size={200} />
|
||||
<div style={{ fontWeight: 800, fontSize: 46, color: "#15151a" }}>{C.name}</div>
|
||||
<div style={{ fontWeight: 500, fontSize: 32, color: hexToRgba("#15151a", 0.5), marginTop: -14 }}>{C.handle}</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-around", width: "100%", marginTop: 6 }}>
|
||||
<StatCol n="۳۲۰" l="پست" /><StatCol n={C.followers} l="دنبالکننده" /><StatCol n="۱۸۰" l="دنبالشده" />
|
||||
</div>
|
||||
<FollowBtn w={500} />
|
||||
<div style={{ marginTop: 8 }}><Grid cell={150} gap={10} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<Vignette /><Grain />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
|
||||
// ── B — bold kinetic ─────────────────────────────────────────────────────────
|
||||
const Card: React.FC<{ x: number; y: number; rot: number; c: string; s?: number }> = ({ x, y, rot, c, s = 230 }) => (
|
||||
<div style={{ position: "absolute", left: x, top: y, width: s, height: s * 1.18, borderRadius: 24, background: c, border: "7px solid #0d0b1a", transform: `rotate(${rot}deg)`, boxShadow: "0 24px 40px rgba(0,0,0,0.4)", padding: 18 }}>
|
||||
<div style={{ width: "55%", height: 14, background: hexToRgba("#fff", 0.85), borderRadius: 999 }} />
|
||||
<div style={{ width: "35%", height: 14, background: hexToRgba("#fff", 0.5), borderRadius: 999, marginTop: 12 }} />
|
||||
<div style={{ position: "absolute", bottom: 16, right: 16, fontSize: 40 }}>♥</div>
|
||||
</div>
|
||||
);
|
||||
const VariantB: React.FC = () => (
|
||||
<AbsoluteFill style={{ fontFamily: FONT, background: "linear-gradient(155deg,#241433,#140d26 55%,#0c0a1c)" }}>
|
||||
<div style={{ position: "absolute", left: "-12%", top: "-6%", width: "60%", height: "42%", borderRadius: "50%", background: hexToRgba("#dc2743", 0.5), filter: "blur(150px)" }} />
|
||||
<div style={{ position: "absolute", right: "-12%", bottom: "-4%", width: "60%", height: "42%", borderRadius: "50%", background: hexToRgba("#7c5cff", 0.45), filter: "blur(160px)" }} />
|
||||
<AbsoluteFill style={{ opacity: 0.12, backgroundImage: `radial-gradient(${hexToRgba("#fff", 0.9)} 2.6px, transparent 2.8px)`, backgroundSize: "50px 50px", maskImage: "radial-gradient(75% 60% at 50% 45%, black, transparent 78%)", WebkitMaskImage: "radial-gradient(75% 60% at 50% 45%, black, transparent 78%)" }} />
|
||||
<Card x={70} y={250} rot={-12} c="#ff5a3c" /><Card x={760} y={300} rot={11} c="#16b5a0" s={250} />
|
||||
<Card x={60} y={1230} rot={9} c="#ffb23c" s={250} /><Card x={770} y={1300} rot={-10} c="#3aa0ff" />
|
||||
<div style={{ position: "absolute", inset: 0, direction: "rtl", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", textAlign: "center", padding: "0 60px" }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 40, color: hexToRgba("#fff", 0.8) }}>{C.tagline}</div>
|
||||
<div style={{ fontWeight: 900, fontSize: 132, color: "#fff", letterSpacing: -2, lineHeight: 1.0, marginTop: 16, textShadow: "0 10px 40px rgba(0,0,0,0.5)" }}>{C.name}</div>
|
||||
<div style={{ direction: "ltr", fontWeight: 800, fontSize: 60, color: "#ff9a3c", marginTop: 10 }}>{C.handle}</div>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 14, background: hexToRgba("#fff", 0.1), border: `2px solid ${hexToRgba("#fff", 0.25)}`, borderRadius: 999, padding: "14px 30px", color: "#fff", fontWeight: 700, fontSize: 38, marginTop: 34 }}>♥ {C.followers} دنبالکننده</div>
|
||||
<div style={{ marginTop: 34 }}><FollowBtn w={440} big /></div>
|
||||
</div>
|
||||
<Vignette /><Grain />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
|
||||
// ── C — premium glass ────────────────────────────────────────────────────────
|
||||
const VariantC: React.FC = () => (
|
||||
<AbsoluteFill style={{ fontFamily: FONT, background: "radial-gradient(120% 80% at 50% 0%,#1a1530,#0c0a14 60%,#08070e)" }}>
|
||||
<div style={{ position: "absolute", left: "50%", top: "30%", width: "90%", height: "45%", transform: "translateX(-50%)", borderRadius: "50%", background: hexToRgba("#cc2366", 0.4), filter: "blur(180px)" }} />
|
||||
<div style={{ position: "absolute", right: "20%", top: "8%", width: 260, height: 260, borderRadius: 50, background: IG, opacity: 0.5, filter: "blur(60px)" }} />
|
||||
<div style={{ position: "absolute", inset: 0, direction: "rtl", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "0 70px" }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 38, letterSpacing: 6, color: hexToRgba("#fff", 0.55), marginBottom: 40 }}>به ما بپیوندید</div>
|
||||
<div style={{ width: "100%", borderRadius: 48, background: hexToRgba("#ffffff", 0.06), border: `2px solid ${hexToRgba("#fff", 0.14)}`, boxShadow: "0 40px 90px rgba(0,0,0,0.5)", padding: "70px 50px", display: "flex", flexDirection: "column", alignItems: "center", gap: 28 }}>
|
||||
<Avatar size={230} />
|
||||
<div style={{ fontWeight: 800, fontSize: 58, color: "#fff" }}>{C.name}</div>
|
||||
<div style={{ direction: "ltr", fontWeight: 500, fontSize: 38, color: hexToRgba("#fff", 0.55), marginTop: -16 }}>{C.handle}</div>
|
||||
<div style={{ fontWeight: 500, fontSize: 34, color: hexToRgba("#fff", 0.7), textAlign: "center", lineHeight: 1.5 }}>{C.tagline}</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-around", width: "100%", marginTop: 10, paddingTop: 30, borderTop: `2px solid ${hexToRgba("#fff", 0.1)}` }}>
|
||||
<StatCol n="۳۲۰" l="پست" dark /><StatCol n={C.followers} l="دنبالکننده" dark /><StatCol n="۱۸۰" l="دنبالشده" dark />
|
||||
</div>
|
||||
<div style={{ marginTop: 16 }}><FollowBtn w={520} big /></div>
|
||||
</div>
|
||||
</div>
|
||||
<Vignette /><Grain />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
|
||||
export const IGPromoDirections: React.FC<z.infer<typeof igPromoSchema>> = ({ variant }) => {
|
||||
useVideoConfig();
|
||||
return variant === "A" ? <VariantA /> : variant === "B" ? <VariantB /> : <VariantC />;
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { ThreeCanvas } from "@remotion/three";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { Environment, Lightformer } from "@react-three/drei";
|
||||
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
|
||||
import * as THREE from "three";
|
||||
|
||||
// Challenge: match the low-poly isometric Yalda sofreh reference. Flat-shaded
|
||||
// low-poly geometry in @remotion/three, isometric-ish camera, warm candle light +
|
||||
// bloom, on a deep-red paisley ground.
|
||||
|
||||
const Rig: React.FC = () => {
|
||||
const { camera } = useThree();
|
||||
camera.position.set(7.5, 6.2, 7.5);
|
||||
camera.lookAt(0, 0.7, 0);
|
||||
camera.updateProjectionMatrix();
|
||||
return null;
|
||||
};
|
||||
|
||||
const mat = (color: string, opts: Partial<THREE.MeshStandardMaterialParameters> = {}) => (
|
||||
<meshStandardMaterial color={color} flatShading roughness={0.62} metalness={0.06} {...opts} />
|
||||
);
|
||||
|
||||
// a watermelon slice — extruded wedge (red flesh) + green rind edge + seeds
|
||||
const Slice: React.FC<{ position: [number, number, number]; rotation: [number, number, number] }> = ({ position, rotation }) => {
|
||||
const flesh = useMemo(() => {
|
||||
const s = new THREE.Shape();
|
||||
s.moveTo(-0.7, 0);
|
||||
s.lineTo(0.7, 0);
|
||||
s.absarc(0, 0, 0.7, 0, Math.PI, false);
|
||||
return new THREE.ExtrudeGeometry(s, { depth: 0.16, bevelEnabled: false });
|
||||
}, []);
|
||||
return (
|
||||
<group position={position} rotation={rotation}>
|
||||
<mesh geometry={flesh}>{mat("#e0322a")}</mesh>
|
||||
<mesh position={[0, 0.35, 0.08]} rotation={[0, 0, 0]}>
|
||||
<torusGeometry args={[0.72, 0.06, 6, 16, Math.PI]} />
|
||||
{mat("#2f7d3a")}
|
||||
</mesh>
|
||||
{[[-0.3, 0.3], [0.3, 0.3], [0, 0.5], [-0.15, 0.15], [0.15, 0.15]].map(([x, y], i) => (
|
||||
<mesh key={i} position={[x, y, 0.17]} scale={[1, 1.5, 0.4]}>
|
||||
<sphereGeometry args={[0.05, 6, 6]} />
|
||||
{mat("#201018", { roughness: 0.4 })}
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const Pomegranate: React.FC<{ position: [number, number, number] }> = ({ position }) => (
|
||||
<group position={position}>
|
||||
<mesh castShadow>
|
||||
<icosahedronGeometry args={[0.55, 1]} />
|
||||
{mat("#c0271f")}
|
||||
</mesh>
|
||||
<mesh position={[0, 0.55, 0]}>
|
||||
<coneGeometry args={[0.18, 0.3, 5]} />
|
||||
{mat("#8e2018")}
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
|
||||
const Candle: React.FC<{ position: [number, number, number] }> = ({ position }) => (
|
||||
<group position={position}>
|
||||
<mesh>
|
||||
<cylinderGeometry args={[0.16, 0.18, 0.7, 10]} />
|
||||
{mat("#f3ead2")}
|
||||
</mesh>
|
||||
<mesh position={[0, 0.5, 0]}>
|
||||
<coneGeometry args={[0.09, 0.26, 7]} />
|
||||
<meshStandardMaterial color="#ffd27a" emissive="#ffb437" emissiveIntensity={3} flatShading />
|
||||
</mesh>
|
||||
<pointLight position={[0, 0.6, 0]} intensity={6} distance={4} color="#ffb04a" />
|
||||
</group>
|
||||
);
|
||||
|
||||
export const YaldaSofreh3D: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
const spin = frame * 0.0;
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#b3262b" }}>
|
||||
<ThreeCanvas width={width} height={height} camera={{ position: [7.5, 6.2, 7.5], fov: 30 }} style={{ position: "absolute", inset: 0 }}>
|
||||
<Rig />
|
||||
<ambientLight intensity={0.45} />
|
||||
<directionalLight position={[5, 9, 4]} intensity={1.6} color="#fff2dc" castShadow />
|
||||
<Environment resolution={128}>
|
||||
<Lightformer intensity={1.4} position={[0, 5, 2]} scale={[8, 3, 1]} color="#ffd9a0" />
|
||||
<Lightformer intensity={1} position={[-4, 2, 3]} scale={[3, 5, 1]} color="#ff8a5a" />
|
||||
</Environment>
|
||||
|
||||
<group rotation={[0, spin, 0]}>
|
||||
{/* paisley-red ground */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.02, 0]} receiveShadow>
|
||||
<planeGeometry args={[26, 26]} />
|
||||
<meshStandardMaterial color="#a32226" roughness={0.9} />
|
||||
</mesh>
|
||||
|
||||
{/* low table + cloth */}
|
||||
<mesh position={[0, 0.35, 0]}>
|
||||
<boxGeometry args={[5.2, 0.3, 4.2]} />
|
||||
{mat("#7a4a28")}
|
||||
</mesh>
|
||||
{[[-2.3, 1.8], [2.3, 1.8], [-2.3, -1.8], [2.3, -1.8]].map(([x, z], i) => (
|
||||
<mesh key={i} position={[x, 0.1, z]}>
|
||||
<cylinderGeometry args={[0.12, 0.12, 0.5, 6]} />
|
||||
{mat("#5e3820")}
|
||||
</mesh>
|
||||
))}
|
||||
<mesh position={[0, 0.52, 0]}>
|
||||
<boxGeometry args={[5.0, 0.08, 4.0]} />
|
||||
{mat("#1f5fa6")}
|
||||
</mesh>
|
||||
|
||||
{/* mirror */}
|
||||
<group position={[0, 1.3, -1.3]}>
|
||||
<mesh><boxGeometry args={[1.5, 1.9, 0.12]} />{mat("#8a5a2e")}</mesh>
|
||||
<mesh position={[0, 0, 0.08]}><boxGeometry args={[1.2, 1.6, 0.04]} /><meshStandardMaterial color="#cfe6f0" metalness={0.6} roughness={0.2} flatShading /></mesh>
|
||||
</group>
|
||||
|
||||
{/* watermelon (low-poly) + slices */}
|
||||
<mesh position={[-1.7, 1.2, 0.3]} castShadow>
|
||||
<icosahedronGeometry args={[0.95, 1]} />
|
||||
{mat("#1f6b2e")}
|
||||
</mesh>
|
||||
<Slice position={[-0.4, 0.62, 1.1]} rotation={[-1.1, 0.3, 0]} />
|
||||
<Slice position={[0.4, 0.62, 1.3]} rotation={[-1.2, -0.4, 0]} />
|
||||
|
||||
<Pomegranate position={[0.1, 0.95, -0.2]} />
|
||||
<Pomegranate position={[1.9, 0.95, 1.2]} />
|
||||
|
||||
<Candle position={[-1.0, 0.9, -0.7]} />
|
||||
<Candle position={[1.0, 0.9, -0.7]} />
|
||||
|
||||
{/* Hafez book */}
|
||||
<group position={[1.7, 0.7, 0.2]} rotation={[0, -0.3, 0]}>
|
||||
<mesh><boxGeometry args={[1.0, 0.22, 1.4]} />{mat("#6b3f1c")}</mesh>
|
||||
<mesh position={[0, 0.12, 0]}><boxGeometry args={[0.5, 0.02, 0.7]} /><meshStandardMaterial color="#d4a83c" emissive="#a8842c" emissiveIntensity={0.5} flatShading /></mesh>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<EffectComposer>
|
||||
<Bloom intensity={1.1} luminanceThreshold={0.5} luminanceSmoothing={0.4} mipmapBlur />
|
||||
<Vignette eskil={false} offset={0.3} darkness={0.55} />
|
||||
</EffectComposer>
|
||||
</ThreeCanvas>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user