From 383331e8f17291cb669c1c9bbff368294a238e09 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 23 Jun 2026 15:05:15 +0330 Subject: [PATCH] =?UTF-8?q?feat(remotion):=20+2=20scene=20blocks=20?= =?UTF-8?q?=E2=80=94=20LogoReveal=20(logo=20motion)=20+=20StatCounter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grows the scene-block library toward full template-type coverage: - LogoReveal: premium logo-motion — spring scale-in + glint sweep over the logo (image upload or a branded play-mark placeholder) + brand text + tagline, on the shared 2.5D Three backdrop. Fields: logoUrl, brandText, tagline. - StatCounter: animated count-up to a target (English-digit value → Persian display) + suffix + label. Fields: value, suffix, label. Registry now has 8 blocks. Both verified via FlexStory props-override stills. Co-Authored-By: Claude Opus 4.8 --- .../remotion/src/scenes/blocks/LogoReveal.tsx | 75 +++++++++++++++++++ .../src/scenes/blocks/StatCounter.tsx | 57 ++++++++++++++ services/remotion/src/scenes/registry.ts | 4 + 3 files changed, 136 insertions(+) create mode 100644 services/remotion/src/scenes/blocks/LogoReveal.tsx create mode 100644 services/remotion/src/scenes/blocks/StatCounter.tsx diff --git a/services/remotion/src/scenes/blocks/LogoReveal.tsx b/services/remotion/src/scenes/blocks/LogoReveal.tsx new file mode 100644 index 0000000..3711ec1 --- /dev/null +++ b/services/remotion/src/scenes/blocks/LogoReveal.tsx @@ -0,0 +1,75 @@ +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, useSceneTransition } from "../chrome"; +import type { BlockProps, SceneBlock } from "../types"; + +const resolveSrc = (u: string) => (/^https?:\/\//.test(u) ? u : staticFile(u)); + +const LogoReveal: React.FC = ({ data, colors, L, durationInFrames }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { opacity } = useSceneTransition(durationInFrames, L); + const logoSp = spring({ frame: frame - 4, fps, config: { damping: 12, stiffness: 120 } }); + const logoScale = interpolate(logoSp, [0, 1], [0.4, 1]); + const logoRot = interpolate(logoSp, [0, 1], [-12, 0]); + const sweep = interpolate(frame, [12, 36], [-1.3, 1.3], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const textOp = interpolate(frame, [18, 34], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const tagOp = interpolate(frame, [26, 42], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const logoSize = L.pick(L.vmin(280), L.vmin(260), L.vmin(300)); + + return ( + + + +
+ {data.logoUrl ? ( + + ) : ( +
+
+
+ )} + {/* glint sweep across the logo */} +
+
+
+ {data.brandText} +
+
+ {data.tagline} +
+ + + + + ); +}; + +export const LogoRevealBlock: SceneBlock = { + id: "LogoReveal", + label: "نمایش لوگو", + component: LogoReveal, + fields: [ + { key: "logoUrl", label: "لوگو (تصویر)", type: "image", default: "" }, + { key: "brandText", label: "نام برند", type: "text", default: "برند شما" }, + { key: "tagline", label: "شعار", type: "text", default: "شعار شما اینجا قرار می‌گیرد" }, + ], + defaultDurationSec: 4, + minDurationSec: 2, + maxDurationSec: 7, +}; diff --git a/services/remotion/src/scenes/blocks/StatCounter.tsx b/services/remotion/src/scenes/blocks/StatCounter.tsx new file mode 100644 index 0000000..2cc6499 --- /dev/null +++ b/services/remotion/src/scenes/blocks/StatCounter.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { FONT } from "../../lib/fonts"; +import { hexToRgba } from "../../lib/anim"; +import { ThreeBackdrop, Grain, Vignette, ProgressDots, Kicker, useSceneTransition } from "../chrome"; +import type { BlockProps, SceneBlock } from "../types"; + +const faNum = (s: string) => s.replace(/[0-9]/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]); + +const StatCounter: React.FC = ({ data, colors, L, index, total, durationInFrames }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { opacity, slide } = useSceneTransition(durationInFrames, L); + const target = parseFloat(data.value) || 0; + const decimals = (data.value.split(".")[1] || "").length; + const prog = spring({ + frame: frame - 6, + fps, + config: { damping: 22, stiffness: 70 }, + durationInFrames: Math.max(20, Math.min(durationInFrames - 14, 64)), + }); + const current = (target * prog).toFixed(decimals); + const labelOp = interpolate(frame, [10, 26], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + return ( + + + + +
+ {faNum(current)} + {data.suffix} +
+
+ {data.label} +
+
+ + + +
+ ); +}; + +export const StatCounterBlock: SceneBlock = { + id: "StatCounter", + label: "شمارنده / آمار", + component: StatCounter, + fields: [ + { key: "value", label: "عدد", type: "text", default: "98" }, + { key: "suffix", label: "پسوند (٪ + …)", type: "text", default: "٪" }, + { key: "label", label: "توضیح", type: "text", default: "رضایت کاربران" }, + ], + defaultDurationSec: 4, + minDurationSec: 2, + maxDurationSec: 7, +}; diff --git a/services/remotion/src/scenes/registry.ts b/services/remotion/src/scenes/registry.ts index 21d383a..6d5ecdb 100644 --- a/services/remotion/src/scenes/registry.ts +++ b/services/remotion/src/scenes/registry.ts @@ -5,6 +5,8 @@ import { ImageCaptionBlock } from "./blocks/ImageCaption"; import { KineticQuoteBlock } from "./blocks/KineticQuote"; import { SlideshowBlock } from "./blocks/Slideshow"; import { OutroCTABlock } from "./blocks/OutroCTA"; +import { LogoRevealBlock } from "./blocks/LogoReveal"; +import { StatCounterBlock } from "./blocks/StatCounter"; /** * The scene-block registry. A FlexStory template is an ordered list of these @@ -18,6 +20,8 @@ export const SCENE_BLOCKS: Record = { [KineticQuoteBlock.id]: KineticQuoteBlock, [SlideshowBlock.id]: SlideshowBlock, [OutroCTABlock.id]: OutroCTABlock, + [LogoRevealBlock.id]: LogoRevealBlock, + [StatCounterBlock.id]: StatCounterBlock, }; export const BLOCK_LIST = Object.values(SCENE_BLOCKS);