2026-06-25 12:11:27 +03:30
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 ;
}
2026-06-25 12:46:53 +03:30
// 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 ;
2026-06-25 12:11:27 +03:30
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 ) ;
} , [ ] ) ;
2026-06-25 12:46:53 +03:30
const props = input ? . props ? ? DEFAULT_DEMO ;
2026-06-25 12:11:27 +03:30
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 / > ) ;