dc1fe11604
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
5.3 KiB
TypeScript
129 lines
5.3 KiB
TypeScript
import React, { useEffect, useMemo, useState } from "react";
|
||
import { createRoot } from "react-dom/client";
|
||
import { Player } from "@remotion/player";
|
||
import { FlexStory, calcFlexStoryMetadata, flexStoryDefaults } from "../src/compositions/FlexStory";
|
||
|
||
/**
|
||
* Standalone, isolated client-side player (Approach A). Runs as its own React-19 app
|
||
* so the React-Three-Fiber v9 templates render in the browser without touching the
|
||
* React-18 Next host. The studio embeds this via an <iframe> and feeds it the
|
||
* project's scene data (URL hash for the first paint, postMessage for live edits).
|
||
*
|
||
* Free tier shows a watermark overlay here (preview only). The clean, no-watermark
|
||
* EXPORT is issued server-side — never trust the client to drop the watermark.
|
||
*/
|
||
const FPS = 30;
|
||
const ASPECTS: Record<string, [number, number]> = {
|
||
"16:9": [1920, 1080],
|
||
"1:1": [1080, 1080],
|
||
"9:16": [1080, 1920],
|
||
};
|
||
|
||
interface PlayerInput {
|
||
props?: typeof flexStoryDefaults;
|
||
aspect?: keyof typeof ASPECTS;
|
||
watermark?: boolean;
|
||
}
|
||
|
||
// Asset-free demo shown when opened with no scene data (so the bare /player/ URL
|
||
// renders something real to test the in-browser engine).
|
||
const DEFAULT_DEMO = {
|
||
scenes: [
|
||
{ blockId: "IGIntro", durationSec: 3, props: { badge: "اینستاگرام", headline: "صفحهٔ ما را دنبال کنید", subtitle: "هر روز یک طرح تازه" } },
|
||
{ blockId: "IGProfile", durationSec: 5, props: { headline: "صفحهٔ ما را دنبال کنید", handle: "flat.studio", name: "استودیو فلت", category: "هنر و طراحی", bio1: "هر روز یک طرح تازه ✨", bio2: "آموزش، قالب و الهام برای طراحان", bio3: "سفارش و دانلود 👇", link: "flat.studio/shop", posts: "۳۲۰", followers: "۲۴٫۸ هزار", following: "۱۸۰", hi1: "جدید", hi2: "قالبها", hi3: "آموزش", hi4: "نمونهکار", followLabel: "دنبال کردن", messageLabel: "پیام" } },
|
||
{ blockId: "IGFeed", durationSec: 4, props: { caption: "محتوای ما را ببینید" } },
|
||
{ blockId: "IGStats", durationSec: 3, props: { bigValue: "۲۴۸۰۰", bigLabel: "دنبالکننده", stat2Value: "۱۲۰۰۰۰۰", stat2Label: "پسند", stat3Value: "۳۲۰", stat3Label: "پست", proofLine: "به جمع هزاران دنبالکنندهٔ ما بپیوندید" } },
|
||
{ blockId: "IGFollowCTA", durationSec: 3, props: { headline: "همین حالا دنبال کنید", handle: "@flat.studio", buttonLabel: "دنبال کردن", followedLabel: "دنبال شد", footer: "لینک در بایو 👆" } },
|
||
],
|
||
accentColor: "#dc2743", secondaryColor: "#7c5cff", backgroundColor: "#f7f4fa", textColor: "#15151a", music: "", sfx: false, finish: false,
|
||
} as unknown as typeof flexStoryDefaults;
|
||
|
||
function decodeHash(): PlayerInput | null {
|
||
try {
|
||
const h = window.location.hash.replace(/^#/, "");
|
||
if (!h) return null;
|
||
return JSON.parse(decodeURIComponent(escape(window.atob(h))));
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const Watermark: React.FC = () => (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
inset: 0,
|
||
pointerEvents: "none",
|
||
display: "flex",
|
||
flexWrap: "wrap",
|
||
alignContent: "center",
|
||
justifyContent: "center",
|
||
gap: "12vmin",
|
||
transform: "rotate(-24deg) scale(1.4)",
|
||
opacity: 0.16,
|
||
mixBlendMode: "overlay",
|
||
}}
|
||
>
|
||
{Array.from({ length: 24 }).map((_, i) => (
|
||
<span key={i} style={{ color: "#fff", fontWeight: 800, fontSize: "5vmin", letterSpacing: 2, whiteSpace: "nowrap", fontFamily: "system-ui, sans-serif" }}>
|
||
FlatRender • نسخهٔ پیشنمایش
|
||
</span>
|
||
))}
|
||
</div>
|
||
);
|
||
|
||
const App: React.FC = () => {
|
||
const [input, setInput] = useState<PlayerInput | null>(() => decodeHash());
|
||
|
||
useEffect(() => {
|
||
const onMsg = (e: MessageEvent) => {
|
||
if (e.data && e.data.type === "flatrender:props" && e.data.payload) {
|
||
setInput(e.data.payload as PlayerInput);
|
||
}
|
||
};
|
||
window.addEventListener("message", onMsg);
|
||
// Announce readiness so the studio can push the current props.
|
||
try {
|
||
window.parent?.postMessage({ type: "flatrender:ready" }, "*");
|
||
} catch {
|
||
/* not embedded */
|
||
}
|
||
return () => window.removeEventListener("message", onMsg);
|
||
}, []);
|
||
|
||
const props = input?.props ?? DEFAULT_DEMO;
|
||
const aspect = (input?.aspect ?? "9:16") as keyof typeof ASPECTS;
|
||
const [w, h] = ASPECTS[aspect] ?? ASPECTS["9:16"];
|
||
const watermark = input?.watermark ?? true;
|
||
|
||
const durationInFrames = useMemo(() => {
|
||
try {
|
||
return Math.max(1, calcFlexStoryMetadata({ props }).durationInFrames);
|
||
} catch {
|
||
return FPS * 10;
|
||
}
|
||
}, [props]);
|
||
|
||
return (
|
||
<div style={{ position: "fixed", inset: 0, background: "#000", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
||
<Player
|
||
component={FlexStory}
|
||
inputProps={props}
|
||
durationInFrames={durationInFrames}
|
||
fps={FPS}
|
||
compositionWidth={w}
|
||
compositionHeight={h}
|
||
style={{ width: "100%", height: "100%" }}
|
||
controls
|
||
loop
|
||
acknowledgeRemotionLicense
|
||
/>
|
||
{watermark && <Watermark />}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
createRoot(document.getElementById("root")!).render(<App />);
|