From 8c4bc2c62647c51a1b3a43f3b37e32efe8e8dd0c Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 25 Jun 2026 07:26:01 +0330 Subject: [PATCH] feat(remotion): craft kit (stop-motion + paper-cut) + PaperCut block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real visible quality leap — a handmade craft aesthetic code can't fake by being smooth: - craft.ts: useStopMotion (quantize the frame to "on twos/threes" + per-step jitter → choppy handmade motion), paperShadow (layered cast shadows for paper depth), PAPER_TEXTURE (procedural fibrous paper grain). - PaperCut block: a layered paper-cut landscape — sun + 4 brand-coloured paper hills with real cast shadows + paper grain, rising into place on stop-motion timing with an idle wobble, + paper-cut title/subtitle. Re-flows 16:9/1:1/9:16. Registry now has 13 blocks. Verified: warm Yalda render (fits the Persian/seasonal moat) + a stop-motion demo clip showing the on-threes choppy rise. Co-Authored-By: Claude Opus 4.8 --- .../remotion/src/scenes/blocks/PaperCut.tsx | 75 +++++++++++++++++++ services/remotion/src/scenes/craft.ts | 27 +++++++ services/remotion/src/scenes/registry.ts | 2 + 3 files changed, 104 insertions(+) create mode 100644 services/remotion/src/scenes/blocks/PaperCut.tsx create mode 100644 services/remotion/src/scenes/craft.ts diff --git a/services/remotion/src/scenes/blocks/PaperCut.tsx b/services/remotion/src/scenes/blocks/PaperCut.tsx new file mode 100644 index 0000000..c2f69f7 --- /dev/null +++ b/services/remotion/src/scenes/blocks/PaperCut.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useVideoConfig } from "remotion"; +import { FONT } from "../../lib/fonts"; +import { hexToRgba, mixHex } from "../../lib/anim"; +import { Vignette } from "../chrome"; +import { useStopMotion, paperShadow, PAPER_TEXTURE } from "../craft"; +import type { BlockProps, SceneBlock } from "../types"; + +const PaperCut: React.FC = ({ data, colors, L, index, total, durationInFrames }) => { + const { fps, width, height } = useVideoConfig(); + const { stepped, jitter } = useStopMotion(3); + + const out = interpolate(stepped, [durationInFrames - 12, durationInFrames], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const opacity = 1 - out; + + // each layer rises into place on stepped (handmade) timing + const rise = (delay: number) => { + const sp = spring({ frame: stepped - delay, fps, config: { damping: 16, stiffness: 70 } }); + return interpolate(sp, [0, 1], [height * 0.55, 0]); + }; + + // brand-derived paper palette: light sky → darkening hills + const sky = mixHex(colors.backgroundColor, "#fff6e8", 0.5); + const sun = mixHex(colors.accentColor, "#ffd27a", 0.45); + const hills = [ + { y: height * 0.5, c: mixHex(colors.secondaryColor, "#ffffff", 0.45), depth: 1, delay: 0 }, + { y: height * 0.62, c: mixHex(colors.secondaryColor, colors.accentColor, 0.5), depth: 1.6, delay: 3 }, + { y: height * 0.74, c: colors.accentColor, depth: 2.3, delay: 6 }, + { y: height * 0.86, c: mixHex(colors.accentColor, "#3a2418", 0.4), depth: 3.2, delay: 9 }, + ]; + const hillPath = (y: number, amp: number) => + `M0 ${height} L0 ${y} C ${width * 0.33} ${y - amp} ${width * 0.66} ${y + amp * 0.7} ${width} ${y - amp * 0.2} L ${width} ${height} Z`; + + return ( + + {/* sun */} +
+ + {/* layered paper hills (stop-motion rise + idle wobble) */} + {hills.map((h, i) => ( + + + + ))} + + {/* paper grain over the whole scene */} + + + {/* title block (paper-cut text) */} + +
+ {data.title} +
+
+ {data.subtitle} +
+
+ + +
+ ); +}; + +export const PaperCutBlock: SceneBlock = { + id: "PaperCut", + label: "کاغذبری (پیپرکات + استاپ‌موشن)", + component: PaperCut, + fields: [ + { key: "title", label: "عنوان", type: "text", default: "داستان شما" }, + { key: "subtitle", label: "زیرعنوان", type: "text", default: "ساخته‌شده با عشق، لایه به لایه" }, + ], + defaultDurationSec: 5, + minDurationSec: 3, + maxDurationSec: 9, +}; diff --git a/services/remotion/src/scenes/craft.ts b/services/remotion/src/scenes/craft.ts new file mode 100644 index 0000000..8f32efc --- /dev/null +++ b/services/remotion/src/scenes/craft.ts @@ -0,0 +1,27 @@ +import { useCurrentFrame } from "remotion"; +import { rand } from "../lib/anim"; + +/** + * Craft kit — the primitives that give the *handmade* look code can't fake by being + * smooth: stop-motion timing (quantize the frame so motion plays "on twos/threes" + * with a per-step wobble) and paper-cut depth (layered drop-shadows + a fibrous + * paper-grain texture). Used by the PaperCut block; reusable by any block. + */ + +/** Stop-motion timing: step the frame so animation holds for `stride` frames + * (choppy, handmade), plus a deterministic per-step jitter for the wobble. */ +export function useStopMotion(stride = 3) { + const frame = useCurrentFrame(); + const stepped = Math.floor(frame / stride) * stride; + const jitter = (seed: number, amount: number) => (rand(stepped * 13.7 + seed) - 0.5) * amount; + return { stepped, jitter }; +} + +/** Layered-paper cast shadow for paper-cut depth (deeper layer = softer/larger). */ +export const paperShadow = (depth: number) => + `drop-shadow(0 ${Math.round(depth * 5)}px ${Math.round(depth * 8)}px rgba(40,28,18,0.22))`; + +/** Procedural fibrous paper grain — stretched fractal noise (vertical fibers), + * laid over each paper layer for a real torn-card texture. Deterministic. */ +export const PAPER_TEXTURE = + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.012 0.45' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23p)'/%3E%3C/svg%3E\")"; diff --git a/services/remotion/src/scenes/registry.ts b/services/remotion/src/scenes/registry.ts index 37b18f4..a9dc438 100644 --- a/services/remotion/src/scenes/registry.ts +++ b/services/remotion/src/scenes/registry.ts @@ -11,6 +11,7 @@ import { BarChartBlock } from "./blocks/BarChart"; import { StompBlock } from "./blocks/Stomp"; import { DeviceMockupBlock } from "./blocks/DeviceMockup"; import { ProductShowcaseBlock } from "./blocks/ProductShowcase"; +import { PaperCutBlock } from "./blocks/PaperCut"; /** * The scene-block registry. A FlexStory template is an ordered list of these @@ -30,6 +31,7 @@ export const SCENE_BLOCKS: Record = { [BarChartBlock.id]: BarChartBlock, [StompBlock.id]: StompBlock, [DeviceMockupBlock.id]: DeviceMockupBlock, + [PaperCutBlock.id]: PaperCutBlock, }; export const BLOCK_LIST = Object.values(SCENE_BLOCKS);