116 lines
3.6 KiB
TypeScript
116 lines
3.6 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 ?? flexStoryDefaults;
|
|||
|
|
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 />);
|