From 7394c5ce78a6a2976bb2eaca112b544eca686f58 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 23 Jun 2026 20:18:06 +0330 Subject: [PATCH] feat(remotion): +ProductShowcase block (phone/browser device mockup) Adds the product/app-showcase template type the engine was missing: a 2.5D device frame (rounded phone with notch, or a browser window with traffic-lights + URL bar) holding an uploaded screenshot, with title/subtitle and the shared Three backdrop. Fields: screenshot, title, subtitle, device (phone|browser). Registry now 9 blocks. Verified via FlexStory props-override stills (both device modes). Co-Authored-By: Claude Opus 4.8 --- .../src/scenes/blocks/ProductShowcase.tsx | 90 +++++++++++++++++++ services/remotion/src/scenes/registry.ts | 2 + 2 files changed, 92 insertions(+) create mode 100644 services/remotion/src/scenes/blocks/ProductShowcase.tsx diff --git a/services/remotion/src/scenes/blocks/ProductShowcase.tsx b/services/remotion/src/scenes/blocks/ProductShowcase.tsx new file mode 100644 index 0000000..1578762 --- /dev/null +++ b/services/remotion/src/scenes/blocks/ProductShowcase.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion"; +import { FONT } from "../../lib/fonts"; +import { hexToRgba, mixHex } from "../../lib/anim"; +import { ThreeBackdrop, Grain, Vignette, ProgressDots, Kicker, useSceneTransition } from "../chrome"; +import type { BlockProps, SceneBlock } from "../types"; + +const resolveSrc = (u: string) => (/^https?:\/\//.test(u) ? u : staticFile(u)); + +const ProductShowcase: React.FC = ({ data, colors, L, index, total, durationInFrames }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { opacity, slide } = useSceneTransition(durationInFrames, L); + const sp = spring({ frame: frame - 2, fps, config: { damping: 16, stiffness: 90 } }); + const rise = interpolate(sp, [0, 1], [L.vmin(70), 0]); + const scale = interpolate(sp, [0, 1], [0.88, 1]); + const float = Math.sin(frame / 28) * L.vmin(7); + const browser = (data.device || "phone") === "browser"; + + // device geometry + const phoneW = L.pick(L.vmin(330), L.vmin(310), L.vmin(360)); + const phoneH = phoneW * 2.05; + const browW = L.pick(L.vmin(720), L.vmin(640), L.vmin(700)); + const browH = browW * 0.62; + const w = browser ? browW : phoneW; + const h = browser ? browH : phoneH; + const rim = L.vmin(browser ? 8 : 12); + const radius = L.vmin(browser ? 16 : 44); + + const Screen = data.screenshot ? ( + + ) : ( +
+ ▦ +
+ ); + + const Device = ( +
+
+ {browser && ( +
+ {["#fb7185", "#fbbf24", "#34d399"].map((c) => ( + + ))} + +
+ )} +
{Screen}
+ {!browser &&
} +
+
+ ); + + const Text = ( +
+ +
{data.title}
+
{data.subtitle}
+
+ ); + + return ( + + + + {Device} + {Text} + + + + + + ); +}; + +export const ProductShowcaseBlock: SceneBlock = { + id: "ProductShowcase", + label: "معرفی محصول/اپ", + component: ProductShowcase, + fields: [ + { key: "screenshot", label: "اسکرین‌شات", type: "image", default: "" }, + { key: "title", label: "عنوان", type: "text", default: "محصول شما" }, + { key: "subtitle", label: "توضیح", type: "text", default: "ویژگی کلیدی محصول را اینجا بنویسید", multiline: true }, + { key: "device", label: "قاب (phone/browser)", type: "text", default: "phone" }, + ], + defaultDurationSec: 4, + minDurationSec: 2, + maxDurationSec: 8, +}; diff --git a/services/remotion/src/scenes/registry.ts b/services/remotion/src/scenes/registry.ts index 6d5ecdb..a0c9779 100644 --- a/services/remotion/src/scenes/registry.ts +++ b/services/remotion/src/scenes/registry.ts @@ -7,6 +7,7 @@ import { SlideshowBlock } from "./blocks/Slideshow"; import { OutroCTABlock } from "./blocks/OutroCTA"; import { LogoRevealBlock } from "./blocks/LogoReveal"; import { StatCounterBlock } from "./blocks/StatCounter"; +import { ProductShowcaseBlock } from "./blocks/ProductShowcase"; /** * The scene-block registry. A FlexStory template is an ordered list of these @@ -22,6 +23,7 @@ export const SCENE_BLOCKS: Record = { [OutroCTABlock.id]: OutroCTABlock, [LogoRevealBlock.id]: LogoRevealBlock, [StatCounterBlock.id]: StatCounterBlock, + [ProductShowcaseBlock.id]: ProductShowcaseBlock, }; export const BLOCK_LIST = Object.values(SCENE_BLOCKS);