feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
/** A layer definition without an `id` — fresh ids are assigned when the template is applied. */
|
||||
export interface SceneTemplateLayer {
|
||||
type: "text" | "image" | "video" | "shape" | "draw";
|
||||
name?: string;
|
||||
visible?: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
zIndex: number;
|
||||
props: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const SCENE_BROWSER_CATEGORIES = [
|
||||
{ id: "all", label: "All Scenes" },
|
||||
{ id: "characters", label: "Characters" },
|
||||
{ id: "business", label: "Business" },
|
||||
{ id: "technology", label: "Technology" },
|
||||
{ id: "nature", label: "Nature" },
|
||||
{ id: "abstract", label: "Abstract" },
|
||||
{ id: "sports", label: "Sports" },
|
||||
{ id: "food", label: "Food" },
|
||||
] as const;
|
||||
|
||||
export type SceneBrowserCategoryId =
|
||||
(typeof SCENE_BROWSER_CATEGORIES)[number]["id"];
|
||||
|
||||
export type SceneBrowserContentCategory = Exclude<
|
||||
SceneBrowserCategoryId,
|
||||
"all"
|
||||
>;
|
||||
|
||||
export type SceneBrowserMediaFilter = "all" | "video" | "photo";
|
||||
|
||||
export interface BrowserSceneItem {
|
||||
id: string;
|
||||
name: string;
|
||||
category: SceneBrowserContentCategory;
|
||||
mediaType: "video" | "photo";
|
||||
characterCount: number;
|
||||
durationLabel: string;
|
||||
gradientFrom: string;
|
||||
gradientTo: string;
|
||||
/** Pre-built layers that populate the scene when selected. */
|
||||
templateLayers: SceneTemplateLayer[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout helpers — canvas is 1280 × 720
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Two-column layout: solid colour left, image placeholder right. */
|
||||
function splitLayout(
|
||||
bg: string,
|
||||
titleColor = "#FFFFFF",
|
||||
subtitleColor = "#94a3b8"
|
||||
): SceneTemplateLayer[] {
|
||||
return [
|
||||
{
|
||||
type: "shape",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 0,
|
||||
props: { shape: "rect", fill: bg, stroke: bg, strokeWidth: 0, cornerRadius: 0 },
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
x: 660,
|
||||
y: 60,
|
||||
width: 540,
|
||||
height: 600,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 1,
|
||||
props: { src: "", cornerRadius: 12 },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 80,
|
||||
y: 230,
|
||||
width: 530,
|
||||
height: 120,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 2,
|
||||
props: {
|
||||
text: "Your Main Title",
|
||||
fontSize: 60,
|
||||
fill: titleColor,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
align: "left",
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
animation: "none",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 80,
|
||||
y: 380,
|
||||
width: 530,
|
||||
height: 80,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 3,
|
||||
props: {
|
||||
text: "Your Subtitle Here",
|
||||
fontSize: 36,
|
||||
fill: subtitleColor,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
align: "left",
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
animation: "none",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Centred title + subtitle, no image placeholder. */
|
||||
function centeredLayout(
|
||||
bg: string,
|
||||
titleColor = "#FFFFFF",
|
||||
subtitleColor = "#94a3b8"
|
||||
): SceneTemplateLayer[] {
|
||||
return [
|
||||
{
|
||||
type: "shape",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 0,
|
||||
props: { shape: "rect", fill: bg, stroke: bg, strokeWidth: 0, cornerRadius: 0 },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 80,
|
||||
y: 265,
|
||||
width: 1120,
|
||||
height: 135,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 1,
|
||||
props: {
|
||||
text: "Your Main Title",
|
||||
fontSize: 72,
|
||||
fill: titleColor,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
align: "center",
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
animation: "none",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 80,
|
||||
y: 430,
|
||||
width: 1120,
|
||||
height: 80,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 2,
|
||||
props: {
|
||||
text: "Your Subtitle Here",
|
||||
fontSize: 40,
|
||||
fill: subtitleColor,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
align: "center",
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
animation: "none",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Full-bleed image background with a dark overlay + centred text on top. */
|
||||
function overlayLayout(
|
||||
bg: string,
|
||||
titleColor = "#FFFFFF",
|
||||
subtitleColor = "#e2e8f0"
|
||||
): SceneTemplateLayer[] {
|
||||
return [
|
||||
{
|
||||
type: "shape",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 0,
|
||||
props: { shape: "rect", fill: bg, stroke: bg, strokeWidth: 0, cornerRadius: 0 },
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 1,
|
||||
props: { src: "", cornerRadius: 0 },
|
||||
},
|
||||
{
|
||||
type: "shape",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rotation: 0,
|
||||
opacity: 0.55,
|
||||
zIndex: 2,
|
||||
props: { shape: "rect", fill: "#000000", stroke: "#000000", strokeWidth: 0, cornerRadius: 0 },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 80,
|
||||
y: 265,
|
||||
width: 1120,
|
||||
height: 135,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 3,
|
||||
props: {
|
||||
text: "Your Main Title",
|
||||
fontSize: 72,
|
||||
fill: titleColor,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
align: "center",
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
animation: "none",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 80,
|
||||
y: 430,
|
||||
width: 1120,
|
||||
height: 80,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
zIndex: 4,
|
||||
props: {
|
||||
text: "Your Subtitle Here",
|
||||
fontSize: 40,
|
||||
fill: subtitleColor,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
align: "center",
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
animation: "none",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scene catalog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const BROWSER_SCENES: BrowserSceneItem[] = [
|
||||
// ── Characters ────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "man-waving",
|
||||
name: "Man waving hello",
|
||||
category: "characters",
|
||||
mediaType: "video",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-sky-200",
|
||||
gradientTo: "to-blue-300",
|
||||
templateLayers: splitLayout("#0c1a3d"),
|
||||
},
|
||||
{
|
||||
id: "woman-presenting",
|
||||
name: "Woman presenting",
|
||||
category: "characters",
|
||||
mediaType: "video",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-violet-200",
|
||||
gradientTo: "to-purple-300",
|
||||
templateLayers: splitLayout("#1a0a2e"),
|
||||
},
|
||||
{
|
||||
id: "friendly-greeting",
|
||||
name: "Friendly office greeting",
|
||||
category: "characters",
|
||||
mediaType: "photo",
|
||||
characterCount: 2,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-rose-200",
|
||||
gradientTo: "to-pink-300",
|
||||
templateLayers: splitLayout("#2d0a14"),
|
||||
},
|
||||
{
|
||||
id: "customer-support",
|
||||
name: "Customer support agent",
|
||||
category: "characters",
|
||||
mediaType: "video",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-cyan-200",
|
||||
gradientTo: "to-teal-300",
|
||||
templateLayers: splitLayout("#071a1a"),
|
||||
},
|
||||
|
||||
// ── Business ──────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "team-meeting",
|
||||
name: "Team meeting",
|
||||
category: "business",
|
||||
mediaType: "video",
|
||||
characterCount: 4,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-blue-200",
|
||||
gradientTo: "to-indigo-300",
|
||||
templateLayers: splitLayout("#0a1a3d"),
|
||||
},
|
||||
{
|
||||
id: "handshake-deal",
|
||||
name: "Handshake closing deal",
|
||||
category: "business",
|
||||
mediaType: "photo",
|
||||
characterCount: 2,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-slate-200",
|
||||
gradientTo: "to-gray-300",
|
||||
templateLayers: splitLayout("#0f172a"),
|
||||
},
|
||||
{
|
||||
id: "startup-pitch",
|
||||
name: "Startup pitch deck",
|
||||
category: "business",
|
||||
mediaType: "video",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-indigo-200",
|
||||
gradientTo: "to-violet-300",
|
||||
templateLayers: centeredLayout("#0f0f2e"),
|
||||
},
|
||||
{
|
||||
id: "office-collaboration",
|
||||
name: "Office collaboration",
|
||||
category: "business",
|
||||
mediaType: "video",
|
||||
characterCount: 3,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-blue-200",
|
||||
gradientTo: "to-sky-300",
|
||||
templateLayers: splitLayout("#0a1428"),
|
||||
},
|
||||
|
||||
// ── Technology ────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "city-skyline",
|
||||
name: "City skyline",
|
||||
category: "technology",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-indigo-200",
|
||||
gradientTo: "to-blue-400",
|
||||
templateLayers: overlayLayout("#0a0f2e"),
|
||||
},
|
||||
{
|
||||
id: "tech-network",
|
||||
name: "Tech network",
|
||||
category: "technology",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-cyan-200",
|
||||
gradientTo: "to-indigo-300",
|
||||
templateLayers: centeredLayout("#071a1a"),
|
||||
},
|
||||
{
|
||||
id: "coding-desk",
|
||||
name: "Developer at desk",
|
||||
category: "technology",
|
||||
mediaType: "photo",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-emerald-200",
|
||||
gradientTo: "to-teal-300",
|
||||
templateLayers: splitLayout("#071c14"),
|
||||
},
|
||||
{
|
||||
id: "data-visualization",
|
||||
name: "Data visualization",
|
||||
category: "technology",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-violet-200",
|
||||
gradientTo: "to-fuchsia-300",
|
||||
templateLayers: centeredLayout("#1a0a2e"),
|
||||
},
|
||||
|
||||
// ── Nature ────────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "forest-path",
|
||||
name: "Forest morning path",
|
||||
category: "nature",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-green-200",
|
||||
gradientTo: "to-emerald-300",
|
||||
templateLayers: overlayLayout("#071c0f"),
|
||||
},
|
||||
{
|
||||
id: "ocean-sunset",
|
||||
name: "Ocean sunset",
|
||||
category: "nature",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-amber-200",
|
||||
gradientTo: "to-orange-300",
|
||||
templateLayers: overlayLayout("#1c0f07"),
|
||||
},
|
||||
{
|
||||
id: "mountain-aerial",
|
||||
name: "Mountain aerial",
|
||||
category: "nature",
|
||||
mediaType: "photo",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-sky-200",
|
||||
gradientTo: "to-blue-300",
|
||||
templateLayers: overlayLayout("#0c1a2e"),
|
||||
},
|
||||
{
|
||||
id: "wildlife-meadow",
|
||||
name: "Wildlife meadow",
|
||||
category: "nature",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-lime-200",
|
||||
gradientTo: "to-green-300",
|
||||
templateLayers: overlayLayout("#0a1c07"),
|
||||
},
|
||||
|
||||
// ── Abstract ──────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "abstract-waves",
|
||||
name: "Abstract waves",
|
||||
category: "abstract",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-fuchsia-200",
|
||||
gradientTo: "to-purple-300",
|
||||
templateLayers: centeredLayout("#1a0a2e"),
|
||||
},
|
||||
{
|
||||
id: "gradient-flow",
|
||||
name: "Gradient flow",
|
||||
category: "abstract",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-pink-200",
|
||||
gradientTo: "to-rose-300",
|
||||
templateLayers: centeredLayout("#1c0a14"),
|
||||
},
|
||||
{
|
||||
id: "geometric-shapes",
|
||||
name: "Geometric shapes",
|
||||
category: "abstract",
|
||||
mediaType: "photo",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-indigo-200",
|
||||
gradientTo: "to-violet-300",
|
||||
templateLayers: centeredLayout("#0f0a2e"),
|
||||
},
|
||||
{
|
||||
id: "particle-burst",
|
||||
name: "Particle burst",
|
||||
category: "abstract",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-blue-200",
|
||||
gradientTo: "to-cyan-300",
|
||||
templateLayers: centeredLayout("#071628"),
|
||||
},
|
||||
|
||||
// ── Sports ────────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "sports-celebration",
|
||||
name: "Sports celebration",
|
||||
category: "sports",
|
||||
mediaType: "video",
|
||||
characterCount: 3,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-orange-200",
|
||||
gradientTo: "to-red-300",
|
||||
templateLayers: splitLayout("#1c0f07"),
|
||||
},
|
||||
{
|
||||
id: "soccer-goal",
|
||||
name: "Soccer goal moment",
|
||||
category: "sports",
|
||||
mediaType: "video",
|
||||
characterCount: 2,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-green-200",
|
||||
gradientTo: "to-lime-300",
|
||||
templateLayers: splitLayout("#0a1c07"),
|
||||
},
|
||||
{
|
||||
id: "gym-workout",
|
||||
name: "Gym workout",
|
||||
category: "sports",
|
||||
mediaType: "photo",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-amber-200",
|
||||
gradientTo: "to-yellow-300",
|
||||
templateLayers: splitLayout("#1c1007"),
|
||||
},
|
||||
{
|
||||
id: "running-track",
|
||||
name: "Running on track",
|
||||
category: "sports",
|
||||
mediaType: "video",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-sky-200",
|
||||
gradientTo: "to-indigo-300",
|
||||
templateLayers: splitLayout("#0a1428"),
|
||||
},
|
||||
|
||||
// ── Food ──────────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: "food-preparation",
|
||||
name: "Food preparation",
|
||||
category: "food",
|
||||
mediaType: "video",
|
||||
characterCount: 1,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-amber-200",
|
||||
gradientTo: "to-orange-300",
|
||||
templateLayers: splitLayout("#1c0f07"),
|
||||
},
|
||||
{
|
||||
id: "restaurant-plating",
|
||||
name: "Restaurant plating",
|
||||
category: "food",
|
||||
mediaType: "photo",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-rose-200",
|
||||
gradientTo: "to-red-300",
|
||||
templateLayers: overlayLayout("#1c0a0f"),
|
||||
},
|
||||
{
|
||||
id: "coffee-pour",
|
||||
name: "Coffee pour slow-mo",
|
||||
category: "food",
|
||||
mediaType: "video",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-yellow-200",
|
||||
gradientTo: "to-amber-300",
|
||||
templateLayers: overlayLayout("#0f0a07"),
|
||||
},
|
||||
{
|
||||
id: "fresh-ingredients",
|
||||
name: "Fresh ingredients",
|
||||
category: "food",
|
||||
mediaType: "photo",
|
||||
characterCount: 0,
|
||||
durationLabel: "3-10 sec.",
|
||||
gradientFrom: "from-lime-200",
|
||||
gradientTo: "to-green-300",
|
||||
templateLayers: overlayLayout("#0a1c07"),
|
||||
},
|
||||
];
|
||||
|
||||
export function filterBrowserScenes(
|
||||
scenes: BrowserSceneItem[],
|
||||
options: {
|
||||
categoryId: SceneBrowserCategoryId;
|
||||
mediaFilter: SceneBrowserMediaFilter;
|
||||
search: string;
|
||||
}
|
||||
): BrowserSceneItem[] {
|
||||
const query = options.search.trim().toLowerCase();
|
||||
|
||||
return scenes.filter((scene) => {
|
||||
if (options.categoryId !== "all" && scene.category !== options.categoryId) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
options.mediaFilter !== "all" &&
|
||||
scene.mediaType !== options.mediaFilter
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (query && !scene.name.toLowerCase().includes(query)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user