diff --git a/.gitignore b/.gitignore index 9606bbc..1ba30bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# dependencies +# dependencies (root + any nested, e.g. services/remotion/node_modules) /node_modules +node_modules/ /.pnp .pnp.js .yarn/install-state.gz @@ -55,3 +56,10 @@ node-agent.exe # node-agent local build + secrets services/node-agent/dist/ agent.env + +# remotion render outputs (regenerated; thumbnails/previews live in public/template-media) +services/remotion/out/ + +# local scratch / agent work +/-w +/.agent-work/ diff --git a/backend/db/migrations/32_content_render_engine.sql b/backend/db/migrations/32_content_render_engine.sql new file mode 100644 index 0000000..0b2aec6 --- /dev/null +++ b/backend/db/migrations/32_content_render_engine.sql @@ -0,0 +1,16 @@ +-- 32_content_render_engine.sql +-- Two render engines per template: After Effects (.aep, rendered by a node-agent) +-- and Remotion (code-based React composition). render_engine selects which; for +-- Remotion templates render_remotion_comp holds the composition id to render +-- (the .aep / render_aep_comp columns stay null for those). +-- +-- Apply manually on the live DB (migrations are not auto-run): +-- docker exec -i psql -U postgres -d flatrender < 32_content_render_engine.sql + +ALTER TABLE content.projects + ADD COLUMN IF NOT EXISTS render_engine TEXT NOT NULL DEFAULT 'AfterEffects'; + +ALTER TABLE content.projects + ADD COLUMN IF NOT EXISTS render_remotion_comp TEXT; + +-- Existing templates are all After Effects; the default already covers them. diff --git a/messages/en.json b/messages/en.json index dcb3814..6b77818 100644 --- a/messages/en.json +++ b/messages/en.json @@ -144,14 +144,14 @@ "a7": "Yes. Cancel from your account settings at any time. You keep access through the end of your billing period, and you can downgrade to Free without losing your projects." }, "pricing": { - "heading": "Choose your FlatRender plan", + "heading": "Pay by the second, not by the video", "monthly": "Monthly", "annual": "Annual", "saveBadge": "Save up to {percent}%", "subscribe": "Subscribe", "freeBannerTitle": "Free plan", "freeBannerDesc": "Free forever, no credit card required", - "perMonth": "/ mo", + "perMonth": "monthly", "billedAnnually": "billed annually", "compareTitle": "Compare all plans", "allFeatures": "All features", @@ -161,7 +161,45 @@ "proName": "Pro", "proDesc": "Become a pro and unlock more powerful video, design and website editing tools for commercial use.", "businessName": "Business", - "businessDesc": "Advanced level solution for teams and businesses. Includes reseller license." + "businessDesc": "Advanced level solution for teams and businesses. Includes reseller license.", + "subheading": "Each render costs render-seconds equal to the video length × a quality multiplier. Every plan gives you a monthly bucket of render-seconds.", + "toman": "Toman", + "free": "Free", + "mostPopular": "Most popular", + "currentPlan": "Current plan", + "choosePlan": "Choose plan", + "startFree": "Start free", + "processing": "Redirecting…", + "signInToBuy": "Sign in to buy", + "emptyState": "No plans are available right now.", + "perMonthSuffix": "/ mo", + "featSeconds": "{seconds} render-seconds / month", + "featResolution": "Up to {res} quality", + "featParallelOne": "1 render at a time", + "featParallel": "{n} parallel renders", + "featStorage": "{gb} GB cloud storage", + "featSpeed": "{factor}× render speed", + "featWatermarkOn": "FlatRender watermark", + "featWatermarkOff": "No watermark", + "calcTitle": "How many seconds do I need?", + "calcDesc": "Pick a video length and quality to see the per-render cost in seconds.", + "calcLength": "Video length", + "calcResolution": "Output quality", + "calcCost": "Cost per render", + "calcSecondsUnit": "seconds", + "calcRendersWith": "With each plan:", + "calcVideosFmt": "≈ {count} videos", + "multiplierTitle": "Quality multiplier", + "multiplierDesc": "Render-seconds per render = video length × the multiplier below.", + "multiplierColRes": "Quality", + "multiplierColMul": "Multiplier", + "faqTitle": "Frequently asked", + "faqQ1": "What is a render-second?", + "faqA1": "Instead of a video-count limit, you buy render-seconds. A 15-second video at 720p uses exactly 15 seconds of your balance.", + "faqQ2": "Why does higher quality cost more seconds?", + "faqA2": "4K rendering is much heavier, so each second of video counts as 4 render-seconds; 1080p counts as 2×.", + "faqQ3": "What if I run out of seconds?", + "faqA3": "Upgrade your plan or wait for the next period. Your max resolution and parallel renders also follow your plan." }, "footer": { "brandName": "FlatRender", diff --git a/messages/fa.json b/messages/fa.json index 47adf25..c9132df 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -144,14 +144,14 @@ "a7": "بله. هر زمان از تنظیمات حساب لغو کنید. دسترسی تا پایان دوره صورت‌حساب باقی می‌ماند و می‌توانید به پلن رایگان برگردید بدون اینکه پروژه‌هایتان از دست بروند." }, "pricing": { - "heading": "پلن فلت‌رندر خود را انتخاب کنید", + "heading": "پرداخت بر اساس ثانیه، نه تعداد ویدیو", "monthly": "ماهانه", "annual": "سالانه", "saveBadge": "تا {percent}٪ صرفه‌جویی", "subscribe": "اشتراک", "freeBannerTitle": "پلن رایگان", "freeBannerDesc": "برای همیشه رایگان، بدون نیاز به کارت اعتباری", - "perMonth": "/ ماه", + "perMonth": "ماهانه", "billedAnnually": "پرداخت سالانه", "compareTitle": "مقایسه همه پلن‌ها", "allFeatures": "همه امکانات", @@ -161,7 +161,45 @@ "proName": "Pro", "proDesc": "حرفه‌ای شوید و ابزارهای قدرتمندتر ویدیو، طراحی و وب‌سایت را برای استفاده تجاری باز کنید.", "businessName": "Business", - "businessDesc": "راه‌حل پیشرفته برای تیم‌ها و کسب‌وکارها. شامل مجوز فروش مجدد." + "businessDesc": "راه‌حل پیشرفته برای تیم‌ها و کسب‌وکارها. شامل مجوز فروش مجدد.", + "subheading": "هزینهٔ هر رندر برابر است با طول ویدیو ضربدر ضریب کیفیت. هر پلن ماهانه مقداری «ثانیهٔ رندر» در اختیار شما می‌گذارد.", + "toman": "تومان", + "free": "رایگان", + "mostPopular": "محبوب‌ترین", + "currentPlan": "پلن فعلی", + "choosePlan": "انتخاب پلن", + "startFree": "شروع رایگان", + "processing": "در حال انتقال…", + "signInToBuy": "برای خرید وارد شوید", + "emptyState": "در حال حاضر پلنی برای نمایش وجود ندارد.", + "perMonthSuffix": "/ ماه", + "featSeconds": "{seconds} ثانیهٔ رندر در ماه", + "featResolution": "کیفیت تا {res}", + "featParallelOne": "۱ رندر همزمان", + "featParallel": "{n} رندر همزمان", + "featStorage": "{gb} گیگابایت فضای ابری", + "featSpeed": "سرعت رندر ×{factor}", + "featWatermarkOn": "دارای واترمارک FlatRender", + "featWatermarkOff": "بدون واترمارک", + "calcTitle": "چند ثانیه لازم دارم؟", + "calcDesc": "طول و کیفیت ویدیو را انتخاب کنید تا هزینهٔ ثانیه‌ای هر رندر را ببینید.", + "calcLength": "طول ویدیو", + "calcResolution": "کیفیت خروجی", + "calcCost": "هزینهٔ هر رندر", + "calcSecondsUnit": "ثانیه", + "calcRendersWith": "با هر پلن:", + "calcVideosFmt": "≈ {count} ویدیو", + "multiplierTitle": "ضریب کیفیت", + "multiplierDesc": "ثانیهٔ مصرفی هر رندر = طول ویدیو × ضریب کیفیت زیر.", + "multiplierColRes": "کیفیت", + "multiplierColMul": "ضریب", + "faqTitle": "پرسش‌های پرتکرار", + "faqQ1": "ثانیهٔ رندر یعنی چه؟", + "faqA1": "به‌جای محدودیت تعداد ویدیو، شما مقداری ثانیهٔ رندر می‌خرید. یک ویدیوی ۱۵ ثانیه‌ای با کیفیت ۷۲۰p دقیقاً ۱۵ ثانیه از سهم شما کم می‌کند.", + "faqQ2": "چرا کیفیت بالاتر ثانیهٔ بیشتری می‌برد؟", + "faqA2": "رندر ۴K پردازش سنگین‌تری دارد، بنابراین هر ثانیه ویدیو معادل ۴ ثانیهٔ رندر حساب می‌شود؛ ۱۰۸۰p معادل ۲ برابر.", + "faqQ3": "اگر ثانیه‌هایم تمام شود چه می‌شود؟", + "faqA3": "می‌توانید پلن خود را ارتقا دهید یا تا شروع دورهٔ بعد صبر کنید. سقف کیفیت و رندر همزمان نیز بر اساس پلن شماست." }, "footer": { "brandName": "فلت‌رندر", diff --git a/public/template-media/Birthday3D-16x9.png b/public/template-media/Birthday3D-16x9.png new file mode 100644 index 0000000..2b1641d Binary files /dev/null and b/public/template-media/Birthday3D-16x9.png differ diff --git a/public/template-media/Birthday3D-1x1.png b/public/template-media/Birthday3D-1x1.png new file mode 100644 index 0000000..8751313 Binary files /dev/null and b/public/template-media/Birthday3D-1x1.png differ diff --git a/public/template-media/Birthday3D-9x16.png b/public/template-media/Birthday3D-9x16.png new file mode 100644 index 0000000..8115bb2 Binary files /dev/null and b/public/template-media/Birthday3D-9x16.png differ diff --git a/public/template-media/Birthday3D.mp4 b/public/template-media/Birthday3D.mp4 new file mode 100644 index 0000000..b5b4b53 Binary files /dev/null and b/public/template-media/Birthday3D.mp4 differ diff --git a/public/template-media/Countdown-16x9.png b/public/template-media/Countdown-16x9.png new file mode 100644 index 0000000..4dff8e4 Binary files /dev/null and b/public/template-media/Countdown-16x9.png differ diff --git a/public/template-media/Countdown-1x1.png b/public/template-media/Countdown-1x1.png new file mode 100644 index 0000000..1c74f72 Binary files /dev/null and b/public/template-media/Countdown-1x1.png differ diff --git a/public/template-media/Countdown-9x16.png b/public/template-media/Countdown-9x16.png new file mode 100644 index 0000000..716c66b Binary files /dev/null and b/public/template-media/Countdown-9x16.png differ diff --git a/public/template-media/Countdown.mp4 b/public/template-media/Countdown.mp4 new file mode 100644 index 0000000..ae65e78 Binary files /dev/null and b/public/template-media/Countdown.mp4 differ diff --git a/public/template-media/EventInvite-16x9.png b/public/template-media/EventInvite-16x9.png new file mode 100644 index 0000000..7aa85e9 Binary files /dev/null and b/public/template-media/EventInvite-16x9.png differ diff --git a/public/template-media/EventInvite-1x1.png b/public/template-media/EventInvite-1x1.png new file mode 100644 index 0000000..63dfcad Binary files /dev/null and b/public/template-media/EventInvite-1x1.png differ diff --git a/public/template-media/EventInvite-9x16.png b/public/template-media/EventInvite-9x16.png new file mode 100644 index 0000000..19c7516 Binary files /dev/null and b/public/template-media/EventInvite-9x16.png differ diff --git a/public/template-media/EventInvite.mp4 b/public/template-media/EventInvite.mp4 new file mode 100644 index 0000000..51ce0e0 Binary files /dev/null and b/public/template-media/EventInvite.mp4 differ diff --git a/public/template-media/GlitterReveal-16x9.png b/public/template-media/GlitterReveal-16x9.png new file mode 100644 index 0000000..4d9b04b Binary files /dev/null and b/public/template-media/GlitterReveal-16x9.png differ diff --git a/public/template-media/GlitterReveal-1x1.png b/public/template-media/GlitterReveal-1x1.png new file mode 100644 index 0000000..3b31c8c Binary files /dev/null and b/public/template-media/GlitterReveal-1x1.png differ diff --git a/public/template-media/GlitterReveal-9x16.png b/public/template-media/GlitterReveal-9x16.png new file mode 100644 index 0000000..da84fb3 Binary files /dev/null and b/public/template-media/GlitterReveal-9x16.png differ diff --git a/public/template-media/GlitterReveal.mp4 b/public/template-media/GlitterReveal.mp4 new file mode 100644 index 0000000..7b889f3 Binary files /dev/null and b/public/template-media/GlitterReveal.mp4 differ diff --git a/public/template-media/HappyBirthday-16x9.png b/public/template-media/HappyBirthday-16x9.png new file mode 100644 index 0000000..0173abd Binary files /dev/null and b/public/template-media/HappyBirthday-16x9.png differ diff --git a/public/template-media/HappyBirthday-1x1.png b/public/template-media/HappyBirthday-1x1.png new file mode 100644 index 0000000..8f67249 Binary files /dev/null and b/public/template-media/HappyBirthday-1x1.png differ diff --git a/public/template-media/HappyBirthday-9x16.png b/public/template-media/HappyBirthday-9x16.png new file mode 100644 index 0000000..88476e5 Binary files /dev/null and b/public/template-media/HappyBirthday-9x16.png differ diff --git a/public/template-media/HappyBirthday.mp4 b/public/template-media/HappyBirthday.mp4 new file mode 100644 index 0000000..6240d39 Binary files /dev/null and b/public/template-media/HappyBirthday.mp4 differ diff --git a/public/template-media/Hero3D-16x9.png b/public/template-media/Hero3D-16x9.png new file mode 100644 index 0000000..fee9c7b Binary files /dev/null and b/public/template-media/Hero3D-16x9.png differ diff --git a/public/template-media/Hero3D-1x1.png b/public/template-media/Hero3D-1x1.png new file mode 100644 index 0000000..f74eeff Binary files /dev/null and b/public/template-media/Hero3D-1x1.png differ diff --git a/public/template-media/Hero3D-9x16.png b/public/template-media/Hero3D-9x16.png new file mode 100644 index 0000000..8d60886 Binary files /dev/null and b/public/template-media/Hero3D-9x16.png differ diff --git a/public/template-media/Hero3D.mp4 b/public/template-media/Hero3D.mp4 new file mode 100644 index 0000000..ff4068e Binary files /dev/null and b/public/template-media/Hero3D.mp4 differ diff --git a/public/template-media/InstaPromo-16x9.png b/public/template-media/InstaPromo-16x9.png new file mode 100644 index 0000000..e0ee8da Binary files /dev/null and b/public/template-media/InstaPromo-16x9.png differ diff --git a/public/template-media/InstaPromo-1x1.png b/public/template-media/InstaPromo-1x1.png new file mode 100644 index 0000000..7c86ef5 Binary files /dev/null and b/public/template-media/InstaPromo-1x1.png differ diff --git a/public/template-media/InstaPromo-9x16.png b/public/template-media/InstaPromo-9x16.png new file mode 100644 index 0000000..ce3febd Binary files /dev/null and b/public/template-media/InstaPromo-9x16.png differ diff --git a/public/template-media/InstaPromo.mp4 b/public/template-media/InstaPromo.mp4 new file mode 100644 index 0000000..bef9224 Binary files /dev/null and b/public/template-media/InstaPromo.mp4 differ diff --git a/public/template-media/LogoMotion-16x9.png b/public/template-media/LogoMotion-16x9.png new file mode 100644 index 0000000..a65883f Binary files /dev/null and b/public/template-media/LogoMotion-16x9.png differ diff --git a/public/template-media/LogoMotion-1x1.png b/public/template-media/LogoMotion-1x1.png new file mode 100644 index 0000000..be02b05 Binary files /dev/null and b/public/template-media/LogoMotion-1x1.png differ diff --git a/public/template-media/LogoMotion-9x16.png b/public/template-media/LogoMotion-9x16.png new file mode 100644 index 0000000..4fdade2 Binary files /dev/null and b/public/template-media/LogoMotion-9x16.png differ diff --git a/public/template-media/LogoMotion.mp4 b/public/template-media/LogoMotion.mp4 new file mode 100644 index 0000000..638e46d Binary files /dev/null and b/public/template-media/LogoMotion.mp4 differ diff --git a/public/template-media/Nowruz3D-16x9.png b/public/template-media/Nowruz3D-16x9.png new file mode 100644 index 0000000..f3b3d89 Binary files /dev/null and b/public/template-media/Nowruz3D-16x9.png differ diff --git a/public/template-media/Nowruz3D-1x1.png b/public/template-media/Nowruz3D-1x1.png new file mode 100644 index 0000000..5e04994 Binary files /dev/null and b/public/template-media/Nowruz3D-1x1.png differ diff --git a/public/template-media/Nowruz3D-9x16.png b/public/template-media/Nowruz3D-9x16.png new file mode 100644 index 0000000..1e3ce83 Binary files /dev/null and b/public/template-media/Nowruz3D-9x16.png differ diff --git a/public/template-media/Nowruz3D.mp4 b/public/template-media/Nowruz3D.mp4 new file mode 100644 index 0000000..a11c752 Binary files /dev/null and b/public/template-media/Nowruz3D.mp4 differ diff --git a/public/template-media/NowruzGreeting-16x9.png b/public/template-media/NowruzGreeting-16x9.png new file mode 100644 index 0000000..0965b09 Binary files /dev/null and b/public/template-media/NowruzGreeting-16x9.png differ diff --git a/public/template-media/NowruzGreeting-1x1.png b/public/template-media/NowruzGreeting-1x1.png new file mode 100644 index 0000000..a0dd0fd Binary files /dev/null and b/public/template-media/NowruzGreeting-1x1.png differ diff --git a/public/template-media/NowruzGreeting-9x16.png b/public/template-media/NowruzGreeting-9x16.png new file mode 100644 index 0000000..8bc4b9a Binary files /dev/null and b/public/template-media/NowruzGreeting-9x16.png differ diff --git a/public/template-media/NowruzGreeting.mp4 b/public/template-media/NowruzGreeting.mp4 new file mode 100644 index 0000000..cf156ea Binary files /dev/null and b/public/template-media/NowruzGreeting.mp4 differ diff --git a/public/template-media/Opener-16x9.png b/public/template-media/Opener-16x9.png new file mode 100644 index 0000000..7ba591a Binary files /dev/null and b/public/template-media/Opener-16x9.png differ diff --git a/public/template-media/Opener-1x1.png b/public/template-media/Opener-1x1.png new file mode 100644 index 0000000..3fa1678 Binary files /dev/null and b/public/template-media/Opener-1x1.png differ diff --git a/public/template-media/Opener-9x16.png b/public/template-media/Opener-9x16.png new file mode 100644 index 0000000..383af68 Binary files /dev/null and b/public/template-media/Opener-9x16.png differ diff --git a/public/template-media/Opener.mp4 b/public/template-media/Opener.mp4 new file mode 100644 index 0000000..7cc0f97 Binary files /dev/null and b/public/template-media/Opener.mp4 differ diff --git a/public/template-media/Promo3D-16x9.png b/public/template-media/Promo3D-16x9.png new file mode 100644 index 0000000..79fe964 Binary files /dev/null and b/public/template-media/Promo3D-16x9.png differ diff --git a/public/template-media/Promo3D-1x1.png b/public/template-media/Promo3D-1x1.png new file mode 100644 index 0000000..7f57c60 Binary files /dev/null and b/public/template-media/Promo3D-1x1.png differ diff --git a/public/template-media/Promo3D-9x16.png b/public/template-media/Promo3D-9x16.png new file mode 100644 index 0000000..0c661f9 Binary files /dev/null and b/public/template-media/Promo3D-9x16.png differ diff --git a/public/template-media/Promo3D.mp4 b/public/template-media/Promo3D.mp4 new file mode 100644 index 0000000..3c42d95 Binary files /dev/null and b/public/template-media/Promo3D.mp4 differ diff --git a/public/template-media/QuoteCard-16x9.png b/public/template-media/QuoteCard-16x9.png new file mode 100644 index 0000000..f01b472 Binary files /dev/null and b/public/template-media/QuoteCard-16x9.png differ diff --git a/public/template-media/QuoteCard-1x1.png b/public/template-media/QuoteCard-1x1.png new file mode 100644 index 0000000..0174eeb Binary files /dev/null and b/public/template-media/QuoteCard-1x1.png differ diff --git a/public/template-media/QuoteCard-9x16.png b/public/template-media/QuoteCard-9x16.png new file mode 100644 index 0000000..f748838 Binary files /dev/null and b/public/template-media/QuoteCard-9x16.png differ diff --git a/public/template-media/QuoteCard.mp4 b/public/template-media/QuoteCard.mp4 new file mode 100644 index 0000000..f42fc20 Binary files /dev/null and b/public/template-media/QuoteCard.mp4 differ diff --git a/public/template-media/SalePromo-16x9.png b/public/template-media/SalePromo-16x9.png new file mode 100644 index 0000000..b4adbb6 Binary files /dev/null and b/public/template-media/SalePromo-16x9.png differ diff --git a/public/template-media/SalePromo-1x1.png b/public/template-media/SalePromo-1x1.png new file mode 100644 index 0000000..d1ebd47 Binary files /dev/null and b/public/template-media/SalePromo-1x1.png differ diff --git a/public/template-media/SalePromo-9x16.png b/public/template-media/SalePromo-9x16.png new file mode 100644 index 0000000..542cbf9 Binary files /dev/null and b/public/template-media/SalePromo-9x16.png differ diff --git a/public/template-media/SalePromo.mp4 b/public/template-media/SalePromo.mp4 new file mode 100644 index 0000000..e5c98d3 Binary files /dev/null and b/public/template-media/SalePromo.mp4 differ diff --git a/public/template-media/Slideshow-16x9.png b/public/template-media/Slideshow-16x9.png new file mode 100644 index 0000000..135fca7 Binary files /dev/null and b/public/template-media/Slideshow-16x9.png differ diff --git a/public/template-media/Slideshow-1x1.png b/public/template-media/Slideshow-1x1.png new file mode 100644 index 0000000..4c5f81e Binary files /dev/null and b/public/template-media/Slideshow-1x1.png differ diff --git a/public/template-media/Slideshow-9x16.png b/public/template-media/Slideshow-9x16.png new file mode 100644 index 0000000..42d18f5 Binary files /dev/null and b/public/template-media/Slideshow-9x16.png differ diff --git a/public/template-media/Slideshow.mp4 b/public/template-media/Slideshow.mp4 new file mode 100644 index 0000000..fb3c36e Binary files /dev/null and b/public/template-media/Slideshow.mp4 differ diff --git a/public/template-media/YouTubeIntro-16x9.png b/public/template-media/YouTubeIntro-16x9.png new file mode 100644 index 0000000..7d1c0fb Binary files /dev/null and b/public/template-media/YouTubeIntro-16x9.png differ diff --git a/public/template-media/YouTubeIntro-1x1.png b/public/template-media/YouTubeIntro-1x1.png new file mode 100644 index 0000000..5e2d574 Binary files /dev/null and b/public/template-media/YouTubeIntro-1x1.png differ diff --git a/public/template-media/YouTubeIntro-9x16.png b/public/template-media/YouTubeIntro-9x16.png new file mode 100644 index 0000000..782286c Binary files /dev/null and b/public/template-media/YouTubeIntro-9x16.png differ diff --git a/public/template-media/YouTubeIntro.mp4 b/public/template-media/YouTubeIntro.mp4 new file mode 100644 index 0000000..e399731 Binary files /dev/null and b/public/template-media/YouTubeIntro.mp4 differ diff --git a/scripts/seed_remotion_templates.py b/scripts/seed_remotion_templates.py new file mode 100644 index 0000000..441d56b --- /dev/null +++ b/scripts/seed_remotion_templates.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Generates SQL to seed the 10 branded Remotion templates into content.* so they +# appear on the site. Each template -> 1 container + 3 aspect projects, each with +# a scene, Persian text content-elements (bindable -> Remotion props) and shared +# colours (bindable -> colour props) + a per-scene colour-swatch SVG. +# +# Usage: python scripts/seed_remotion_templates.py | docker exec -i fr2-postgres psql -U postgres -d flatrender +import uuid + +# Assets are served from the Next app's public/ folder (relative URLs), so this +# works regardless of MinIO/object-store availability. +MINIO = "" +NS = uuid.UUID("11111111-2222-3333-4444-555555555555") +def uid(s): return str(uuid.uuid5(NS, s)) +def q(s): return "'" + str(s).replace("'", "''") + "'" + +ASPECTS = [("16x9", 1920, 1080, "16:9"), ("1x1", 1080, 1080, "1:1"), ("9x16", 1080, 1920, "9:16")] +CTITLES = {"accentColor": "رنگ اصلی", "secondaryColor": "رنگ دوم", "backgroundColor": "پس‌زمینه", "textColor": "رنگ متن"} + +# id, slug, name(fa), desc(fa), dur, [(textKey,title,value)], (accent,secondary,bg) +T = [ + ("LogoMotion","fr-logo-motion","موشن لوگو","نمایش حرفه‌ای لوگو و نام برند با درخشش و حرکت",5, + [("brandText","متن لوگو","فلت‌رندر"),("tagline","شعار","موشن، ساده و حرفه‌ای")],("#3ba7ff","#a855f7","#04060f")), + ("Opener","fr-opener","تیتراژ آغازین","شروع سینمایی برای ویدیو با عنوان و زیرعنوان",5, + [("kicker","پیش‌متن","تقدیم می‌کند"),("title","عنوان","یک شروع تازه"),("subtitle","زیرعنوان","داستان شما از همین‌جا آغاز می‌شود")],("#22d3ee","#6366f1","#0a0a12")), + ("InstaPromo","fr-insta-promo","تبلیغ پیج اینستاگرام","معرفی و تبلیغ صفحهٔ اینستاگرام با دعوت به فالو",5, + [("handle","آیدی پیج","@flatrender"),("headline","عنوان","پیج ما را دنبال کنید"),("subtext","توضیح","هر روز محتوای تازه و الهام‌بخش"),("cta","دکمه","فالو کنید")],("#fb7185","#f59e0b","#140a12")), + ("YouTubeIntro","fr-youtube-intro","اینترو کانال یوتیوب","اینترو حرفه‌ای کانال یوتیوب با دکمهٔ سابسکرایب",5, + [("channelName","نام کانال","کانال فلت‌رندر"),("subtitle","زیرعنوان","آموزش، ترفند و انگیزه"),("cta","دکمه","سابسکرایب کنید")],("#ff4d4d","#a855f7","#0c0810")), + ("Slideshow","fr-slideshow","اسلایدشو","نمایش پشت‌سرهم چند پیام یا ویژگی به‌صورت اسلاید",9, + [("title","عنوان","چرا فلت‌رندر؟"),("slide1","اسلاید ۱","ساخت ویدیو در چند دقیقه"),("slide2","اسلاید ۲","بدون نیاز به دانش فنی"),("slide3","اسلاید ۳","خروجی با کیفیت حرفه‌ای")],("#34d399","#3b82f6","#060b0a")), + ("HappyBirthday","fr-happy-birthday","تولدت مبارک","کارت تبریک تولد با کاغذرنگی و نام شخص",6, + [("greeting","تبریک","تولدت مبارک"),("name","نام","سارا"),("message","پیام","بهترین‌ها را برایت آرزومندیم 🎉")],("#fb7185","#fde047","#140a18")), + ("SalePromo","fr-sale-promo","فروش ویژه","بنر تبلیغاتی فروش و تخفیف با دعوت به خرید",5, + [("badge","نشان تخفیف","۵۰٪ تخفیف"),("headline","عنوان","فروش ویژهٔ پایان فصل"),("subtext","توضیح","فقط تا پایان همین هفته"),("cta","دکمه","همین حالا خرید کنید")],("#f59e0b","#fb7185","#120a08")), + ("QuoteCard","fr-quote-card","کارت نقل‌قول","نمایش جملهٔ انگیزشی یا نقل‌قول با نام گوینده",6, + [("quote","نقل‌قول","موفقیت، مجموع تلاش‌های کوچکِ هر روز است."),("author","گوینده","فلت‌رندر")],("#22d3ee","#6366f1","#0a0a12")), + ("EventInvite","fr-event-invite","دعوت‌نامهٔ رویداد","دعوت‌نامهٔ شیک برای رویداد با تاریخ و مکان",6, + [("kicker","پیش‌متن","دعوت‌نامه"),("eventTitle","عنوان رویداد","همایش سالانهٔ نوآوری"),("date","تاریخ","۱۵ مهر ۱۴۰۳"),("location","مکان","تهران، سالن همایش‌ها"),("cta","دکمه","ثبت‌نام کنید")],("#a855f7","#3ba7ff","#0a0814")), + ("Countdown","fr-countdown","شمارش معکوس","شمارش معکوس هیجان‌انگیز برای شروع یک رویداد",8, + [("title","عنوان","شروع رویداد تا"),("startNumber","عدد شروع","5"),("goText","متن پایان","شروع!"),("subtitle","زیرعنوان","آماده‌اید؟")],("#3ba7ff","#22d3ee","#04060f")), + ("GlitterReveal","fr-glitter-reveal","نمایش لوگو با غبار درخشان","نمایش جادویی لوگو با ذرات درخشان؛ لوگو و متن قابل ویرایش",6, + [("brandText","نام برند","فلت‌رندر"),("tagline","شعار","موشن، ساده و حرفه‌ای")],("#3ba7ff","#a855f7","#05040e")), + ("NowruzGreeting","fr-nowruz","تبریک نوروز","صحنهٔ بهاری نوروز با شخصیت‌های متحرک؛ حاجی‌فیروز، ماهی قرمز و سبزه",8, + [("greeting","متن تبریک","نوروز مبارک"),("subtitle","زیرعنوان","سال نو پیروز و شادمان"),("message","پیام / سال","۱۴۰۶")],("#f5b942","#e23b3b","#1fb6b0")), + ("Hero3D","fr-hero-3d","نمایش سه‌بعدی برند","نمایش حرفه‌ای و سه‌بعدی لوگو و برند با نورپردازی و جلوه‌های واقعی",6, + [("brandText","نام برند","فلت‌رندر"),("tagline","شعار","موشن، ساده و حرفه‌ای")],("#3ba7ff","#a855f7","#04060f")), + ("Nowruz3D","fr-nowruz-3d","تبریک نوروز سه‌بعدی","صحنهٔ سه‌بعدی نوروز با حاجی‌فیروز، سفرهٔ هفت‌سین و نورپردازی سینمایی",7, + [("greeting","متن تبریک","نوروز مبارک"),("subtitle","زیرعنوان","سال نو پیروز و شادمان"),("message","پیام / سال","۱۴۰۶")],("#f5c542","#e23b3b","#1a1228")), + ("Birthday3D","fr-birthday-3d","تولد سه‌بعدی","صحنهٔ سه‌بعدی تولد با کیک و شمع‌های روشن، بادکنک و کاغذرنگی",6, + [("greeting","تبریک","تولدت مبارک"),("name","نام","سارا"),("message","پیام","بهترین‌ها را برایت آرزومندیم 🎉")],("#fb7185","#a855f7","#1a1226")), + ("Promo3D","fr-promo-3d","فروش ویژه سه‌بعدی","تبلیغ سه‌بعدی فروش و تخفیف با جعبه‌های هدیه و نورپردازی سینمایی",6, + [("badge","نشان تخفیف","۵۰٪ تخفیف"),("headline","عنوان","فروش ویژهٔ پایان فصل"),("subtext","توضیح","فقط تا پایان همین هفته"),("cta","دکمه","همین حالا خرید کنید")],("#f59e0b","#fb7185","#140e1f")), +] + +# Optional Media (image) content elements per template — these surface in the +# studio as upload/replace fields. key = the Remotion prop the image binds to. +MEDIA = { + "GlitterReveal": [("logoUrl", "لوگو (تصویر دلخواه)")], +} + +def swatch_svg(colors): + rects = "".join(f'' for i, c in enumerate(colors)) + return f'{rects}' + +def icon_svg(hex): + return f'' + +out = [] +out.append("BEGIN;") +slugs = ",".join(q(t[1]) for t in T) +out.append(f"DELETE FROM content.project_containers WHERE slug IN ({slugs});") + +for idx, (tid, slug, name, desc, dur, texts, (accent, sec, bg)) in enumerate(T): + cid = uid("c-" + tid) + thumb16 = f"{MINIO}/template-media/{tid}-16x9.png" + preview = f"{MINIO}/template-media/{tid}.mp4" + colors = [("accentColor", accent), ("secondaryColor", sec), ("backgroundColor", bg), ("textColor", "#ffffff")] + out.append( + "INSERT INTO content.project_containers (id,tenant_id,slug,name,description,image,demo,full_demo,mini_demo," + "is_published,is_premium,is_mockup,primary_mode,sort) VALUES (" + f"{q(cid)},NULL,{q(slug)},{q(name)},{q(desc)},{q(thumb16)},{q(preview)},{q(preview)},{q(preview)}," + f"TRUE,FALSE,FALSE,'FLEXIBLE',{idx});") + for (asp, w, h, aspstr) in ASPECTS: + pid = uid(f"p-{tid}-{asp}") + sid = uid(f"s-{tid}-{asp}") + thumb = f"{MINIO}/template-media/{tid}-{asp}.png" + out.append( + "INSERT INTO content.projects (id,container_id,name,image,full_demo,original_width,original_height,aspect," + "project_duration_sec,free_fps,choose_mode,resolution,render_engine,render_remotion_comp,is_published,sort) VALUES (" + f"{q(pid)},{q(cid)},{q(aspstr)},{q(thumb)},{q(preview)},{w},{h},{q(aspstr)}," + f"{dur},30,'FLEXIBLE','FullHD','Remotion',{q(tid+'-'+asp)},TRUE,0);") + out.append( + "INSERT INTO content.scenes (id,project_id,key,title,scene_color_svg,default_duration_sec,sort) VALUES (" + f"{q(sid)},{q(pid)},'c1','صحنه ۱',{q(swatch_svg([accent,sec,bg,'#ffffff']))},{dur},0);") + for pos, (k, title, val) in enumerate(texts): + out.append( + "INSERT INTO content.scene_content_elements (id,scene_id,key,title,type,default_value,position_in_container,direction_layer_value) VALUES (" + f"{q(uid(f'ce-{tid}-{asp}-{k}'))},{q(sid)},{q(k)},{q(title)},'Text',{q(val)},{pos},1);") + for mpos, (k, title) in enumerate(MEDIA.get(tid, [])): + out.append( + "INSERT INTO content.scene_content_elements (id,scene_id,key,title,type,default_value,position_in_container,direction_layer_value) VALUES (" + f"{q(uid(f'ce-{tid}-{asp}-{k}'))},{q(sid)},{q(k)},{q(title)},'Media','',{len(texts)+mpos},0);") + for si, (k, hexv) in enumerate(colors): + out.append( + "INSERT INTO content.shared_colors (id,project_id,element_key,title,icon,attr_value,default_color,sort) VALUES (" + f"{q(uid(f'sc-{tid}-{asp}-{k}'))},{q(pid)},{q(k)},{q(CTITLES[k])},{q(icon_svg(hexv))},'fill',{q(hexv)},{si});") + +out.append("COMMIT;") +out.append("SELECT count(*) AS containers FROM content.project_containers WHERE slug LIKE 'fr-%';") +print("\n".join(out)) diff --git a/services/content/FlatRender.ContentSvc/Application/Services/TemplateService.cs b/services/content/FlatRender.ContentSvc/Application/Services/TemplateService.cs index dec7c3b..6345704 100644 --- a/services/content/FlatRender.ContentSvc/Application/Services/TemplateService.cs +++ b/services/content/FlatRender.ContentSvc/Application/Services/TemplateService.cs @@ -176,6 +176,8 @@ public class TemplateService(ContentDbContext db) ProjectDurationSec = req.ProjectDurationSec, MinDurationSec = req.MinDurationSec, MaxDurationSec = req.MaxDurationSec, FreeFps = req.FreeFps, ChooseMode = chooseMode, Resolution = resolution, VipFactor = req.VipFactor, RenderAepComp = req.RenderAepComp, + RenderEngine = string.IsNullOrWhiteSpace(req.RenderEngine) ? "AfterEffects" : req.RenderEngine, + RenderRemotionComp = req.RenderRemotionComp, IsPublished = req.IsPublished, Sort = req.Sort }; @@ -225,6 +227,7 @@ public class TemplateService(ContentDbContext db) ProjectDurationSec = src.ProjectDurationSec, MinDurationSec = src.MinDurationSec, MaxDurationSec = src.MaxDurationSec, FreeFps = src.FreeFps, ChooseMode = src.ChooseMode, Resolution = resolution, VipFactor = src.VipFactor, RenderAepComp = src.RenderAepComp, + RenderEngine = src.RenderEngine, RenderRemotionComp = src.RenderRemotionComp, SharedLayerImage = src.SharedLayerImage, SharedColorsSvg = src.SharedColorsSvg, SharedColorPresetsSvg = src.SharedColorPresetsSvg, IsPublished = false, Sort = src.Sort, @@ -359,6 +362,8 @@ public class TemplateService(ContentDbContext db) project.MinDurationSec = req.MinDurationSec; project.MaxDurationSec = req.MaxDurationSec; project.FreeFps = req.FreeFps; project.ChooseMode = chooseMode; project.Resolution = resolution; project.VipFactor = req.VipFactor; project.RenderAepComp = req.RenderAepComp; + if (!string.IsNullOrWhiteSpace(req.RenderEngine)) project.RenderEngine = req.RenderEngine; + if (req.RenderRemotionComp != null) project.RenderRemotionComp = req.RenderRemotionComp; project.SharedLayerImage = req.SharedLayerImage; project.SharedColorsSvg = req.SharedColorsSvg; project.SharedColorPresetsSvg = req.SharedColorPresetsSvg; project.IsPublished = req.IsPublished; project.Sort = req.Sort; @@ -402,6 +407,8 @@ public class TemplateService(ContentDbContext db) if (req.Image != null) project.Image = req.Image; if (req.FullDemo != null) project.FullDemo = req.FullDemo; if (req.SharedColorsSvg != null) project.SharedColorsSvg = req.SharedColorsSvg; + if (!string.IsNullOrWhiteSpace(req.RenderEngine)) project.RenderEngine = req.RenderEngine; + if (req.RenderRemotionComp != null) project.RenderRemotionComp = req.RenderRemotionComp; project.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); @@ -472,7 +479,8 @@ public class TemplateService(ContentDbContext db) p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec, p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(), p.IsPublished, p.Sort, - p.AepFileUrl, p.AepFileSizeBytes, p.RenderAepComp + p.AepFileUrl, p.AepFileSizeBytes, p.RenderAepComp, + p.RenderEngine, p.RenderRemotionComp ); /// Browse/search all projects (template items) across containers. @@ -489,7 +497,8 @@ public class TemplateService(ContentDbContext db) .Skip((page - 1) * pageSize).Take(pageSize) .Select(p => new ProjectListItemResponse( p.Id, p.ContainerId, p.Container.Name, p.Container.Slug, p.Name, p.Image, - p.Aspect, p.Resolution.ToString(), p.AepFileUrl, p.RenderAepComp, p.IsPublished, p.Sort)) + p.Aspect, p.Resolution.ToString(), p.AepFileUrl, p.RenderAepComp, p.IsPublished, p.Sort, + p.RenderEngine, p.RenderRemotionComp)) .ToListAsync(); return new PagedResponse(items, new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))); @@ -529,6 +538,8 @@ public class TemplateService(ContentDbContext db) if (req.AepFileMd5 != null) project.AepFileMd5 = req.AepFileMd5; if (req.AepFileSizeBytes.HasValue) project.AepFileSizeBytes = req.AepFileSizeBytes; if (!string.IsNullOrWhiteSpace(req.RenderAepComp)) project.RenderAepComp = req.RenderAepComp; + if (!string.IsNullOrWhiteSpace(req.RenderEngine)) project.RenderEngine = req.RenderEngine; + if (req.RenderRemotionComp != null) project.RenderRemotionComp = req.RenderRemotionComp; if (req.Folder != null) project.Folder = req.Folder; project.AepUploadedAt = DateTime.UtcNow; project.UpdatedAt = DateTime.UtcNow; @@ -541,6 +552,7 @@ public class TemplateService(ContentDbContext db) p.OriginalWidth, p.OriginalHeight, p.Aspect, p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec, p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(), p.VipFactor, p.RenderAepComp, + p.RenderEngine, p.RenderRemotionComp, p.SharedLayerImage, p.IsPublished, p.Sort, p.Scenes.Select(MapScene).ToList(), p.SharedColors.Select(sc => new SharedColorResponse(sc.Id, sc.ElementKey, sc.Title, sc.Icon, sc.AttrValue.ToString(), sc.DefaultColor, sc.Sort)).ToList(), diff --git a/services/content/FlatRender.ContentSvc/Domain/Entities/Template.cs b/services/content/FlatRender.ContentSvc/Domain/Entities/Template.cs index b35cdc0..ba9ef69 100644 --- a/services/content/FlatRender.ContentSvc/Domain/Entities/Template.cs +++ b/services/content/FlatRender.ContentSvc/Domain/Entities/Template.cs @@ -95,6 +95,11 @@ public class Project public decimal VipFactor { get; set; } = 1.0m; public string RenderAepComp { get; set; } = "flatrender"; + /// Render engine for this template: "AfterEffects" (default) or "Remotion". + public string RenderEngine { get; set; } = "AfterEffects"; + /// For Remotion templates, the composition id to render (e.g. "KineticQuote"). + public string? RenderRemotionComp { get; set; } + public string? SharedLayerImage { get; set; } public string? SharedColorsSvg { get; set; } public string? SharedColorPresetsSvg { get; set; } diff --git a/services/content/FlatRender.ContentSvc/Infrastructure/Data/ContentDbContext.cs b/services/content/FlatRender.ContentSvc/Infrastructure/Data/ContentDbContext.cs index 1dbad4d..a0634e2 100644 --- a/services/content/FlatRender.ContentSvc/Infrastructure/Data/ContentDbContext.cs +++ b/services/content/FlatRender.ContentSvc/Infrastructure/Data/ContentDbContext.cs @@ -307,6 +307,8 @@ public class ContentDbContext(DbContextOptions options) : DbCo e.Property(x => x.Resolution).HasColumnName("resolution"); e.Property(x => x.VipFactor).HasColumnName("vip_factor"); e.Property(x => x.RenderAepComp).HasColumnName("render_aep_comp"); + e.Property(x => x.RenderEngine).HasColumnName("render_engine"); + e.Property(x => x.RenderRemotionComp).HasColumnName("render_remotion_comp"); e.Property(x => x.SharedLayerImage).HasColumnName("shared_layer_image"); e.Property(x => x.SharedColorsSvg).HasColumnName("shared_colors_svg"); e.Property(x => x.SharedColorPresetsSvg).HasColumnName("shared_color_presets_svg"); diff --git a/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs b/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs index 9d6ec2b..38b0ff4 100644 --- a/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs +++ b/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs @@ -213,7 +213,9 @@ public record CreateProjectRequest( decimal VipFactor, string RenderAepComp, bool IsPublished, - int Sort + int Sort, + string? RenderEngine = null, + string? RenderRemotionComp = null ); public record UpdateProjectRequest( @@ -239,7 +241,9 @@ public record UpdateProjectRequest( string? SharedColorsSvg, string? SharedColorPresetsSvg, bool IsPublished, - int Sort + int Sort, + string? RenderEngine = null, + string? RenderRemotionComp = null ); // Partial update — only non-null fields are applied, so editing an aspect/resolution @@ -260,7 +264,9 @@ public record SetAepRequest( string? AepFileMd5, long? AepFileSizeBytes, string? RenderAepComp, - string? Folder + string? Folder, + string? RenderEngine = null, + string? RenderRemotionComp = null ); public record PatchProjectRequest( @@ -279,7 +285,9 @@ public record PatchProjectRequest( int? Sort, string? Image, string? FullDemo, - string? SharedColorsSvg + string? SharedColorsSvg, + string? RenderEngine = null, + string? RenderRemotionComp = null ); // ── CMS ────────────────────────────────────────────────────────────────────── diff --git a/services/content/FlatRender.ContentSvc/Models/Responses/Responses.cs b/services/content/FlatRender.ContentSvc/Models/Responses/Responses.cs index 5e8418c..bf961c9 100644 --- a/services/content/FlatRender.ContentSvc/Models/Responses/Responses.cs +++ b/services/content/FlatRender.ContentSvc/Models/Responses/Responses.cs @@ -131,7 +131,9 @@ public record ProjectResponse( int Sort, string? AepFileUrl, long? AepFileSizeBytes, - string RenderAepComp + string RenderAepComp, + string RenderEngine, + string? RenderRemotionComp ); public record ProjectListItemResponse( @@ -146,7 +148,9 @@ public record ProjectListItemResponse( string? AepFileUrl, string RenderAepComp, bool IsPublished, - int Sort + int Sort, + string RenderEngine, + string? RenderRemotionComp ); public record ProjectAssetResponse(Guid Id, Guid ProjectId, string Name, string Kind, string Url, long? SizeBytes, int Sort); @@ -171,6 +175,8 @@ public record ProjectDetailResponse( string Resolution, decimal VipFactor, string RenderAepComp, + string RenderEngine, + string? RenderRemotionComp, string? SharedLayerImage, bool IsPublished, int Sort, diff --git a/services/node-agent/cmd/agent/main.go b/services/node-agent/cmd/agent/main.go index 4dd7fce..cf77eb5 100644 --- a/services/node-agent/cmd/agent/main.go +++ b/services/node-agent/cmd/agent/main.go @@ -515,6 +515,13 @@ func (a *Agent) runJob(ctx context.Context, job *client.ClaimedJob) { binds = append(binds, runner.Binding{Key: b.Key, Type: b.Type, Value: b.Value}) } + // Default empty engine to AfterEffects for backwards-compat with older + // orchestrators that don't send the field yet. + engine := job.Engine + if engine == "" { + engine = runner.EngineAfterEffects + } + rJob := &runner.Job{ JobID: job.JobID, SavedProjectID: job.SavedProjectID, @@ -523,6 +530,8 @@ func (a *Agent) runJob(ctx context.Context, job *client.ClaimedJob) { FrameRate: job.FrameRate, HasMusic: job.HasMusic, HasVoiceover: job.HasVoiceover, + Engine: engine, + RemotionDir: a.cfg.RemotionProjectDir, AEPFilePath: aepPath, CompName: job.CompName, AfterFxPath: a.cfg.AfterFxPath, diff --git a/services/node-agent/internal/client/client.go b/services/node-agent/internal/client/client.go index 0a31b3d..e65d6ba 100644 --- a/services/node-agent/internal/client/client.go +++ b/services/node-agent/internal/client/client.go @@ -152,6 +152,9 @@ type ClaimedJob struct { FrameRate int `json:"frame_rate"` HasMusic bool `json:"has_music"` HasVoiceover bool `json:"has_voiceover"` + // Engine selects the render engine: "AfterEffects" (default) or "Remotion". + // For Remotion jobs CompName is the composition id and AEPDownloadURL is empty. + Engine string `json:"engine,omitempty"` // AEPDownloadURL is a presigned MinIO GET URL for the .aep template file // (or .zip bundle). Empty when the template has not been uploaded yet — triggers mock render. AEPDownloadURL string `json:"aep_download_url,omitempty"` diff --git a/services/node-agent/internal/config/config.go b/services/node-agent/internal/config/config.go index 2518eda..aaae434 100644 --- a/services/node-agent/internal/config/config.go +++ b/services/node-agent/internal/config/config.go @@ -85,6 +85,10 @@ type Config struct { // WorkDir is the scratch directory for render temp files and AE project copies. WorkDir string + // RemotionProjectDir is the Remotion project root (package.json + src/index.ts) + // used by the code-based render engine. Empty disables Remotion jobs on this node. + RemotionProjectDir string + // HeartbeatIntervalSec is how often the agent sends a heartbeat to the orchestrator. HeartbeatIntervalSec int @@ -115,6 +119,7 @@ func Load() (*Config, error) { AEPath: getEnv("AE_PATH", ""), AfterFxPath: getEnv("AFTERFX_PATH", ""), WorkDir: getEnv("WORK_DIR", os.TempDir()), + RemotionProjectDir: getEnv("REMOTION_PROJECT_DIR", ""), AgentVersion: getEnv("AGENT_VERSION", "0.1.0"), AEVersion: getEnv("AE_VERSION", "2024"), HeartbeatIntervalSec: getInt("HEARTBEAT_INTERVAL_SEC", 5), diff --git a/services/node-agent/internal/runner/remotion.go b/services/node-agent/internal/runner/remotion.go new file mode 100644 index 0000000..774d822 --- /dev/null +++ b/services/node-agent/internal/runner/remotion.go @@ -0,0 +1,248 @@ +// Remotion render engine. +// +// FlatRender supports two template engines that both produce a web-playable MP4: +// +// - AfterEffects (EngineAfterEffects) — aerender.exe renders a .aep template, +// bindings are written into the project first; see runner.go / binder.go. +// - Remotion (EngineRemotion) — a code-based React/Remotion composition +// is rendered with `npx remotion render`; bindings become --props; this file. +// +// The two engines are interchangeable from the job loop's point of view: Run() +// dispatches on Job.Engine and each returns the path to an MP4 on disk. +package runner + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync/atomic" + "time" +) + +// Engine identifiers. These mirror the values the orchestrator stores per +// template (content.templates.render_engine) and sends on the claimed job. +const ( + EngineAfterEffects = "AfterEffects" + EngineRemotion = "Remotion" +) + +// Remotion prints "Rendered /" while drawing frames and +// "Stitched /" while muxing them into the MP4. We parse both to +// build a real percentage. +var ( + reRemRendered = regexp.MustCompile(`Rendered\s+(\d+)/(\d+)`) + reRemStitched = regexp.MustCompile(`Stitched\s+(\d+)/(\d+)`) +) + +// npxCmd returns the platform-appropriate npx launcher. +func npxCmd() string { + if runtime.GOOS == "windows" { + return "npx.cmd" + } + return "npx" +} + +// remotionProps maps the user's bindings into a Remotion props JSON object. +// For code-based templates the binding Key is the composition's schema field +// (logoText, accentColor, …) and Value is the user's edited string. Anything the +// user didn't touch falls back to the composition's defaultProps. +func remotionProps(job *Job) (string, error) { + props := make(map[string]string, len(job.Bindings)) + for _, b := range job.Bindings { + if b.Key == "" { + continue + } + props[b.Key] = b.Value + } + data, err := json.Marshal(props) + if err != nil { + return "", err + } + return string(data), nil +} + +// crlfSplit is a bufio.SplitFunc that breaks on either \n or \r so we capture +// each progress-bar repaint (Remotion redraws the bar with \r, not \n). +func crlfSplit(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + for i, b := range data { + if b == '\n' || b == '\r' { + return i + 1, data[:i], nil + } + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil // request more data +} + +// RunRemotion renders a code-based (Remotion) template to MP4. +// +// - remotionDir is the Remotion project root (has package.json + src/index.ts). +// - job.CompName is the Remotion composition id (e.g. "KineticQuote"). +// - job.Bindings become --props. +// - job.Resolution selects an output height tier (free=360p … 4k). +// +// Returns the path to the rendered MP4. Progress + periodic previews are streamed +// through the same callbacks the AE engine uses, so the UI is engine-agnostic. +func RunRemotion(ctx context.Context, remotionDir string, job *Job, outputPath string, onProgress ProgressFn, onPreview PreviewFn) (string, error) { + if remotionDir == "" { + return "", fmt.Errorf("remotion project dir not set (REMOTION_PROJECT_DIR)") + } + if job.CompName == "" { + return "", fmt.Errorf("remotion render requires a composition id (CompName)") + } + if st, err := os.Stat(remotionDir); err != nil || !st.IsDir() { + return "", fmt.Errorf("remotion project dir not found: %s", remotionDir) + } + + propsJSON, err := remotionProps(job) + if err != nil { + return "", fmt.Errorf("build props: %w", err) + } + + // Render at the composition's native resolution, then downscale to the quality + // tier with ffmpeg (scale=-2:h preserves aspect). Remotion's --height flag + // overrides height but keeps the native width, which squishes non-matching + // aspect ratios — so we deliberately scale in the same ffmpeg post-step the AE + // engine uses. This also keeps one place to stamp the free-tier watermark later. + nativePath := strings.TrimSuffix(outputPath, filepath.Ext(outputPath)) + ".native.mp4" + entry := filepath.Join("src", "index.ts") + args := []string{ + "remotion", "render", entry, job.CompName, nativePath, + "--props=" + propsJSON, + "--log=info", + } + + log.Printf("[remotion] job %s → comp %q, props %s (cwd=%s)", job.JobID, job.CompName, propsJSON, remotionDir) + cmd := exec.CommandContext(ctx, npxCmd(), args...) + cmd.Dir = remotionDir + + // Merge stdout+stderr into one pipe — Remotion writes the progress bar to + // stderr and structured logs to stdout; we want both. + pr, pw := io.Pipe() + cmd.Stdout = pw + cmd.Stderr = pw + + var curFrame, totalFrames, stitched, totalStitch int64 + var phase atomic.Int32 // 0=bundling 1=rendering 2=stitching + go func() { + sc := bufio.NewScanner(pr) + sc.Buffer(make([]byte, 64*1024), 1024*1024) + sc.Split(crlfSplit) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" { + continue + } + _, _ = io.WriteString(os.Stdout, "[remotion] "+line+"\n") + if m := reRemRendered.FindStringSubmatch(line); m != nil { + cur, _ := strconv.ParseInt(m[1], 10, 64) + tot, _ := strconv.ParseInt(m[2], 10, 64) + atomic.StoreInt64(&curFrame, cur) + atomic.StoreInt64(&totalFrames, tot) + phase.Store(1) + } + if m := reRemStitched.FindStringSubmatch(line); m != nil { + cur, _ := strconv.ParseInt(m[1], 10, 64) + tot, _ := strconv.ParseInt(m[2], 10, 64) + atomic.StoreInt64(&stitched, cur) + atomic.StoreInt64(&totalStitch, tot) + phase.Store(2) + } + } + }() + + if err := cmd.Start(); err != nil { + _ = pw.Close() + return "", fmt.Errorf("start remotion: %w", err) + } + + done := make(chan error, 1) + go func() { + werr := cmd.Wait() + _ = pw.Close() // unblock the scanner goroutine + done <- werr + }() + + _ = onProgress(ctx, 4, "در حال آماده‌سازی قالب…") // "Preparing template…" + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + lastPreview := time.Time{} + + for { + select { + case werr := <-done: + if werr != nil { + return "", fmt.Errorf("remotion render exit: %w", werr) + } + if st, serr := os.Stat(nativePath); serr != nil || st.Size() == 0 { + return "", fmt.Errorf("remotion finished but produced no output at %s", nativePath) + } + // Downscale to the quality tier (aspect-preserving). When ffmpeg is + // missing or the tier is unknown, ship the native render unchanged. + h := resolutionHeight(job.Resolution) + if h > 0 && ffmpegPath() != "" { + _ = onProgress(ctx, 96, "در حال بهینه‌سازی کیفیت…") // "Optimizing quality…" + mp4, terr := transcodeToMP4(ctx, nativePath, outputPath, h) + if terr != nil { + log.Printf("[remotion] tier transcode failed (%v) — shipping native render", terr) + _ = onProgress(ctx, 98, "اتمام رندر") + return nativePath, nil + } + _ = os.Remove(nativePath) + _ = onProgress(ctx, 98, "اتمام رندر") + return mp4, nil + } + _ = onProgress(ctx, 98, "اتمام رندر") + return nativePath, nil + case <-ticker.C: + pct, msg := remotionProgress(phase.Load(), + atomic.LoadInt64(&curFrame), atomic.LoadInt64(&totalFrames), + atomic.LoadInt64(&stitched), atomic.LoadInt64(&totalStitch)) + _ = onProgress(ctx, pct, msg) + if onPreview != nil && time.Since(lastPreview) >= 8*time.Second { + lastPreview = time.Now() + if perr := onPreview(ctx, GeneratePreviewB64(pct, job.Quality, job.Resolution)); perr != nil { + log.Printf("[remotion] preview push error: %v", perr) + } + } + case <-ctx.Done(): + _ = cmd.Process.Kill() + return "", ctx.Err() + } + } +} + +// remotionProgress maps the render phase + frame counts to a 4–96 percentage +// (leaving headroom for the orchestrator's upload step) plus a Persian message. +func remotionProgress(phase int32, cur, total, stch, stchTotal int64) (int, string) { + switch phase { + case 2: // stitching → 70..96 + if stchTotal > 0 { + frac := float64(stch) / float64(stchTotal) + return 70 + int(frac*26), fmt.Sprintf("در حال ساخت ویدیو… %d از %d", stch, stchTotal) + } + return 70, "در حال ساخت ویدیو…" + case 1: // rendering frames → 8..70 + if total > 0 { + frac := float64(cur) / float64(total) + return 8 + int(frac*62), fmt.Sprintf("در حال رندر… فریم %d از %d", cur, total) + } + return 8, "در حال رندر…" + default: // bundling + return 5, "در حال کامپایل قالب…" + } +} diff --git a/services/node-agent/internal/runner/remotion_test.go b/services/node-agent/internal/runner/remotion_test.go new file mode 100644 index 0000000..9b04ebb --- /dev/null +++ b/services/node-agent/internal/runner/remotion_test.go @@ -0,0 +1,163 @@ +package runner + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + "time" +) + +// remotionProjectDir resolves the repo's services/remotion directory relative to +// this test package (services/node-agent/internal/runner), or skips the test when +// it (or npx) is unavailable — keeps the test green on CI nodes without the +// Remotion project checked out. +func remotionProjectDir(t *testing.T) string { + t.Helper() + if v := os.Getenv("REMOTION_PROJECT_DIR"); v != "" { + return v + } + dir, err := filepath.Abs(filepath.Join("..", "..", "..", "remotion")) + if err != nil { + t.Fatalf("abs: %v", err) + } + if _, err := os.Stat(filepath.Join(dir, "package.json")); err != nil { + t.Skipf("remotion project not found at %s (skipping)", dir) + } + return dir +} + +func TestRemotionProps(t *testing.T) { + job := &Job{Bindings: []Binding{ + {Key: "logoText", Value: "HELLO"}, + {Key: "accentColor", Value: "#22d3ee"}, + {Key: "", Value: "ignored"}, // empty keys are dropped + }} + got, err := remotionProps(job) + if err != nil { + t.Fatalf("remotionProps: %v", err) + } + want := `{"accentColor":"#22d3ee","logoText":"HELLO"}` + if got != want { + t.Fatalf("props = %s, want %s", got, want) + } +} + +func TestRemotionProgress(t *testing.T) { + cases := []struct { + phase int32 + cur, total, stch, stchTot int64 + wantMin, wantMax int + }{ + {0, 0, 0, 0, 0, 5, 5}, // bundling + {1, 90, 180, 0, 0, 30, 45}, // half the frames rendered + {2, 90, 180, 90, 180, 80, 90}, // half stitched + } + for _, c := range cases { + pct, _ := remotionProgress(c.phase, c.cur, c.total, c.stch, c.stchTot) + if pct < c.wantMin || pct > c.wantMax { + t.Errorf("phase %d: pct %d not in [%d,%d]", c.phase, pct, c.wantMin, c.wantMax) + } + } +} + +// TestRunRemotion_EndToEnd renders a real composition through the engine and +// asserts an MP4 lands on disk. Slow (spawns Chrome) — run with `go test -run +// RunRemotion -timeout 6m`. Skipped automatically without the project or npx. +func TestRunRemotion_EndToEnd(t *testing.T) { + if testing.Short() { + t.Skip("skipping end-to-end render in -short mode") + } + remDir := remotionProjectDir(t) + if _, err := exec.LookPath(npxCmd()); err != nil { + t.Skipf("%s not on PATH (skipping)", npxCmd()) + } + + out := filepath.Join(t.TempDir(), "engine-out.mp4") + job := &Job{ + JobID: "test-remotion-e2e", + Engine: EngineRemotion, + CompName: "KineticQuote", + Quality: "free", + Resolution: "360p", // exercises the height tier mapping + Bindings: []Binding{ + {Key: "quote", Value: "Two engines, one output."}, + {Key: "author", Value: "Engine Test"}, + {Key: "accentColor", Value: "#22d3ee"}, + }, + } + + var lastPct int + onProgress := func(_ context.Context, pct int, _ string) error { lastPct = pct; return nil } + + ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute) + defer cancel() + + got, err := RunRemotion(ctx, remDir, job, out, onProgress, nil) + if err != nil { + t.Fatalf("RunRemotion: %v", err) + } + st, err := os.Stat(got) + if err != nil { + t.Fatalf("stat output: %v", err) + } + if st.Size() == 0 { + t.Fatal("output file is empty") + } + if lastPct < 90 { + t.Errorf("final progress only reached %d%%", lastPct) + } + t.Logf("rendered %s (%d bytes), final progress %d%%", got, st.Size(), lastPct) +} + +// TestRun_RemotionEngine exercises the real integration point the node-agent uses: +// runner.Run() dispatching on Job.Engine. With Engine=Remotion and an empty AE path +// (which would otherwise trigger the AE mock), it must route to the Remotion engine +// and produce a real MP4. +func TestRun_RemotionEngine(t *testing.T) { + if testing.Short() { + t.Skip("skipping end-to-end render in -short mode") + } + remDir := remotionProjectDir(t) + if _, err := exec.LookPath(npxCmd()); err != nil { + t.Skipf("%s not on PATH (skipping)", npxCmd()) + } + + job := &Job{ + JobID: "test-run-dispatch", + Engine: EngineRemotion, + RemotionDir: remDir, + CompName: "KineticQuote", + Quality: "free", + Resolution: "360p", + Bindings: []Binding{{Key: "author", Value: "Dispatch Test"}}, + } + noop := func(context.Context, int, string) error { return nil } + + ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute) + defer cancel() + + // aePath empty: an AE job would mock here; a Remotion job must still render for real. + got, err := Run(ctx, "", t.TempDir(), job, noop, nil) + if err != nil { + t.Fatalf("Run (remotion engine): %v", err) + } + st, err := os.Stat(got) + if err != nil || st.Size() == 0 { + t.Fatalf("no output from Run: %v", err) + } + if string(mustRead(t, got)[:4]) == "mock" { + t.Fatal("Run produced the AE mock output instead of a real Remotion render") + } + t.Logf("Run dispatched to Remotion → %s (%d bytes)", got, st.Size()) +} + +func mustRead(t *testing.T, path string) []byte { + t.Helper() + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return b +} diff --git a/services/node-agent/internal/runner/runner.go b/services/node-agent/internal/runner/runner.go index 43c70df..701bbdb 100644 --- a/services/node-agent/internal/runner/runner.go +++ b/services/node-agent/internal/runner/runner.go @@ -46,6 +46,11 @@ type Job struct { FrameRate int HasMusic bool HasVoiceover bool + // Engine selects the render engine: EngineAfterEffects (default, "" treated as + // AE for backwards-compat) or EngineRemotion (code-based React templates). + Engine string + // RemotionDir is the Remotion project root, used only when Engine == EngineRemotion. + RemotionDir string // AEPFilePath is the local path to the downloaded .aep project file. // In a full implementation the agent downloads this from MinIO before calling Run. AEPFilePath string @@ -75,6 +80,12 @@ func Run(ctx context.Context, aePath, workDir string, job *Job, onProgress Progr } outputPath := filepath.Join(outputDir, "output.mp4") + // Engine dispatch. Remotion is fully self-contained (Node + Chrome), so it + // never touches the AE / mock paths below. + if strings.EqualFold(job.Engine, EngineRemotion) { + return RunRemotion(ctx, job.RemotionDir, job, outputPath, onProgress, onPreview) + } + // Mock render when AE isn't installed (aePath empty) OR when this job has no // template project to render (AEPFilePath empty — the template bundle wasn't // uploaded/promoted yet). Mock drives progress+preview to completion so the job diff --git a/services/remotion/package-lock.json b/services/remotion/package-lock.json new file mode 100644 index 0000000..f5589a6 --- /dev/null +++ b/services/remotion/package-lock.json @@ -0,0 +1,3319 @@ +{ + "name": "flatrender-remotion", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flatrender-remotion", + "version": "0.1.0", + "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.1.2", + "@react-three/postprocessing": "^3.0.4", + "@remotion/cli": "4.0.290", + "@remotion/three": "^4.0.290", + "@remotion/zod-types": "4.0.290", + "@types/three": "^0.171.0", + "postprocessing": "^6.39.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "remotion": "4.0.290", + "three": "^0.171.0", + "zod": "3.22.3" + }, + "devDependencies": { + "@types/react": "19.0.0", + "typescript": "5.5.4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.1.2.tgz", + "integrity": "sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.28.9", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-reconciler": "^0.31.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.25.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/@remotion/bundler": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.290.tgz", + "integrity": "sha512-nE9BZKcldoHQ2Z26+vq0MUnt1Hj8UnRyt7wNlwPpTOz0IzUnaPpii99vMlUL64krqkNLGW80V5kcABViODaM+g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/studio": "4.0.290", + "@remotion/studio-shared": "4.0.290", + "css-loader": "5.2.7", + "esbuild": "0.25.0", + "react-refresh": "0.9.0", + "remotion": "4.0.290", + "source-map": "0.7.3", + "style-loader": "4.0.0", + "webpack": "5.96.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/cli": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.290.tgz", + "integrity": "sha512-1uagTi1+JSfMmF52+4+5LbHD/+MoJvTEuB7V7cCI8xthYpWjgkyGIola235EfqnzwIoN6pQUOBZHH2fQZuLK7Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/bundler": "4.0.290", + "@remotion/media-utils": "4.0.290", + "@remotion/player": "4.0.290", + "@remotion/renderer": "4.0.290", + "@remotion/studio": "4.0.290", + "@remotion/studio-server": "4.0.290", + "@remotion/studio-shared": "4.0.290", + "dotenv": "9.0.2", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.290" + }, + "bin": { + "remotion": "remotion-cli.js", + "remotionb": "remotionb-cli.js", + "remotiond": "remotiond-cli.js" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.290.tgz", + "integrity": "sha512-GRo1E2M2i82WDMX2t2CB1GwfoWRu7iX7WDiwOH8g1Ro5jEH8SW2Ike3wqJNrohAriISm8x+/3gKYmYUps8p/Sw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.290.tgz", + "integrity": "sha512-gEQEcDBRtm6b2nW7kDwJdFWG8XipLhb7AP7WFO+u7OwFr9YmSDYCDIz4GnL8XMrvzDAwDmzj6zgJkP+v3jgirw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.290.tgz", + "integrity": "sha512-OAxfdm2HyiTJCCnkJjAU9bJBqdw62g+NwapSD0/KwVPfXRWV/2zN+LW1ToanQ96HdVtNGwXryTv4FO0HO2IlOA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.290.tgz", + "integrity": "sha512-oo52TZX343ZuGCX+JeJpmPjSTLe9hCV6UlxjAJZhNzjkihUDFU9twa7jwE2ktZDsRAgvIxrs18xwQN8hLFxHUQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.290.tgz", + "integrity": "sha512-IoHvGiqUZqmdUCSr8s5/EFwcZa3v0x7PHD3fxr73xZyddXDla1soFiXQO1xQzH7C5LmuEx1ekoQhxcx8oFRf0A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.290.tgz", + "integrity": "sha512-QN++ycuoeeeSLGrOA/MJ2onkqMRm0FaHh/6dh60dVK9wjWoMDrzZL1V8fPX075SyWVL01SN7bzTyotwaUJrMxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.290.tgz", + "integrity": "sha512-acRUOIeswQuJ74tKnuziwZIUcsQ6isQpwYUla0CiFrC9MVdbTFlt37eyvmkgXXkCartPWlFptfDYDv9li6mBmA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/media-parser": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.290.tgz", + "integrity": "sha512-u3/nRI7ghf0Cnci2DrbHrdHLIW7GFCaV5qVNT6IzOqdpVmwiS9sz3jAM85r7wrMKfed2QqVbKsRaLSY3MGA0RQ==", + "license": "Remotion License https://remotion.dev/license" + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.290.tgz", + "integrity": "sha512-VbBy7NtcLm/jG9c2nq2DYSMIx6AI+EyX/66EQ57txnMIuR/OSdx2j0PoMg/lyAl1RzZp8k/epBKTcod420y2PQ==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.290" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/player": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.290.tgz", + "integrity": "sha512-RsOSm3NOGwyRz11nRqfgFs0Mx7pWrXnHCcmWWUy5iaPKEbt8sCPsO4tVN5Ceogh0QdesoC3FGNeD/iGYk46e+A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.290" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.290.tgz", + "integrity": "sha512-/yNFkY5L/4jOmvgMqAmAa+qXenRilJrSWWDMdLFta2znXIE2m8NGZzQo9MlE7I+N1DcdZ2W+vrka01Epf7HIgQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/streaming": "4.0.290", + "execa": "5.1.1", + "extract-zip": "2.0.1", + "remotion": "4.0.290", + "source-map": "^0.8.0-beta.0", + "ws": "8.17.1" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.290", + "@remotion/compositor-darwin-x64": "4.0.290", + "@remotion/compositor-linux-arm64-gnu": "4.0.290", + "@remotion/compositor-linux-arm64-musl": "4.0.290", + "@remotion/compositor-linux-x64-gnu": "4.0.290", + "@remotion/compositor-linux-x64-musl": "4.0.290", + "@remotion/compositor-win32-x64-msvc": "4.0.290" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.290.tgz", + "integrity": "sha512-i043W/aXDG2ffOhMqxj9DjzCegzraH+WaiAZ8wMihXWkkYz83r2RgN+w+Zb1a10MM3VG1U2dgZ2QYymu3SEq+w==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.290.tgz", + "integrity": "sha512-Wf78/DCCmO+Ck4IWyVBzcWQVMubIbArxZUFgc/45owF0d7d3W3iKV/rs0deUAWkHTcR4mI+47ha2e3vDRdOMhQ==", + "license": "MIT", + "dependencies": { + "@remotion/media-parser": "4.0.290", + "@remotion/media-utils": "4.0.290", + "@remotion/player": "4.0.290", + "@remotion/renderer": "4.0.290", + "@remotion/studio-shared": "4.0.290", + "@remotion/zod-types": "4.0.290", + "memfs": "3.4.3", + "open": "^8.4.2", + "remotion": "4.0.290", + "semver": "7.5.3", + "source-map": "0.7.3", + "zod": "3.22.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.290.tgz", + "integrity": "sha512-r+iSKai3gkvnx0d0QE6/YYmwAM3L8C4AJz4ITkw4txtBNbrOWJds8abbBCNs/zmfVLflsft+V0Y76vW9a/7HZA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.290", + "@remotion/renderer": "4.0.290", + "@remotion/studio-shared": "4.0.290", + "memfs": "3.4.3", + "open": "^8.4.2", + "recast": "0.23.9", + "remotion": "4.0.290", + "semver": "7.5.3", + "source-map": "0.7.3" + } + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.290.tgz", + "integrity": "sha512-CU5aGCzVUEadOb2XqG0gs/rUl5W8UIZVakNjZQcBVUXRq81fyOPCKvrBQHxwA2zWTiyMmvgKAm9LEuozVzAY2A==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.290" + } + }, + "node_modules/@remotion/three": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/three/-/three-4.0.290.tgz", + "integrity": "sha512-aDV5IJMau18OkKJbUpMl+YeQC+yHNw6/U6bKLe/SngocYUfWth47MYtwRa2/3gtkeatWWfCf1N567l5LeJOUYg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.290" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "remotion": "4.0.290", + "three": ">=0.137.0" + } + }, + "node_modules/@remotion/zod-types": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.290.tgz", + "integrity": "sha512-3VSLK8ycb76fMXxFELaQ5EMSbQIL4iRrcPRr+6eKothchF+Bm8C2IlB2dqTZMj5W/3AP71qv4vBizi8ZgHjhTg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.290" + }, + "peerDependencies": { + "zod": "3.22.3" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", + "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.171.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.171.0.tgz", + "integrity": "sha512-oLuT1SAsT+CUg/wxUTFHo0K3NtJLnx9sJhZWQJp/0uXqFpzSk1hRHmvWvpaAWSfvx2db0lVKZ5/wV0I0isD2mQ==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/n8ao": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.2.tgz", + "integrity": "sha512-gh4i0xFP8DiRaNoX75kPiG3kGl7PmX9SW/OIn3Sv/YZmHrA8faLdSU1MM6M4gKtfKpxDnZagodu8FKob5URCVw==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, + "node_modules/nanoid": { + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", + "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postprocessing": { + "version": "6.39.1", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz", + "integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.168.0 < 0.185.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/recast": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", + "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remotion": { + "version": "4.0.290", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.290.tgz", + "integrity": "sha512-htwe74xP6et06V+xjCGH8hID0f3DEdSUKEu6oCUYYZUcE1gytEwhvubQyMGXH54uPfztV+jRZnWZnSSpXKxYBg==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.171.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", + "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/services/remotion/package.json b/services/remotion/package.json new file mode 100644 index 0000000..a137130 --- /dev/null +++ b/services/remotion/package.json @@ -0,0 +1,31 @@ +{ + "name": "flatrender-remotion", + "version": "0.1.0", + "private": true, + "description": "FlatRender code-based (Remotion) video template renderer", + "scripts": { + "dev": "remotion studio", + "render": "remotion render", + "still": "remotion still", + "upgrade": "remotion upgrade" + }, + "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.1.2", + "@react-three/postprocessing": "^3.0.4", + "@remotion/cli": "4.0.290", + "@remotion/three": "^4.0.290", + "@remotion/zod-types": "4.0.290", + "@types/three": "^0.171.0", + "postprocessing": "^6.39.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "remotion": "4.0.290", + "three": "^0.171.0", + "zod": "3.22.3" + }, + "devDependencies": { + "@types/react": "19.0.0", + "typescript": "5.5.4" + } +} diff --git a/services/remotion/public/fonts/vazirmatn-400.woff2 b/services/remotion/public/fonts/vazirmatn-400.woff2 new file mode 100644 index 0000000..1aea5a7 Binary files /dev/null and b/services/remotion/public/fonts/vazirmatn-400.woff2 differ diff --git a/services/remotion/public/fonts/vazirmatn-600.woff2 b/services/remotion/public/fonts/vazirmatn-600.woff2 new file mode 100644 index 0000000..7bf974e Binary files /dev/null and b/services/remotion/public/fonts/vazirmatn-600.woff2 differ diff --git a/services/remotion/public/fonts/vazirmatn-700.woff2 b/services/remotion/public/fonts/vazirmatn-700.woff2 new file mode 100644 index 0000000..cf6a0f4 Binary files /dev/null and b/services/remotion/public/fonts/vazirmatn-700.woff2 differ diff --git a/services/remotion/public/fonts/vazirmatn-800.woff2 b/services/remotion/public/fonts/vazirmatn-800.woff2 new file mode 100644 index 0000000..20ba462 Binary files /dev/null and b/services/remotion/public/fonts/vazirmatn-800.woff2 differ diff --git a/services/remotion/public/fonts/vazirmatn-900.woff2 b/services/remotion/public/fonts/vazirmatn-900.woff2 new file mode 100644 index 0000000..d67113d Binary files /dev/null and b/services/remotion/public/fonts/vazirmatn-900.woff2 differ diff --git a/services/remotion/remotion.config.ts b/services/remotion/remotion.config.ts new file mode 100644 index 0000000..7dd55f5 --- /dev/null +++ b/services/remotion/remotion.config.ts @@ -0,0 +1,18 @@ +import { Config } from "@remotion/cli/config"; + +Config.setVideoImageFormat("jpeg"); +Config.setOverwriteOutput(true); +// Higher quality concurrency defaults for the logo-intro previews. +Config.setConcurrency(4); + +// Remotion's bundled Chrome Headless Shell download is geo-blocked (403) from +// Iran, so point it at the locally-installed Chrome instead. Override with the +// REMOTION_BROWSER env var on machines where Chrome lives elsewhere. +Config.setBrowserExecutable( + process.env.REMOTION_BROWSER ?? + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" +); + +// Required for WebGL / Three.js (@remotion/three) templates to render headless. +// "angle" works with the local Chrome; the node-agent inherits this from config. +Config.setChromiumOpenGlRenderer("angle"); diff --git a/services/remotion/src/Root.tsx b/services/remotion/src/Root.tsx new file mode 100644 index 0000000..6c9feb7 --- /dev/null +++ b/services/remotion/src/Root.tsx @@ -0,0 +1,135 @@ +import { Composition } from "remotion"; +import { ASPECTS } from "./lib/aspect"; +import { TEMPLATES } from "./templates"; +import { Three3DTest } from "./compositions/Three3DTest"; +import { + IlluminatedCircles, + illuminatedCirclesSchema, +} from "./compositions/IlluminatedCircles"; +import { + KineticQuote, + kineticQuoteSchema, +} from "./compositions/KineticQuote"; +import { + GradientPromo, + gradientPromoSchema, +} from "./compositions/GradientPromo"; +import { + VerticalStory, + verticalStorySchema, +} from "./compositions/VerticalStory"; + +const FPS = 30; + +export const RemotionRoot: React.FC = () => { + return ( + <> + {/* Logo intro — 16:9 */} + + + {/* Kinetic typography quote — 1:1 social */} + + + {/* Marketing / sale promo — 16:9 */} + + + {/* Vertical social story — 9:16 */} + + + {/* 3D feasibility test */} + + + {/* Branded templates — each registered in all three aspects. */} + {TEMPLATES.flatMap((tpl) => + ASPECTS.map((a) => ( + + )) + )} + + ); +}; diff --git a/services/remotion/src/compositions/Birthday3D.tsx b/services/remotion/src/compositions/Birthday3D.tsx new file mode 100644 index 0000000..066118f --- /dev/null +++ b/services/remotion/src/compositions/Birthday3D.tsx @@ -0,0 +1,179 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { ThreeCanvas } from "@remotion/three"; +import * as THREE from "three"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; +import { StudioEnv, StudioFloor, StudioLights, StudioEffects, Confetti3D } from "../lib/three-kit"; + +export const birthday3DSchema = z.object({ + greeting: z.string(), + name: z.string(), + message: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +const Candle: React.FC<{ x: number; z: number; i: number; accent: string }> = ({ x, z, i, accent }) => { + const frame = useCurrentFrame(); + const flick = 1 + Math.sin(frame / 4 + i) * 0.18; + return ( + + + + + + + + + + + + ); +}; + +const Cake: React.FC<{ accent: string; secondary: string }> = ({ accent, secondary }) => { + const cream = "#fbeede"; + const frost = accent; + const candleN = 5; + return ( + + {/* plate */} + + + + + {/* tier 1 */} + + + + + + + + + {/* tier 2 */} + + + + + + + + + {/* cherries */} + {Array.from({ length: 8 }).map((_, i) => ( + + + + + ))} + {/* candles */} + {Array.from({ length: candleN }).map((_, i) => { + const a = (i / candleN) * Math.PI * 2; + return ; + })} + + ); +}; + +const Balloon: React.FC<{ x: number; z: number; i: number; color: string }> = ({ x, z, i, color }) => { + const frame = useCurrentFrame(); + const bob = Math.sin(frame / 30 + i) * 0.25; + const sway = Math.sin(frame / 40 + i * 2) * 0.1; + const baseY = 1.4 + (i % 3) * 0.5; + return ( + + + + + + + + + + + + + + + ); +}; + +const Scene: React.FC<{ accent: string; secondary: string }> = ({ accent, secondary }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const enter = spring({ frame: frame - 8, fps, config: { damping: 14, stiffness: 60 } }); + const orbit = Math.sin(frame / 110) * 0.2; + const balloonColors = [accent, secondary, "#fde047", "#34d399", "#60a5fa"]; + return ( + + + + + + {[[-2.2, -0.5], [2.2, -0.6], [-1.7, -1.6], [1.8, -1.4], [0, -2.2]].map((p, i) => ( + + ))} + + + ); +}; + +export const Birthday3D: React.FC = ({ + greeting, + name, + message, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { width, height, fps } = useVideoConfig(); + const L = useLayout(); + + const gOp = interpolate(frame, [12, 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const namePop = spring({ frame: frame - 30, fps, config: { damping: 10, stiffness: 120 } }); + const nameScale = interpolate(namePop, [0, 1], [0.4, 1]); + const msgOp = interpolate(frame, [150, 172], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + return ( + + + + + + + + +
+ {greeting} +
+
+ {name} +
+
+ {message} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/Countdown.tsx b/services/remotion/src/compositions/Countdown.tsx new file mode 100644 index 0000000..1346ece --- /dev/null +++ b/services/remotion/src/compositions/Countdown.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba } from "../lib/anim"; + +export const countdownSchema = z.object({ + title: z.string(), + // coerce so a string binding ("5") from the studio still validates as a number + startNumber: z.coerce.number().int().min(1).max(9), + goText: z.string(), + subtitle: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const Countdown: React.FC = ({ + title, + startNumber, + goText, + subtitle, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const L = useLayout(); + + const titleR = useReveal(6, { from: 24 }); + + // Count down one number per second after a short intro. + const introF = Math.round(fps * 1.2); + const elapsed = Math.max(0, frame - introF); + const sec = Math.floor(elapsed / fps); + const current = startNumber - sec; // >0 → number, <=0 → GO + const localInSec = (elapsed % fps) / fps; + + // Each tick pops in and fades/scales out. + const pop = spring({ frame: (elapsed % fps), fps, config: { damping: 12, stiffness: 130, mass: 0.7 } }); + const scaleIn = interpolate(pop, [0, 1], [0.4, 1]); + const scaleOut = interpolate(localInSec, [0.7, 1], [1, 1.4], { extrapolateLeft: "clamp" }); + const fadeOut = interpolate(localInSec, [0.75, 1], [1, 0], { extrapolateLeft: "clamp" }); + + const isGo = current <= 0; + const ringProgress = 1 - localInSec; + const ringR = L.vmin(220); + const circ = 2 * Math.PI * ringR; + + const sub = useReveal(introF + 4, { from: 24 }); + + return ( + + + +
+ {title} +
+ + + {/* Progress ring */} + {!isGo && ( + + + + + )} + +
+ {isGo ? goText : current} +
+
+ +
+ {subtitle} +
+
+ ); +}; diff --git a/services/remotion/src/compositions/EventInvite.tsx b/services/remotion/src/compositions/EventInvite.tsx new file mode 100644 index 0000000..497abd1 --- /dev/null +++ b/services/remotion/src/compositions/EventInvite.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba } from "../lib/anim"; + +export const eventInviteSchema = z.object({ + kicker: z.string(), + eventTitle: z.string(), + date: z.string(), + location: z.string(), + cta: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const EventInvite: React.FC = ({ + kicker, + eventTitle, + date, + location, + cta, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const L = useLayout(); + + const kick = useReveal(8, { from: 22 }); + const title = useReveal(22, { from: 44 }); + const meta = useReveal(44, { from: 26 }); + const ctaR = useReveal(64, { from: 22, damping: 12 }); + + // Elegant double border that draws in. + const borderInset = interpolate(frame, [0, 30], [L.vmin(40), L.vmin(70)], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + const borderOp = interpolate(frame, [0, 24], [0, 1], { extrapolateRight: "clamp" }); + + return ( + + + + {/* Ornamental frame */} +
+
+ + +
+ {kicker} +
+
+ {eventTitle} +
+ +
+ + +
+ +
+ {cta} +
+
+ + ); +}; + +const Meta: React.FC<{ L: ReturnType; icon: string; label: string; color: string; accent: string }> = ({ L, icon, label, color, accent }) => ( +
+ {icon} + {label} +
+); diff --git a/services/remotion/src/compositions/GlitterReveal.tsx b/services/remotion/src/compositions/GlitterReveal.tsx new file mode 100644 index 0000000..153424a --- /dev/null +++ b/services/remotion/src/compositions/GlitterReveal.tsx @@ -0,0 +1,196 @@ +import React from "react"; +import { + AbsoluteFill, + Img, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; + +export const glitterRevealSchema = z.object({ + brandText: z.string(), + tagline: z.string(), + /** Optional logo image URL. When empty the FlatRender brand mark is used. */ + logoUrl: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +// ── Default FlatRender brand mark (used when the user hasn't uploaded a logo) ── +const DefaultLogo: React.FC<{ size: number }> = ({ size }) => ( + + + + + + + +); + +// Deterministic glitter field — each particle flies in from the edge, gathers at +// the logo, then disperses into an ambient orbit (the classic glitter-dust reveal). +const GLITTER = Array.from({ length: 150 }).map((_, i) => ({ + i, + angleIn: rand(i) * Math.PI * 2, + distIn: 520 + rand(i + 7) * 460, + // gather target: a tight cluster over the logo + tx: (rand(i + 11) - 0.5) * 360, + ty: (rand(i + 19) - 0.5) * 240, + // ambient orbit it settles into + ambAngle: rand(i + 23) * Math.PI * 2, + ambR: 230 + rand(i + 29) * 320, + size: 1.6 + rand(i + 3) * 4.5, + delay: (i % 18) * 0.9, + speed: 0.4 + rand(i + 5) * 1.2, +})); + +const Glitter: React.FC<{ accent: string; secondary: string; gold: string }> = ({ + accent, + secondary, + gold, +}) => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + const L = useLayout(); + const cx = width / 2; + const cy = height / 2; + + return ( + + + {GLITTER.map((p) => { + const conv = interpolate(frame, [p.delay, p.delay + 34], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + const disp = interpolate(frame, [46, 86], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.inOut(Easing.quad), + }); + // start (far out) → gather cluster → ambient orbit + const sx = cx + Math.cos(p.angleIn) * L.vmin(p.distIn); + const sy = cy + Math.sin(p.angleIn) * L.vmin(p.distIn); + const gx = cx + L.vmin(p.tx); + const gy = cy + L.vmin(p.ty); + const ax = cx + Math.cos(p.ambAngle + frame * 0.004 * p.speed) * L.vmin(p.ambR); + const ay = cy + Math.sin(p.ambAngle + frame * 0.004 * p.speed) * L.vmin(p.ambR); + const tgtX = gx + (ax - gx) * disp; + const tgtY = gy + (ay - gy) * disp; + const x = sx + (tgtX - sx) * conv; + const y = sy + (tgtY - sy) * conv; + + const twinkle = 0.3 + 0.7 * Math.abs(Math.sin((frame + p.i * 13) / (6 + (p.i % 5)))); + const appear = interpolate(frame, [p.delay, p.delay + 10], [0, 1], { extrapolateRight: "clamp" }); + const c = p.i % 4 === 0 ? gold : p.i % 3 === 0 ? secondary : accent; + const r = L.vmin(p.size) * (0.7 + conv * 0.5); + return ( + + ); + })} + + + ); +}; + +export const GlitterReveal: React.FC = ({ + brandText, + tagline, + logoUrl, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const L = useLayout(); + const gold = "#fcd34d"; + + // Logo reveal (the glitter gathers ~frame 44, then the logo emerges). + const logoSpring = spring({ frame: frame - 42, fps, config: { damping: 13, stiffness: 95, mass: 0.9 } }); + const logoScale = interpolate(logoSpring, [0, 1], [0.55, 1]); + const logoOpacity = interpolate(frame, [42, 60], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + // Bright convergence flash. + const flash = interpolate(frame, [40, 47, 60], [0, 0.85, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + // Core glow that breathes behind the logo. + const glow = interpolate(frame, [44, 70], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const breathe = 1 + 0.05 * Math.sin(frame / 16); + + // Shine sweep across the logo at reveal. + const sweepX = interpolate(frame, [58, 88], [-L.vmin(360), L.vmin(360)], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic) }); + const sweepOp = interpolate(frame, [58, 66, 82, 90], [0, 0.9, 0.9, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + // Text. + const brandY = interpolate(frame, [70, 92], [L.vmin(70), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + const brandOpacity = interpolate(frame, [70, 90], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const tagOpacity = interpolate(frame, [92, 112], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const tagSpacing = interpolate(frame, [92, 120], [L.vmin(14), L.vmin(6)], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + const logoSize = L.vmin(240); + const hasLogo = Boolean(logoUrl && logoUrl.trim().length > 0); + + return ( + + {/* Deep radial backdrop */} + + + {/* Core glow */} + +
+ + + + + {/* Logo */} + +
+ {hasLogo ? ( + + ) : ( + + )} +
+
+ + {/* Convergence flash */} + +
+ + + {/* Shine sweep */} + +
+ + + {/* Brand text + tagline */} + +
+ {brandText} +
+
+ {tagline} +
+
+ + ); +}; diff --git a/services/remotion/src/compositions/GradientPromo.tsx b/services/remotion/src/compositions/GradientPromo.tsx new file mode 100644 index 0000000..86f6106 --- /dev/null +++ b/services/remotion/src/compositions/GradientPromo.tsx @@ -0,0 +1,245 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { zColor } from "@remotion/zod-types"; +import { z } from "zod"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; + +export const gradientPromoSchema = z.object({ + eyebrow: z.string(), + headline: z.string(), + subheadline: z.string(), + ctaText: z.string(), + badgeText: z.string(), + accentColor: zColor(), + secondaryColor: zColor(), + backgroundColor: zColor(), +}); + +type Props = z.infer; + +// ── Drifting mesh-gradient blobs ───────────────────────────────────────────── + +const BLOBS = [ + { baseX: 0.2, baseY: 0.3, r: 520, useAccent: true }, + { baseX: 0.78, baseY: 0.28, r: 460, useAccent: false }, + { baseX: 0.62, baseY: 0.8, r: 580, useAccent: true }, + { baseX: 0.12, baseY: 0.82, r: 420, useAccent: false }, +]; + +const MeshBackground: React.FC<{ + bg: string; + accent: string; + secondary: string; +}> = ({ bg, accent, secondary }) => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + return ( + + {BLOBS.map((b, i) => { + const dx = Math.sin(frame / (50 + i * 12) + rand(i) * 6) * 70; + const dy = Math.cos(frame / (60 + i * 9) + rand(i + 4) * 6) * 60; + const color = b.useAccent ? accent : secondary; + return ( +
+ ); + })} + {/* Subtle grain/vignette to ground the gradients */} + + + ); +}; + +// ── Spinning offer badge in the corner ─────────────────────────────────────── + +const Badge: React.FC<{ text: string; accent: string; secondary: string }> = ({ + text, + accent, + secondary, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const pop = spring({ + frame: frame - 26, + fps, + config: { damping: 11, mass: 0.6, stiffness: 140 }, + }); + const scale = interpolate(pop, [0, 1], [0, 1]); + const wobble = Math.sin(frame / 16) * 6; + return ( +
+ {text} +
+ ); +}; + +export const GradientPromo: React.FC = ({ + eyebrow, + headline, + subheadline, + ctaText, + badgeText, + accentColor, + secondaryColor, + backgroundColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const reveal = (delay: number) => + spring({ frame: frame - delay, fps, config: { damping: 18, stiffness: 90 } }); + + const eyebrowOp = interpolate(reveal(6), [0, 1], [0, 1]); + const eyebrowX = interpolate(reveal(6), [0, 1], [-40, 0]); + + const headSpring = reveal(14); + const headY = interpolate(headSpring, [0, 1], [60, 0]); + const headOp = interpolate(headSpring, [0, 1], [0, 1]); + + const subOp = interpolate(reveal(28), [0, 1], [0, 1]); + const subY = interpolate(reveal(28), [0, 1], [30, 0]); + + const ctaSpring = reveal(40); + const ctaScale = interpolate(ctaSpring, [0, 1], [0.7, 1]); + const ctaOp = interpolate(ctaSpring, [0, 1], [0, 1]); + const ctaGlow = 0.4 + 0.3 * Math.sin(frame / 12); + + return ( + + + + + +
+ {eyebrow} +
+ +
+ {headline} +
+ +
+ {subheadline} +
+ +
+ {ctaText} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/HappyBirthday.tsx b/services/remotion/src/compositions/HappyBirthday.tsx new file mode 100644 index 0000000..4ef364a --- /dev/null +++ b/services/remotion/src/compositions/HappyBirthday.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba, rand } from "../lib/anim"; + +export const happyBirthdaySchema = z.object({ + greeting: z.string(), + name: z.string(), + message: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +const CONFETTI = Array.from({ length: 60 }); + +export const HappyBirthday: React.FC = ({ + greeting, + name, + message, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps, width, height } = useVideoConfig(); + const L = useLayout(); + + const greet = useReveal(8, { from: 30 }); + const namePop = spring({ frame: frame - 26, fps, config: { damping: 10, stiffness: 120, mass: 0.8 } }); + const nameScale = interpolate(namePop, [0, 1], [0.3, 1]); + const msg = useReveal(56, { from: 24 }); + const colors = [accentColor, secondaryColor, "#fde047", "#fb7185", "#34d399"]; + + return ( + + + + {/* Confetti rain */} + + {CONFETTI.map((_, i) => { + const startDelay = (i % 12) * 2; + const t = Math.max(0, frame - startDelay); + const x = rand(i) * width + Math.sin((frame + i * 20) / 18) * L.vmin(30); + const y = ((rand(i + 5) * height) + t * (2 + rand(i) * 3) * L.unit) % (height + 40) - 20; + const sz = L.vmin(8 + (i % 4) * 4); + const rot = (frame + i * 30) * (i % 2 ? 4 : -4); + return
; + })} + + + +
🎂
+
+ {greeting} +
+
+ {name} +
+
+ {message} +
+
+ + ); +}; diff --git a/services/remotion/src/compositions/Hero3D.tsx b/services/remotion/src/compositions/Hero3D.tsx new file mode 100644 index 0000000..f80aef0 --- /dev/null +++ b/services/remotion/src/compositions/Hero3D.tsx @@ -0,0 +1,160 @@ +import React, { useMemo } from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { ThreeCanvas } from "@remotion/three"; +import * as THREE from "three"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; + +export const hero3DSchema = z.object({ + brandText: z.string(), + tagline: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +const SHAPES = ["icosa", "octa", "dodeca", "box", "torus"] as const; + +// One floating polyhedron, drifting + self-rotating (animated off the timeline, +// not R3F's render loop, so renders stay deterministic). +const FloatingShape: React.FC<{ i: number; accent: string; secondary: string }> = ({ i, accent, secondary }) => { + const frame = useCurrentFrame(); + const kind = SHAPES[i % SHAPES.length]; + const ang = rand(i) * Math.PI * 2; + const radius = 2.6 + rand(i + 5) * 2.4; + const depth = -1 - rand(i + 9) * 4; + const x = Math.cos(ang + frame * 0.004 * (0.5 + rand(i) * 0.6)) * radius; + const y = Math.sin(ang * 1.7 + frame * 0.006) * (1.4 + rand(i + 3) * 1.4); + const s = 0.18 + rand(i + 7) * 0.35; + const col = i % 2 === 0 ? accent : secondary; + const appear = interpolate(frame, [8 + i * 2, 36 + i * 2], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + return ( + + {kind === "icosa" && } + {kind === "octa" && } + {kind === "dodeca" && } + {kind === "box" && } + {kind === "torus" && } + + + ); +}; + +const Bokeh: React.FC<{ accent: string; secondary: string }> = ({ accent, secondary }) => { + const frame = useCurrentFrame(); + return ( + + {Array.from({ length: 16 }).map((_, i) => { + const x = (rand(i) - 0.5) * 12; + const y = (rand(i + 11) - 0.5) * 7; + const z = -6 - rand(i + 4) * 5; + const tw = 0.3 + 0.5 * Math.abs(Math.sin((frame + i * 20) / 25)); + const col = i % 3 === 0 ? secondary : accent; + return ( + + + + + ); + })} + + ); +}; + +const Scene: React.FC<{ accent: string; secondary: string }> = ({ accent, secondary }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const pop = spring({ frame: frame - 6, fps, config: { damping: 12, stiffness: 80, mass: 1 } }); + const heroScale = interpolate(pop, [0, 1], [0, 1.35]); + const heroSpin = frame * 0.02; + + const heroColor = useMemo(() => mixHex(accent, secondary, 0.25), [accent, secondary]); + + return ( + + + + + + + + {Array.from({ length: 10 }).map((_, i) => ( + + ))} + + {/* Hero faceted gem */} + + + + + {/* Inner glow core */} + + + + + + ); +}; + +export const Hero3D: React.FC = ({ + brandText, + tagline, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + const L = useLayout(); + + const brandY = interpolate(frame, [70, 92], [L.vmin(60), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + const brandOp = interpolate(frame, [70, 90], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const tagOp = interpolate(frame, [92, 114], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const tagSpacing = interpolate(frame, [92, 122], [L.vmin(14), L.vmin(6)], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + return ( + + {/* gradient vignette behind the 3D */} + + + + + + + {/* 2D text overlay (crisp Persian via Vazirmatn) */} + +
+ {brandText} +
+
+ {tagline} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/IlluminatedCircles.tsx b/services/remotion/src/compositions/IlluminatedCircles.tsx new file mode 100644 index 0000000..8bf4549 --- /dev/null +++ b/services/remotion/src/compositions/IlluminatedCircles.tsx @@ -0,0 +1,388 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { zColor } from "@remotion/zod-types"; +import { z } from "zod"; + +export const illuminatedCirclesSchema = z.object({ + logoText: z.string(), + tagline: z.string(), + accentColor: zColor(), + secondaryColor: zColor(), + backgroundColor: zColor(), +}); + +type Props = z.infer; + +// ── Small helpers ──────────────────────────────────────────────────────────── + +/** Mix two hex colors by t (0..1). Cheap linear blend, good enough for glows. */ +function mixHex(a: string, b: string, t: number): string { + const pa = a.replace("#", ""); + const pb = b.replace("#", ""); + const ai = parseInt(pa, 16); + const bi = parseInt(pb, 16); + const ar = (ai >> 16) & 255; + const ag = (ai >> 8) & 255; + const ab = ai & 255; + const br = (bi >> 16) & 255; + const bg = (bi >> 8) & 255; + const bb = bi & 255; + const r = Math.round(ar + (br - ar) * t); + const g = Math.round(ag + (bg - ag) * t); + const bl = Math.round(ab + (bb - ab) * t); + return `rgb(${r}, ${g}, ${bl})`; +} + +function hexToRgba(hex: string, alpha: number): string { + const p = hex.replace("#", ""); + const i = parseInt(p, 16); + const r = (i >> 16) & 255; + const g = (i >> 8) & 255; + const b = i & 255; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +// ── Background: deep radial gradient + drifting nebula + vignette ───────────── + +const Background: React.FC<{ bg: string; accent: string; secondary: string }> = ({ + bg, + accent, + secondary, +}) => { + const frame = useCurrentFrame(); + const drift = Math.sin(frame / 60) * 40; + return ( + + + + {/* Vignette */} + + + ); +}; + +// ── Concentric illuminated rings ───────────────────────────────────────────── + +const RING_DEFS = [ + { r: 150, speed: 0.5, dash: "2 14", width: 2, op: 0.9 }, + { r: 230, speed: -0.32, dash: "1 22", width: 1.5, op: 0.7 }, + { r: 320, speed: 0.22, dash: "3 28", width: 2.5, op: 0.85 }, + { r: 420, speed: -0.16, dash: "1 40", width: 1.5, op: 0.55 }, + { r: 520, speed: 0.12, dash: "2 60", width: 1.5, op: 0.4 }, +]; + +const Rings: React.FC<{ accent: string; secondary: string }> = ({ + accent, + secondary, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const entrance = spring({ + frame, + fps, + config: { damping: 14, mass: 0.9, stiffness: 90 }, + }); + const scale = interpolate(entrance, [0, 1], [0.55, 1]); + const groupOpacity = interpolate(frame, [0, 28], [0, 1], { + extrapolateRight: "clamp", + }); + + return ( + + + + + + + + + + {RING_DEFS.map((ring, i) => { + const rot = frame * ring.speed; + // Each ring reveals its dash over the first ~30 frames. + const circ = 2 * Math.PI * ring.r; + const draw = interpolate(frame, [4 + i * 4, 34 + i * 4], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + return ( + + + + ); + })} + + + ); +}; + +// ── Orbiting illuminated particles ─────────────────────────────────────────── + +const PARTICLES = Array.from({ length: 28 }).map((_, i) => { + // Deterministic pseudo-random placement (no Math.random — keeps renders stable). + const a = (i * 137.508 * Math.PI) / 180; // golden angle + const ringRadius = 150 + ((i * 53) % 380); + const size = 2 + ((i * 17) % 5); + const speed = 0.15 + ((i % 5) * 0.06) * (i % 2 === 0 ? 1 : -1); + const phase = (i * 41) % 360; + return { a, ringRadius, size, speed, phase, idx: i }; +}); + +const Particles: React.FC<{ accent: string; secondary: string }> = ({ + accent, + secondary, +}) => { + const frame = useCurrentFrame(); + const appear = interpolate(frame, [18, 50], [0, 1], { + extrapolateRight: "clamp", + }); + return ( + + + {PARTICLES.map((p) => { + const ang = p.a + (frame * p.speed * Math.PI) / 180; + const x = Math.cos(ang) * p.ringRadius; + const y = Math.sin(ang) * p.ringRadius; + const twinkle = + 0.4 + 0.6 * Math.abs(Math.sin((frame + p.phase) / 9)); + const color = p.idx % 3 === 0 ? secondary : accent; + return ( + + ); + })} + + + ); +}; + +// ── Central core glow that pulses behind the logo ──────────────────────────── + +const CoreGlow: React.FC<{ accent: string; secondary: string }> = ({ + accent, + secondary, +}) => { + const frame = useCurrentFrame(); + const grow = interpolate(frame, [30, 70], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + const breathe = 1 + 0.06 * Math.sin(frame / 14); + const size = 460 * grow * breathe; + return ( + +
+ + ); +}; + +// ── Sweeping light flare across the logo at reveal ─────────────────────────── + +const LightSweep: React.FC = () => { + const frame = useCurrentFrame(); + const x = interpolate(frame, [62, 92], [-900, 900], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.inOut(Easing.cubic), + }); + const op = interpolate(frame, [62, 70, 88, 96], [0, 0.85, 0.85, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + return ( + +
+ + ); +}; + +// ── Logo + tagline reveal ──────────────────────────────────────────────────── + +const LogoReveal: React.FC<{ logoText: string; tagline: string; accent: string }> = ({ + logoText, + tagline, + accent, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const logoSpring = spring({ + frame: frame - 55, + fps, + config: { damping: 16, mass: 1, stiffness: 80 }, + }); + const logoScale = interpolate(logoSpring, [0, 1], [1.25, 1]); + const logoOpacity = interpolate(frame, [55, 78], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const blur = interpolate(frame, [55, 84], [26, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + + const tagOpacity = interpolate(frame, [92, 116], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const tagSpacing = interpolate(frame, [92, 130], [22, 10], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + + return ( + +
+ {logoText} +
+
+ {tagline} +
+
+ ); +}; + +// ── Composition root ───────────────────────────────────────────────────────── + +export const IlluminatedCircles: React.FC = ({ + logoText, + tagline, + accentColor, + secondaryColor, + backgroundColor, +}) => { + return ( + + + + + + + + + ); +}; diff --git a/services/remotion/src/compositions/InstaPromo.tsx b/services/remotion/src/compositions/InstaPromo.tsx new file mode 100644 index 0000000..2aa340f --- /dev/null +++ b/services/remotion/src/compositions/InstaPromo.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba } from "../lib/anim"; + +export const instaPromoSchema = z.object({ + handle: z.string(), + headline: z.string(), + subtext: z.string(), + cta: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const InstaPromo: React.FC = ({ + handle, + headline, + subtext, + cta, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const L = useLayout(); + const card = useReveal(8, { from: 60, damping: 14 }); + const head = useReveal(26, { from: 36 }); + const ctaR = useReveal(52, { from: 24, damping: 12 }); + const heart = interpolate(frame % 60, [0, 15, 30], [1, 1.25, 1]); + + return ( + + + + + {/* Profile chip */} +
+
+
📸
+
+ {handle} +
+ +
+ {headline} +
+ +
+ {subtext} +
+ + {/* Floating reactions */} +
❤️
+ +
+ {cta} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/KineticQuote.tsx b/services/remotion/src/compositions/KineticQuote.tsx new file mode 100644 index 0000000..be8e9f9 --- /dev/null +++ b/services/remotion/src/compositions/KineticQuote.tsx @@ -0,0 +1,194 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { zColor } from "@remotion/zod-types"; +import { z } from "zod"; +import { hexToRgba, mixHex } from "../lib/anim"; + +export const kineticQuoteSchema = z.object({ + quote: z.string(), + author: z.string(), + accentColor: zColor(), + secondaryColor: zColor(), + backgroundColor: zColor(), +}); + +type Props = z.infer; + +// ── Slowly rotating gradient sheen behind the text ─────────────────────────── + +const SheenBackground: React.FC<{ + bg: string; + accent: string; + secondary: string; +}> = ({ bg, accent, secondary }) => { + const frame = useCurrentFrame(); + const angle = (frame * 0.4) % 360; + return ( + + + {/* Soft top glow */} + + + + ); +}; + +// ── Word-by-word reveal of the quote ───────────────────────────────────────── + +const Quote: React.FC<{ quote: string; accent: string; secondary: string }> = ({ + quote, + accent, + secondary, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const words = quote.split(/\s+/).filter(Boolean); + + return ( +
+ {words.map((w, i) => { + const start = 12 + i * 4; + const s = spring({ + frame: frame - start, + fps, + config: { damping: 18, mass: 0.7, stiffness: 110 }, + }); + const y = interpolate(s, [0, 1], [28, 0]); + const op = interpolate(s, [0, 1], [0, 1]); + return ( + + {w} + + ); + })} +
+ ); +}; + +export const KineticQuote: React.FC = ({ + quote, + author, + accentColor, + secondaryColor, + backgroundColor, +}) => { + const frame = useCurrentFrame(); + const words = quote.split(/\s+/).filter(Boolean); + + // The decorative rule + author appear once the quote has finished landing. + const tail = 12 + words.length * 4 + 8; + const ruleW = interpolate(frame, [tail, tail + 18], [0, 120], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + const authorOp = interpolate(frame, [tail + 10, tail + 30], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( + + + + {/* Opening quotation mark */} +
+ “ +
+ + + +
+
+ {author} +
+ + + ); +}; diff --git a/services/remotion/src/compositions/LogoMotion.tsx b/services/remotion/src/compositions/LogoMotion.tsx new file mode 100644 index 0000000..7f8c38c --- /dev/null +++ b/services/remotion/src/compositions/LogoMotion.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; + +export const logoMotionSchema = z.object({ + brandText: z.string(), + tagline: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const LogoMotion: React.FC = ({ + brandText, + tagline, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps, width, height } = useVideoConfig(); + const L = useLayout(); + + // Background: radial brand glow + drifting nebula. + const drift = Math.sin(frame / 50) * 30; + + // Logo entrance. + const logoSpring = spring({ frame, fps, config: { damping: 14, stiffness: 90, mass: 0.9 } }); + const ringScale = interpolate(logoSpring, [0, 1], [0.4, 1]); + const ringOpacity = interpolate(frame, [0, 22], [0, 1], { extrapolateRight: "clamp" }); + + const wordSpring = spring({ frame: frame - 22, fps, config: { damping: 16, stiffness: 80 } }); + const wordScale = interpolate(wordSpring, [0, 1], [1.18, 1]); + const wordOpacity = interpolate(frame, [22, 42], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const wordBlur = interpolate(frame, [22, 46], [16, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + + const tagOpacity = interpolate(frame, [50, 72], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const tagSpacing = interpolate(frame, [50, 80], [L.vmin(14), L.vmin(6)], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + + // Light sweep across the wordmark at reveal. + const sweepX = interpolate(frame, [44, 74], [-L.vmin(700), L.vmin(700)], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic) }); + const sweepOp = interpolate(frame, [44, 52, 70, 78], [0, 0.8, 0.8, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + const ringR = L.vmin(190); + + return ( + + {/* Brand glow */} + + + {/* Orbiting sparks */} + + + {Array.from({ length: 26 }).map((_, i) => { + const ang = (i * 137.5 * Math.PI) / 180 + (frame * (0.1 + (i % 4) * 0.04) * Math.PI) / 180; + const rr = L.vmin(150) + ((i * 47) % L.vmin(360)); + const cx = width / 2 + Math.cos(ang) * rr; + const cy = height / 2 + Math.sin(ang) * rr; + const tw = 0.3 + 0.6 * Math.abs(Math.sin((frame + i * 18) / 10)); + const appear = interpolate(frame, [16, 44], [0, 1], { extrapolateRight: "clamp" }); + const c = i % 3 === 0 ? secondaryColor : accentColor; + const s = L.vmin(2 + (i % 4)); + return ; + })} + + + + {/* Concentric brand ring */} + + + + + + + + + {[ringR, ringR * 0.74].map((r, i) => { + const circ = 2 * Math.PI * r; + const draw = interpolate(frame, [4 + i * 5, 30 + i * 5], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + return ( + + ); + })} + + + + {/* Wordmark + tagline */} + +
+ {brandText} +
+
+ {tagline} +
+
+ + {/* Light sweep */} + +
+ + + ); +}; diff --git a/services/remotion/src/compositions/Nowruz3D.tsx b/services/remotion/src/compositions/Nowruz3D.tsx new file mode 100644 index 0000000..316951a --- /dev/null +++ b/services/remotion/src/compositions/Nowruz3D.tsx @@ -0,0 +1,335 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { ThreeCanvas } from "@remotion/three"; +import { Environment, Lightformer, MeshReflectorMaterial, RoundedBox } from "@react-three/drei"; +import { EffectComposer, Bloom, DepthOfField, Vignette } from "@react-three/postprocessing"; +import * as THREE from "three"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { hexToRgba, rand } from "../lib/anim"; + +export const nowruz3DSchema = z.object({ + greeting: z.string(), + subtitle: z.string(), + message: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +const GOLD = "#f5c542"; +const RED = "#e23b3b"; +const SKIN = "#f0b486"; +const GREEN = "#4fb84f"; + +// ── Stylized 3D Haji Firuz (primitive-built, clay-render look) ──────────────── +const HajiFiruz3D: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const enter = spring({ frame: frame - 20, fps, config: { damping: 14, stiffness: 60 } }); + const bob = Math.abs(Math.sin(frame / 7)) * 0.12 * enter; + const sway = Math.sin(frame / 7) * 0.08 * enter; + const armSwing = Math.sin(frame / 3.2) * 0.5; + const y = -0.5 + bob; + + return ( + + {/* tunic (tapered) */} + + + + + {/* gold hem + sash */} + + + + + + + + + {/* buttons */} + {[0.75, 0.6, 0.45].map((by, i) => ( + + + + + ))} + {/* head */} + + + + + {/* eyes */} + {[-0.12, 0.12].map((ex, i) => ( + + + + + ))} + {/* smile */} + + + + + {/* hat (cone) + band + tip */} + + + + + + + + + + + + + {/* right arm (down, swings) */} + + + + + + + + + + + {/* left arm raised with tambourine */} + + + + + + + + + + + + + + + {Array.from({ length: 8 }).map((_, i) => ( + + + + + ))} + + + {/* legs */} + {[-0.16, 0.16].map((lx, i) => ( + + + + + ))} + + ); +}; + +// ── Haft-Sin props ─────────────────────────────────────────────────────────── +const Candle: React.FC<{ x: number; z: number }> = ({ x, z }) => { + const frame = useCurrentFrame(); + const flick = 1 + Math.sin(frame / 4) * 0.12; + return ( + + + + + + + + + + + + ); +}; + +const Egg: React.FC<{ x: number; z: number; color: string }> = ({ x, z, color }) => ( + + + + +); + +const FishBowl3D: React.FC<{ x: number; z: number }> = ({ x, z }) => { + const frame = useCurrentFrame(); + const fishX = Math.sin(frame / 20) * 0.12; + return ( + + {/* water */} + + + + + {/* fish */} + + + + + {/* glass */} + + + + + + ); +}; + +const Sabzeh3D: React.FC<{ x: number; z: number }> = ({ x, z }) => { + const frame = useCurrentFrame(); + return ( + + + + + + {Array.from({ length: 30 }).map((_, i) => { + const a = rand(i) * Math.PI * 2; + const r = rand(i + 5) * 0.22; + const sway = Math.sin(frame / 18 + i) * 0.08; + return ( + + + + + ); + })} + + ); +}; + +const Petals3D: React.FC = () => { + const frame = useCurrentFrame(); + return ( + + {Array.from({ length: 30 }).map((_, i) => { + const x = (rand(i) - 0.5) * 9; + const z = (rand(i + 3) - 0.5) * 4 - 1; + const fall = 4 - ((frame * (0.01 + rand(i) * 0.02) + rand(i + 7) * 6) % 7); + const rot = frame * 0.03 * (1 + rand(i)); + return ( + + + + + ); + })} + + ); +}; + +// ── Scene ──────────────────────────────────────────────────────────────────── +const Scene: React.FC = () => { + const frame = useCurrentFrame(); + const orbit = Math.sin(frame / 110) * 0.22; + return ( + + + + + + + + + + + + + {/* reflective floor */} + + + + + + + + + + + + + + + + ); +}; + +export const Nowruz3D: React.FC = ({ + greeting, + subtitle, + message, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + const L = useLayout(); + + const gSpring = spring({ frame: frame - 120, fps: 30, config: { damping: 13, stiffness: 90 } }); + const gScale = interpolate(gSpring, [0, 1], [0.6, 1]); + const gOp = interpolate(frame, [120, 140], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const subOp = interpolate(frame, [142, 162], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const msgOp = interpolate(frame, [156, 176], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + return ( + + + + + + + + + + + + + {/* Greeting overlay */} + +
+ {greeting} +
+
+ {subtitle} +
+
+ {message} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/NowruzGreeting.tsx b/services/remotion/src/compositions/NowruzGreeting.tsx new file mode 100644 index 0000000..34a61ef --- /dev/null +++ b/services/remotion/src/compositions/NowruzGreeting.tsx @@ -0,0 +1,307 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout, type Layout } from "../lib/aspect"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; + +export const nowruzGreetingSchema = z.object({ + greeting: z.string(), + subtitle: z.string(), + message: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +// Fixed scene palette (the colour props tint the sky / gold / accent / text). +const GREEN = "#5cc85c"; +const GREEN_D = "#3da53d"; +const SKIN = "#f0b486"; +const SKIN_D = "#d99a68"; + +// ── Sun with rotating rays ─────────────────────────────────────────────────── +const Sun: React.FC<{ L: Layout; gold: string; intro: number }> = ({ L, gold, intro }) => { + const frame = useCurrentFrame(); + const cx = L.width * 0.84; + const cy = L.height * 0.16; + const r = L.vmin(70); + return ( + + + {Array.from({ length: 14 }).map((_, i) => ( + + ))} + + + + + + ); +}; + +// ── Drifting blossom petals ────────────────────────────────────────────────── +const Petals: React.FC<{ L: Layout }> = ({ L }) => { + const frame = useCurrentFrame(); + const COLORS = ["#ffd1e8", "#ffe3f1", "#ffc1dd", "#fff0f6"]; + return ( + + {Array.from({ length: 26 }).map((_, i) => { + const x = rand(i) * L.width + Math.sin((frame + i * 30) / 30) * L.vmin(40); + const fall = ((rand(i + 9) * L.height) + frame * (1 + rand(i) * 2) * L.unit) % (L.height + 40) - 20; + const s = L.vmin(7 + rand(i + 3) * 8); + const rot = (frame + i * 40) * (i % 2 ? 2 : -2); + const appear = interpolate(frame, [0, 25], [0, 1], { extrapolateRight: "clamp" }); + return ( + + + + ); + })} + + ); +}; + +// ── Sabzeh (growing grass) + a tulip ───────────────────────────────────────── +const Sabzeh: React.FC<{ L: Layout; x: number; groundY: number; delay: number; scale?: number }> = ({ L, x, groundY, delay, scale = 1 }) => { + const frame = useCurrentFrame(); + const grow = spring({ frame: frame - delay, fps: 30, config: { damping: 12, stiffness: 80 } }); + const h = L.vmin(70) * scale; + return ( + + {Array.from({ length: 7 }).map((_, i) => { + const lean = (i - 3) * 7 + Math.sin((frame + i * 20) / 22) * 5; + const bh = h * (0.7 + (i % 3) * 0.15) * grow; + return ( + + ); + })} + + ); +}; + +const Tulip: React.FC<{ L: Layout; x: number; groundY: number; delay: number; color: string; scale?: number }> = ({ L, x, groundY, delay, color, scale = 1 }) => { + const frame = useCurrentFrame(); + const grow = spring({ frame: frame - delay, fps: 30, config: { damping: 11, stiffness: 90 } }); + const stem = L.vmin(90) * scale * grow; + const sway = Math.sin((frame + x) / 26) * 4; + const bw = L.vmin(34) * scale * grow; + return ( + + + + + + + + + ); +}; + +// ── Goldfish bowl ──────────────────────────────────────────────────────────── +const FishBowl: React.FC<{ L: Layout; x: number; groundY: number; intro: number }> = ({ L, x, groundY, intro }) => { + const frame = useCurrentFrame(); + const R = L.vmin(78); + const swim = Math.sin(frame / 18) * R * 0.4; + const dir = Math.cos(frame / 18) >= 0 ? 1 : -1; + const tail = Math.sin(frame / 5) * 14; + return ( + + {/* water */} + + + + + {/* bubbles */} + {[0, 1, 2].map((i) => { + const by = (R - ((frame * (1 + i) * L.unit + i * 30) % (R * 1.4))); + return ; + })} + {/* fish */} + + + + + + + + + {/* glass */} + + + + ); +}; + +// ── Butterfly ──────────────────────────────────────────────────────────────── +const Butterfly: React.FC<{ L: Layout; i: number; color: string }> = ({ L, i, color }) => { + const frame = useCurrentFrame(); + const t = frame + i * 25; + const x = interpolate((t * (0.8 + (i % 3) * 0.3)) % (L.width + 200), [0, L.width + 200], [-100, L.width + 100]); + const y = L.height * (0.3 + (i % 3) * 0.12) + Math.sin(t / 14) * L.vmin(50); + const flap = Math.abs(Math.sin(frame / 3.5)); + const s = L.vmin(20); + const appear = interpolate(frame, [90, 110], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + return ( + + + + + + + + + + ); +}; + +// ── Haji Firuz (modern stylized) ───────────────────────────────────────────── +const HajiFiruz: React.FC<{ L: Layout; x: number; groundY: number; red: string; gold: string }> = ({ L, x, groundY, red, gold }) => { + const frame = useCurrentFrame(); + const inF = frame - 60; + // Hop in from the left with a couple of bounces, then dance in place. + const entr = spring({ frame: inF, fps: 30, config: { damping: 12, stiffness: 70 } }); + const startX = -L.width * 0.4; + const px = startX + (x - startX) * entr; + const danceY = inF > 0 ? -Math.abs(Math.sin(frame / 7)) * L.vmin(22) : 0; + const sway = inF > 0 ? Math.sin(frame / 7) * 4 : 0; + // Tambourine shake. + const tam = Math.sin(frame / 3.2) * 18; + const scale = L.vmin(3.2); // unit -> px; character drawn ~ 110 units tall + + if (frame < 60 && entr === 0) return null; + + return ( + + {/* shadow */} + + {/* legs (bouncing) */} + + + + + + + {/* body (red tunic with gold trim) */} + + + + + {/* left arm raised holding tambourine */} + + + {/* tambourine */} + + + + {Array.from({ length: 8 }).map((_, i) => ( + + ))} + + + {/* right arm */} + + + + + {/* head */} + + + {/* face — friendly stylized */} + + + + + + {/* conical hat */} + + + + + ); +}; + +// ── Composition ────────────────────────────────────────────────────────────── +export const NowruzGreeting: React.FC = ({ + greeting, + subtitle, + message, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps, width, height } = useVideoConfig(); + const L = useLayout(); + const gold = accentColor; + const red = secondaryColor; + const sky = backgroundColor; + const groundY = height * 0.84; + + const intro = interpolate(frame, [0, 24], [0, 1], { extrapolateRight: "clamp" }); + const fishIntro = spring({ frame: frame - 95, fps, config: { damping: 12, stiffness: 90 } }); + + // Greeting reveal. + const gSpring = spring({ frame: frame - 150, fps, config: { damping: 12, stiffness: 90, mass: 0.8 } }); + const gScale = interpolate(gSpring, [0, 1], [0.5, 1]); + const gOpacity = interpolate(frame, [150, 168], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const subOpacity = interpolate(frame, [172, 192], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const msgOpacity = interpolate(frame, [186, 206], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + return ( + + {/* Sky gradient */} + + + + + + + {/* Ground */} + + + + {/* Plants along the ground */} + + + + + + + + + + {[0, 1, 2].map((i) => ( + + ))} + + + + + {/* Greeting */} + +
+ {greeting} +
+
+ {subtitle} +
+
+ {message} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/Opener.tsx b/services/remotion/src/compositions/Opener.tsx new file mode 100644 index 0000000..f7fcaad --- /dev/null +++ b/services/remotion/src/compositions/Opener.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba } from "../lib/anim"; + +export const openerSchema = z.object({ + kicker: z.string(), + title: z.string(), + subtitle: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const Opener: React.FC = ({ + kicker, + title, + subtitle, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const L = useLayout(); + const kick = useReveal(8, { from: 24 }); + const sub = useReveal(40, { from: 30 }); + + // Title wipes up behind a clipping mask. + const titleY = interpolate(frame, [18, 44], [L.vmin(140), 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + const lineW = interpolate(frame, [30, 60], [0, L.vmin(260)], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + + return ( + + + + {/* Two framing bars that draw in from the sides */} + {[0, 1].map((i) => { + const w = interpolate(frame, [4, 26], [0, L.vmin(620)], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); + return ( +
+ ); + })} + + +
+ {kicker} +
+ +
+
+ {title} +
+
+ +
+ +
+ {subtitle} +
+ + + ); +}; diff --git a/services/remotion/src/compositions/Promo3D.tsx b/services/remotion/src/compositions/Promo3D.tsx new file mode 100644 index 0000000..fa6117b --- /dev/null +++ b/services/remotion/src/compositions/Promo3D.tsx @@ -0,0 +1,158 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { ThreeCanvas } from "@remotion/three"; +import { RoundedBox } from "@react-three/drei"; +import * as THREE from "three"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { hexToRgba, rand } from "../lib/anim"; +import { StudioEnv, StudioFloor, StudioLights, StudioEffects, Confetti3D } from "../lib/three-kit"; + +export const promo3DSchema = z.object({ + badge: z.string(), + headline: z.string(), + subtext: z.string(), + cta: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +const Gift: React.FC<{ size: number; color: string; ribbon: string }> = ({ size, color, ribbon }) => ( + + + + + {/* crossing ribbons */} + + + + + + + + + {/* bow */} + + {[-1, 1].map((s) => ( + + + + + ))} + + + + + + +); + +const FloatingGift: React.FC<{ i: number; accent: string; secondary: string }> = ({ i, accent, secondary }) => { + const frame = useCurrentFrame(); + const ang = rand(i) * Math.PI * 2; + const radius = 2.4 + rand(i + 5) * 2.0; + const depth = -1 - rand(i + 9) * 3.5; + const x = Math.cos(ang + frame * 0.004 * (0.5 + rand(i) * 0.5)) * radius; + const y = -0.1 + Math.sin(ang * 1.4 + frame / 40) * (1.0 + rand(i + 3) * 1.0); + const size = 0.4 + rand(i + 7) * 0.4; + const colA = i % 2 === 0 ? accent : secondary; + const colB = i % 2 === 0 ? "#fde047" : "#ffffff"; + const appear = interpolate(frame, [6 + i * 2, 32 + i * 2], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + return ( + + + + ); +}; + +const Scene: React.FC<{ accent: string; secondary: string }> = ({ accent, secondary }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const pop = spring({ frame: frame - 10, fps, config: { damping: 11, stiffness: 80 } }); + const heroScale = interpolate(pop, [0, 1], [0, 1.3]); + const heroBob = Math.sin(frame / 22) * 0.12; + const heroSpin = Math.sin(frame / 60) * 0.5; + const orbit = Math.sin(frame / 120) * 0.18; + return ( + + + + + {/* hero gift */} + + + + {Array.from({ length: 9 }).map((_, i) => ( + + ))} + + + ); +}; + +export const Promo3D: React.FC = ({ + badge, + headline, + subtext, + cta, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { width, height, fps } = useVideoConfig(); + const L = useLayout(); + + const badgePop = spring({ frame: frame - 40, fps, config: { damping: 9, stiffness: 130 } }); + const badgeScale = interpolate(badgePop, [0, 1], [0, 1]); + const badgeWobble = Math.sin(frame / 14) * 5; + const headOp = interpolate(frame, [70, 90], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const headY = interpolate(frame, [70, 92], [L.vmin(40), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const subOp = interpolate(frame, [92, 112], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const ctaPop = spring({ frame: frame - 116, fps, config: { damping: 11, stiffness: 120 } }); + const ctaScale = interpolate(ctaPop, [0, 1], [0.6, 1]); + const ctaGlow = 0.4 + 0.3 * Math.sin(frame / 10); + + return ( + + + + + + + + + {/* discount badge */} +
+ {badge} +
+
+ {headline} +
+
+ {subtext} +
+
+ {cta} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/QuoteCard.tsx b/services/remotion/src/compositions/QuoteCard.tsx new file mode 100644 index 0000000..3343dac --- /dev/null +++ b/services/remotion/src/compositions/QuoteCard.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba, mixHex } from "../lib/anim"; + +export const quoteCardSchema = z.object({ + quote: z.string(), + author: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const QuoteCard: React.FC = ({ + quote, + author, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const L = useLayout(); + const words = quote.split(/\s+/).filter(Boolean); + + const markOp = interpolate(frame, [0, 14], [0, 1], { extrapolateRight: "clamp" }); + const tail = 14 + words.length * 3 + 8; + const ruleW = interpolate(frame, [tail, tail + 18], [0, L.vmin(120)], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const auth = useReveal(tail + 8, { from: 20 }); + + return ( + + + + +
+ ” +
+ +
+ {words.map((w, i) => { + const s = spring({ frame: frame - (12 + i * 3), fps, config: { damping: 18, mass: 0.6, stiffness: 110 } }); + return ( + + {w} + + ); + })} +
+ +
+
+ {author} +
+ + + ); +}; diff --git a/services/remotion/src/compositions/SalePromo.tsx b/services/remotion/src/compositions/SalePromo.tsx new file mode 100644 index 0000000..1e8c358 --- /dev/null +++ b/services/remotion/src/compositions/SalePromo.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba } from "../lib/anim"; + +export const salePromoSchema = z.object({ + badge: z.string(), + headline: z.string(), + subtext: z.string(), + cta: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const SalePromo: React.FC = ({ + badge, + headline, + subtext, + cta, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const L = useLayout(); + + const badgePop = spring({ frame: frame - 6, fps, config: { damping: 9, stiffness: 140, mass: 0.6 } }); + const badgeScale = interpolate(badgePop, [0, 1], [0, 1]); + const badgeWobble = Math.sin(frame / 14) * 6; + + const head = useReveal(22, { from: 50 }); + const sub = useReveal(40, { from: 28 }); + const ctaR = useReveal(56, { from: 24, damping: 11 }); + const ctaGlow = 0.4 + 0.3 * Math.sin(frame / 10); + + return ( + + + + + {/* Discount badge */} +
+ {badge} +
+ +
+ {headline} +
+ +
+ {subtext} +
+ +
+ {cta} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/Slideshow.tsx b/services/remotion/src/compositions/Slideshow.tsx new file mode 100644 index 0000000..7d8d425 --- /dev/null +++ b/services/remotion/src/compositions/Slideshow.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground } from "../lib/kit"; +import { hexToRgba, mixHex } from "../lib/anim"; + +export const slideshowSchema = z.object({ + title: z.string(), + slide1: z.string(), + slide2: z.string(), + slide3: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const Slideshow: React.FC = ({ + title, + slide1, + slide2, + slide3, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + const L = useLayout(); + + const slides = [slide1, slide2, slide3]; + const per = durationInFrames / (slides.length + 0.5); // leave a beat for the title + const titleEnd = per * 0.5; + + const titleOp = interpolate(frame, [4, 18, titleEnd - 8, titleEnd], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const titleScale = interpolate(frame, [4, titleEnd], [0.9, 1.05], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + + return ( + + + + {/* Title card */} + +
+ {title} +
+
+ + {/* Slides */} + {slides.map((s, i) => { + const start = titleEnd + i * per; + const local = frame - start; + if (local < -10 || local > per + 10) return null; + const op = interpolate(local, [0, 14, per - 14, per], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + const x = interpolate(local, [0, 18], [L.vmin(60), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + return ( + +
+ {String(i + 1).padStart(2, "0")} +
+
+ {s} +
+
+ ); + })} + + {/* Progress dots */} +
+ {slides.map((_, i) => { + const start = titleEnd + i * per; + const active = frame >= start && frame < start + per; + return
; + })} +
+ + ); +}; diff --git a/services/remotion/src/compositions/Three3DTest.tsx b/services/remotion/src/compositions/Three3DTest.tsx new file mode 100644 index 0000000..200983b --- /dev/null +++ b/services/remotion/src/compositions/Three3DTest.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { ThreeCanvas } from "@remotion/three"; +import { useCurrentFrame, useVideoConfig } from "remotion"; + +const Spinning: React.FC = () => { + const frame = useCurrentFrame(); + return ( + + + + + ); +}; + +export const Three3DTest: React.FC = () => { + const { width, height } = useVideoConfig(); + return ( + + + + + + + ); +}; diff --git a/services/remotion/src/compositions/VerticalStory.tsx b/services/remotion/src/compositions/VerticalStory.tsx new file mode 100644 index 0000000..b6fe27a --- /dev/null +++ b/services/remotion/src/compositions/VerticalStory.tsx @@ -0,0 +1,229 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Easing, +} from "remotion"; +import { zColor } from "@remotion/zod-types"; +import { z } from "zod"; +import { hexToRgba, mixHex, rand } from "../lib/anim"; + +export const verticalStorySchema = z.object({ + kicker: z.string(), + line1: z.string(), + line2: z.string(), + line3: z.string(), + ctaText: z.string(), + accentColor: zColor(), + secondaryColor: zColor(), + backgroundColor: zColor(), +}); + +type Props = z.infer; + +// ── Diagonal animated gradient + floating dust ─────────────────────────────── + +const StoryBackground: React.FC<{ + bg: string; + accent: string; + secondary: string; +}> = ({ bg, accent, secondary }) => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + const shift = interpolate(frame, [0, 180], [0, 60]); + return ( + + + + {Array.from({ length: 22 }).map((_, i) => { + const x = rand(i) * width; + const baseY = rand(i + 9) * height; + const y = (baseY - frame * (0.6 + rand(i) * 1.2)) % height; + const size = 2 + rand(i + 3) * 5; + const tw = 0.2 + 0.6 * Math.abs(Math.sin((frame + i * 20) / 16)); + return ( +
+ ); + })} + + + ); +}; + +const StoryLine: React.FC<{ + text: string; + delay: number; + highlight?: string; +}> = ({ text, delay, highlight }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const s = spring({ + frame: frame - delay, + fps, + config: { damping: 16, mass: 0.8, stiffness: 100 }, + }); + const y = interpolate(s, [0, 1], [70, 0]); + const op = interpolate(s, [0, 1], [0, 1]); + return ( +
+ {text} +
+ ); +}; + +export const VerticalStory: React.FC = ({ + kicker, + line1, + line2, + line3, + ctaText, + accentColor, + secondaryColor, + backgroundColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const kickerOp = interpolate(frame, [4, 20], [0, 1], { + extrapolateRight: "clamp", + }); + + const ctaSpring = spring({ + frame: frame - 64, + fps, + config: { damping: 12, stiffness: 120 }, + }); + const ctaScale = interpolate(ctaSpring, [0, 1], [0.6, 1]); + const ctaOp = interpolate(ctaSpring, [0, 1], [0, 1]); + const arrowBounce = Math.sin(frame / 8) * 8; + + return ( + + + +
+ {kicker} +
+ + + + +
+ + {/* Swipe-up CTA pinned near the bottom */} +
+
+ ⌃ +
+
+ {ctaText} +
+
+
+ ); +}; diff --git a/services/remotion/src/compositions/YouTubeIntro.tsx b/services/remotion/src/compositions/YouTubeIntro.tsx new file mode 100644 index 0000000..8d3e006 --- /dev/null +++ b/services/remotion/src/compositions/YouTubeIntro.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { z } from "zod"; +import { colorSchema } from "../lib/branding"; +import { FONT } from "../lib/fonts"; +import { useLayout } from "../lib/aspect"; +import { BrandBackground, useReveal } from "../lib/kit"; +import { hexToRgba } from "../lib/anim"; + +export const youTubeIntroSchema = z.object({ + channelName: z.string(), + subtitle: z.string(), + cta: z.string(), + ...colorSchema, +}); + +type Props = z.infer; + +export const YouTubeIntro: React.FC = ({ + channelName, + subtitle, + cta, + accentColor, + secondaryColor, + backgroundColor, + textColor, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const L = useLayout(); + + const playPop = spring({ frame, fps, config: { damping: 11, stiffness: 130, mass: 0.7 } }); + const playScale = interpolate(playPop, [0, 1], [0, 1]); + const ripple = (frame % 45) / 45; + + const name = useReveal(28, { from: 40 }); + const sub = useReveal(44, { from: 26 }); + const bell = useReveal(60, { from: 22, damping: 11 }); + const bellWiggle = Math.sin(frame / 5) * (frame > 60 && frame < 90 ? 10 : 0); + + return ( + + + + + {/* Play button with ripple */} +
+
+
+
+
+
+ +
+ {channelName} +
+
+ {subtitle} +
+ + {/* Subscribe pill */} +
+ 🔔 + {cta} +
+ + + ); +}; diff --git a/services/remotion/src/index.ts b/services/remotion/src/index.ts new file mode 100644 index 0000000..f31c790 --- /dev/null +++ b/services/remotion/src/index.ts @@ -0,0 +1,4 @@ +import { registerRoot } from "remotion"; +import { RemotionRoot } from "./Root"; + +registerRoot(RemotionRoot); diff --git a/services/remotion/src/lib/anim.ts b/services/remotion/src/lib/anim.ts new file mode 100644 index 0000000..9009927 --- /dev/null +++ b/services/remotion/src/lib/anim.ts @@ -0,0 +1,30 @@ +// Shared color + animation helpers for FlatRender code-based templates. + +/** Parse a #rrggbb hex string into [r,g,b] (0..255). */ +export function hexRgb(hex: string): [number, number, number] { + const p = hex.replace("#", ""); + const i = parseInt(p.length === 3 ? p.replace(/(.)/g, "$1$1") : p, 16); + return [(i >> 16) & 255, (i >> 8) & 255, i & 255]; +} + +/** Mix two hex colors by t (0..1) → rgb() string. Cheap linear blend. */ +export function mixHex(a: string, b: string, t: number): string { + const [ar, ag, ab] = hexRgb(a); + const [br, bg, bb] = hexRgb(b); + const r = Math.round(ar + (br - ar) * t); + const g = Math.round(ag + (bg - ag) * t); + const bl = Math.round(ab + (bb - ab) * t); + return `rgb(${r}, ${g}, ${bl})`; +} + +/** #rrggbb + alpha → rgba() string. */ +export function hexToRgba(hex: string, alpha: number): string { + const [r, g, b] = hexRgb(hex); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +/** Stable per-index pseudo-random in [0,1) — no Math.random, renders stay deterministic. */ +export function rand(seed: number): number { + const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453; + return x - Math.floor(x); +} diff --git a/services/remotion/src/lib/aspect.ts b/services/remotion/src/lib/aspect.ts new file mode 100644 index 0000000..06809e8 --- /dev/null +++ b/services/remotion/src/lib/aspect.ts @@ -0,0 +1,53 @@ +import { useVideoConfig } from "remotion"; + +export type AspectKind = "wide" | "square" | "tall"; + +export interface Layout { + kind: AspectKind; + width: number; + height: number; + isWide: boolean; + isSquare: boolean; + isTall: boolean; + /** Smaller side / 1000 — a resolution-independent sizing unit. */ + unit: number; + /** Convenience scalers relative to the design baseline (1080 short side). */ + vmin: (n: number) => number; +} + +/** Classify a width×height into one of the three supported aspects. */ +export function classify(width: number, height: number): AspectKind { + const r = width / height; + if (r > 1.2) return "wide"; + if (r < 0.85) return "tall"; + return "square"; +} + +/** + * Aspect-aware layout tokens. Templates read this to adapt one design to 16:9, + * 1:1 and 9:16 — sizing off the shorter side so type and shapes stay balanced + * in every frame. + */ +export function useLayout(): Layout { + const { width, height } = useVideoConfig(); + const kind = classify(width, height); + const short = Math.min(width, height); + const unit = short / 1000; + return { + kind, + width, + height, + isWide: kind === "wide", + isSquare: kind === "square", + isTall: kind === "tall", + unit, + vmin: (n: number) => (n * short) / 1080, + }; +} + +/** The three aspect presets every template is registered in. */ +export const ASPECTS: { id: string; width: number; height: number; label: string }[] = [ + { id: "16x9", width: 1920, height: 1080, label: "16:9" }, + { id: "1x1", width: 1080, height: 1080, label: "1:1" }, + { id: "9x16", width: 1080, height: 1920, label: "9:16" }, +]; diff --git a/services/remotion/src/lib/branding.ts b/services/remotion/src/lib/branding.ts new file mode 100644 index 0000000..197c0a0 --- /dev/null +++ b/services/remotion/src/lib/branding.ts @@ -0,0 +1,46 @@ +import { zColor } from "@remotion/zod-types"; +import { z } from "zod"; + +/** + * FlatRender brand palette + shared template helpers. Every template exposes the + * same colour props (accent / secondary / background / text) so the studio can + * render one consistent "colour change" control set across all of them. + */ +export const BRAND = { + blue: "#3ba7ff", + purple: "#a855f7", + cyan: "#22d3ee", + pink: "#fb7185", + amber: "#f59e0b", + green: "#34d399", + ink: "#04060f", + white: "#ffffff", +} as const; + +/** The shared colour schema every template extends. */ +export const colorSchema = { + accentColor: zColor(), + secondaryColor: zColor(), + backgroundColor: zColor(), + textColor: zColor(), +}; + +/** Default brand colours used by most templates' defaultProps. */ +export const defaultColors = { + accentColor: BRAND.blue, + secondaryColor: BRAND.purple, + backgroundColor: BRAND.ink, + textColor: BRAND.white, +}; + +export type ColorProps = { + accentColor: string; + secondaryColor: string; + backgroundColor: string; + textColor: string; +}; + +/** The FlatRender wordmark, in Persian. */ +export const BRAND_NAME_FA = "فلت‌رندر"; + +export const zHex = (hex: string) => hex as z.infer>; diff --git a/services/remotion/src/lib/fonts.ts b/services/remotion/src/lib/fonts.ts new file mode 100644 index 0000000..250abdb --- /dev/null +++ b/services/remotion/src/lib/fonts.ts @@ -0,0 +1,35 @@ +import { continueRender, delayRender, staticFile } from "remotion"; + +/** The Persian/Latin font used across all FlatRender templates. */ +export const FONT = "Vazirmatn"; + +const WEIGHTS = [400, 600, 700, 800, 900]; + +let started = false; + +/** + * Loads the local Vazirmatn weights (Persian RTL + Latin) and blocks rendering + * until they're ready, so text never renders in a fallback font. Idempotent — + * runs once when this module is first imported. + */ +function loadVazirmatn() { + if (started || typeof document === "undefined") return; + started = true; + for (const w of WEIGHTS) { + const handle = delayRender(`vazirmatn-${w}`); + const face = new FontFace( + FONT, + `url(${staticFile(`fonts/vazirmatn-${w}.woff2`)}) format('woff2')`, + { weight: String(w) } + ); + face + .load() + .then((loaded) => { + document.fonts.add(loaded); + continueRender(handle); + }) + .catch(() => continueRender(handle)); + } +} + +loadVazirmatn(); diff --git a/services/remotion/src/lib/kit.tsx b/services/remotion/src/lib/kit.tsx new file mode 100644 index 0000000..bb6361f --- /dev/null +++ b/services/remotion/src/lib/kit.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { hexToRgba, rand } from "./anim"; +import { useLayout } from "./aspect"; + +/** + * Shared FlatRender template kit: a branded animated background and a couple of + * reveal helpers, so each template focuses on its own content + copy. + */ + +export const BrandBackground: React.FC<{ + bg: string; + accent: string; + secondary: string; + /** number of floating particles (0 = none) */ + particles?: number; + /** add a soft moving second glow */ + nebula?: boolean; +}> = ({ bg, accent, secondary, particles = 0, nebula = true }) => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + const L = useLayout(); + const drift = Math.sin(frame / 55) * 30; + return ( + + + {nebula && ( + + )} + {particles > 0 && ( + + + {Array.from({ length: particles }).map((_, i) => { + const x = rand(i) * width; + const baseY = rand(i + 7) * height; + const y = (baseY - frame * (0.5 + rand(i) * 1.1) * L.unit + height) % height; + const s = L.vmin(2 + (rand(i + 3) * 4)); + const tw = 0.25 + 0.6 * Math.abs(Math.sin((frame + i * 17) / 11)); + const c = i % 3 === 0 ? secondary : accent; + return ( + + ); + })} + + + )} + + + ); +}; + +/** Spring-based reveal: returns opacity + translateY(px) + scale for a delay. */ +export function useReveal(delay: number, opts?: { from?: number; damping?: number }) { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const L = useLayout(); + const s = spring({ frame: frame - delay, fps, config: { damping: opts?.damping ?? 16, stiffness: 90, mass: 0.85 } }); + return { + opacity: interpolate(s, [0, 1], [0, 1]), + y: interpolate(s, [0, 1], [L.vmin(opts?.from ?? 40), 0]), + scale: interpolate(s, [0, 1], [0.9, 1]), + }; +} diff --git a/services/remotion/src/lib/three-kit.tsx b/services/remotion/src/lib/three-kit.tsx new file mode 100644 index 0000000..4083f72 --- /dev/null +++ b/services/remotion/src/lib/three-kit.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { useCurrentFrame } from "remotion"; +import { Environment, Lightformer, MeshReflectorMaterial } from "@react-three/drei"; +import { EffectComposer, Bloom, DepthOfField, Vignette } from "@react-three/postprocessing"; +import { rand } from "./anim"; + +/** + * Shared max-quality 3D building blocks so every 3D template gets the same + * studio look: offline environment reflections (Lightformers — no HDR needed), + * a reflective floor, a 3-point + colour-rim light rig, and a post-processing + * stack (bloom + depth-of-field + vignette). All verified to render headless + * via ANGLE. + */ + +export const StudioEnv: React.FC = () => ( + + + + + +); + +export const StudioLights: React.FC<{ accent: string; secondary: string }> = ({ accent, secondary }) => ( + <> + + + + + +); + +export const StudioFloor: React.FC<{ color?: string; y?: number }> = ({ color = "#241d33", y = -0.62 }) => ( + + + + +); + +export const StudioEffects: React.FC<{ bloom?: number; focus?: number; bokeh?: number; vignette?: number }> = ({ + bloom = 0.75, + focus = 0.013, + bokeh = 3, + vignette = 0.55, +}) => ( + + + + + +); + +/** Falling 3D confetti planes. */ +export const Confetti3D: React.FC<{ colors: string[]; count?: number; top?: number }> = ({ colors, count = 36, top = 4.5 }) => { + const frame = useCurrentFrame(); + return ( + + {Array.from({ length: count }).map((_, i) => { + const x = (rand(i) - 0.5) * 9; + const z = (rand(i + 3) - 0.5) * 4 - 0.5; + const span = 9; + const y = top - ((frame * (0.03 + rand(i) * 0.04) + rand(i + 7) * span) % span); + const rot = frame * 0.06 * (1 + rand(i)); + return ( + + + + + ); + })} + + ); +}; diff --git a/services/remotion/src/templates.tsx b/services/remotion/src/templates.tsx new file mode 100644 index 0000000..997dec0 --- /dev/null +++ b/services/remotion/src/templates.tsx @@ -0,0 +1,226 @@ +/** + * Registry of FlatRender branded templates. Each entry is rendered into the + * three supported aspects (16:9 / 1:1 / 9:16) by Root.tsx, producing composition + * ids like "LogoMotion-16x9". Every template uses Persian text presets + the + * shared colour props so the studio can offer one consistent edit experience. + */ +import React from "react"; +import type { AnyZodObject } from "zod"; +import { BRAND } from "./lib/branding"; + +import { LogoMotion, logoMotionSchema } from "./compositions/LogoMotion"; +import { Opener, openerSchema } from "./compositions/Opener"; +import { InstaPromo, instaPromoSchema } from "./compositions/InstaPromo"; +import { YouTubeIntro, youTubeIntroSchema } from "./compositions/YouTubeIntro"; +import { Slideshow, slideshowSchema } from "./compositions/Slideshow"; +import { HappyBirthday, happyBirthdaySchema } from "./compositions/HappyBirthday"; +import { SalePromo, salePromoSchema } from "./compositions/SalePromo"; +import { QuoteCard, quoteCardSchema } from "./compositions/QuoteCard"; +import { EventInvite, eventInviteSchema } from "./compositions/EventInvite"; +import { Countdown, countdownSchema } from "./compositions/Countdown"; +import { GlitterReveal, glitterRevealSchema } from "./compositions/GlitterReveal"; +import { NowruzGreeting, nowruzGreetingSchema } from "./compositions/NowruzGreeting"; +import { Hero3D, hero3DSchema } from "./compositions/Hero3D"; +import { Nowruz3D, nowruz3DSchema } from "./compositions/Nowruz3D"; +import { Birthday3D, birthday3DSchema } from "./compositions/Birthday3D"; +import { Promo3D, promo3DSchema } from "./compositions/Promo3D"; + +export interface TemplateDef { + /** Base id; the registered composition ids are `${id}-${aspect}`. */ + id: string; + /** Persian display name (used when seeding the site catalog). */ + name: string; + /** Short Persian description for the catalog. */ + description: string; + component: React.FC; // eslint-disable-line @typescript-eslint/no-explicit-any + schema: AnyZodObject; + durationSec: number; + defaultProps: Record; +} + +const c = (accent: string, secondary: string, bg: string) => ({ + accentColor: accent, + secondaryColor: secondary, + backgroundColor: bg, + textColor: BRAND.white, +}); + +export const TEMPLATES: TemplateDef[] = [ + { + id: "LogoMotion", + name: "موشن لوگو", + description: "نمایش حرفه‌ای لوگو و نام برند با درخشش و حرکت", + component: LogoMotion, + schema: logoMotionSchema, + durationSec: 5, + defaultProps: { brandText: "فلت‌رندر", tagline: "موشن، ساده و حرفه‌ای", ...c(BRAND.blue, BRAND.purple, "#04060f") }, + }, + { + id: "Opener", + name: "تیتراژ آغازین", + description: "شروع سینمایی برای ویدیو با عنوان و زیرعنوان", + component: Opener, + schema: openerSchema, + durationSec: 5, + defaultProps: { kicker: "تقدیم می‌کند", title: "یک شروع تازه", subtitle: "داستان شما از همین‌جا آغاز می‌شود", ...c(BRAND.cyan, "#6366f1", "#0a0a12") }, + }, + { + id: "InstaPromo", + name: "تبلیغ پیج اینستاگرام", + description: "معرفی و تبلیغ صفحهٔ اینستاگرام با دعوت به فالو", + component: InstaPromo, + schema: instaPromoSchema, + durationSec: 5, + defaultProps: { handle: "@flatrender", headline: "پیج ما را دنبال کنید", subtext: "هر روز محتوای تازه و الهام‌بخش", cta: "فالو کنید", ...c(BRAND.pink, BRAND.amber, "#140a12") }, + }, + { + id: "YouTubeIntro", + name: "اینترو کانال یوتیوب", + description: "اینترو حرفه‌ای کانال یوتیوب با دکمهٔ سابسکرایب", + component: YouTubeIntro, + schema: youTubeIntroSchema, + durationSec: 5, + defaultProps: { channelName: "کانال فلت‌رندر", subtitle: "آموزش، ترفند و انگیزه", cta: "سابسکرایب کنید", ...c("#ff4d4d", BRAND.purple, "#0c0810") }, + }, + { + id: "Slideshow", + name: "اسلایدشو", + description: "نمایش پشت‌سرهم چند پیام یا ویژگی به‌صورت اسلاید", + component: Slideshow, + schema: slideshowSchema, + durationSec: 9, + defaultProps: { title: "چرا فلت‌رندر؟", slide1: "ساخت ویدیو در چند دقیقه", slide2: "بدون نیاز به دانش فنی", slide3: "خروجی با کیفیت حرفه‌ای", ...c(BRAND.green, "#3b82f6", "#060b0a") }, + }, + { + id: "HappyBirthday", + name: "تولدت مبارک", + description: "کارت تبریک تولد با کاغذرنگی و نام شخص", + component: HappyBirthday, + schema: happyBirthdaySchema, + durationSec: 6, + defaultProps: { greeting: "تولدت مبارک", name: "سارا", message: "بهترین‌ها را برایت آرزومندیم 🎉", ...c(BRAND.pink, "#fde047", "#140a18") }, + }, + { + id: "SalePromo", + name: "فروش ویژه", + description: "بنر تبلیغاتی فروش و تخفیف با دعوت به خرید", + component: SalePromo, + schema: salePromoSchema, + durationSec: 5, + defaultProps: { badge: "۵۰٪ تخفیف", headline: "فروش ویژهٔ پایان فصل", subtext: "فقط تا پایان همین هفته", cta: "همین حالا خرید کنید", ...c(BRAND.amber, BRAND.pink, "#120a08") }, + }, + { + id: "QuoteCard", + name: "کارت نقل‌قول", + description: "نمایش جملهٔ انگیزشی یا نقل‌قول با نام گوینده", + component: QuoteCard, + schema: quoteCardSchema, + durationSec: 6, + defaultProps: { quote: "موفقیت، مجموع تلاش‌های کوچکِ هر روز است.", author: "فلت‌رندر", ...c(BRAND.cyan, "#6366f1", "#0a0a12") }, + }, + { + id: "EventInvite", + name: "دعوت‌نامهٔ رویداد", + description: "دعوت‌نامهٔ شیک برای رویداد با تاریخ و مکان", + component: EventInvite, + schema: eventInviteSchema, + durationSec: 6, + defaultProps: { kicker: "دعوت‌نامه", eventTitle: "همایش سالانهٔ نوآوری", date: "۱۵ مهر ۱۴۰۳", location: "تهران، سالن همایش‌ها", cta: "ثبت‌نام کنید", ...c(BRAND.purple, BRAND.blue, "#0a0814") }, + }, + { + id: "Countdown", + name: "شمارش معکوس", + description: "شمارش معکوس هیجان‌انگیز برای شروع یک رویداد", + component: Countdown, + schema: countdownSchema, + durationSec: 8, + defaultProps: { title: "شروع رویداد تا", startNumber: 5, goText: "شروع!", subtitle: "آماده‌اید؟", ...c(BRAND.blue, BRAND.cyan, "#04060f") }, + }, + { + id: "GlitterReveal", + name: "نمایش لوگو با غبار درخشان", + description: "نمایش جادویی لوگو با ذرات درخشان؛ لوگو و متن قابل ویرایش", + component: GlitterReveal, + schema: glitterRevealSchema, + durationSec: 6, + defaultProps: { brandText: "فلت‌رندر", tagline: "موشن، ساده و حرفه‌ای", logoUrl: "", ...c(BRAND.blue, BRAND.purple, "#05040e") }, + }, + { + id: "NowruzGreeting", + name: "تبریک نوروز", + description: "صحنهٔ بهاری نوروز با شخصیت‌های متحرک؛ حاجی‌فیروز، ماهی قرمز و سبزه", + component: NowruzGreeting, + schema: nowruzGreetingSchema, + durationSec: 7.5, + defaultProps: { + greeting: "نوروز مبارک", + subtitle: "سال نو پیروز و شادمان", + message: "۱۴۰۶", + accentColor: "#f5b942", + secondaryColor: "#e23b3b", + backgroundColor: "#1fb6b0", + textColor: "#fdf6e3", + }, + }, + { + id: "Hero3D", + name: "نمایش سه‌بعدی برند", + description: "نمایش حرفه‌ای و سه‌بعدی لوگو و برند با نورپردازی و جلوه‌های واقعی", + component: Hero3D, + schema: hero3DSchema, + durationSec: 6, + defaultProps: { brandText: "فلت‌رندر", tagline: "موشن، ساده و حرفه‌ای", ...c(BRAND.blue, BRAND.purple, "#04060f") }, + }, + { + id: "Nowruz3D", + name: "تبریک نوروز سه‌بعدی", + description: "صحنهٔ سه‌بعدی نوروز با حاجی‌فیروز، سفرهٔ هفت‌سین و نورپردازی سینمایی", + component: Nowruz3D, + schema: nowruz3DSchema, + durationSec: 7, + defaultProps: { + greeting: "نوروز مبارک", + subtitle: "سال نو پیروز و شادمان", + message: "۱۴۰۶", + accentColor: "#f5c542", + secondaryColor: "#e23b3b", + backgroundColor: "#1a1228", + textColor: "#fdf6e3", + }, + }, + { + id: "Birthday3D", + name: "تولد سه‌بعدی", + description: "صحنهٔ سه‌بعدی تولد با کیک و شمع‌های روشن، بادکنک و کاغذرنگی", + component: Birthday3D, + schema: birthday3DSchema, + durationSec: 6, + defaultProps: { + greeting: "تولدت مبارک", + name: "سارا", + message: "بهترین‌ها را برایت آرزومندیم 🎉", + accentColor: "#fb7185", + secondaryColor: "#a855f7", + backgroundColor: "#1a1226", + textColor: "#fdf6e3", + }, + }, + { + id: "Promo3D", + name: "فروش ویژه سه‌بعدی", + description: "تبلیغ سه‌بعدی فروش و تخفیف با جعبه‌های هدیه و نورپردازی سینمایی", + component: Promo3D, + schema: promo3DSchema, + durationSec: 6, + defaultProps: { + badge: "۵۰٪ تخفیف", + headline: "فروش ویژهٔ پایان فصل", + subtext: "فقط تا پایان همین هفته", + cta: "همین حالا خرید کنید", + accentColor: "#f59e0b", + secondaryColor: "#fb7185", + backgroundColor: "#140e1f", + textColor: "#ffffff", + }, + }, +]; diff --git a/services/remotion/tsconfig.json b/services/remotion/tsconfig.json new file mode 100644 index 0000000..edc5768 --- /dev/null +++ b/services/remotion/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["react"] + }, + "include": ["src"] +} diff --git a/services/render/internal/db/db.go b/services/render/internal/db/db.go index 12ed94d..20f21f9 100644 --- a/services/render/internal/db/db.go +++ b/services/render/internal/db/db.go @@ -666,6 +666,42 @@ func (s *Store) GetTemplateCompName(ctx context.Context, originalProjectID uuid. return *comp, nil } +// TemplateRenderConfig describes how a template should be rendered. +type TemplateRenderConfig struct { + // Engine is "AfterEffects" or "Remotion". + Engine string + // CompName is the composition to render: render_aep_comp for AE, or + // render_remotion_comp for Remotion. + CompName string +} + +// GetTemplateRenderConfig resolves the render engine + composition for a template. +// For Remotion templates the composition id comes from render_remotion_comp; for +// After Effects it comes from render_aep_comp. Defaults to AfterEffects when the +// render_engine column is missing/empty (older rows pre-migration). +func (s *Store) GetTemplateRenderConfig(ctx context.Context, originalProjectID uuid.UUID) (TemplateRenderConfig, error) { + var engine *string + var aepComp, remotionComp *string + err := s.pool.QueryRow(ctx, + `SELECT render_engine, render_aep_comp, render_remotion_comp + FROM content.projects WHERE id = $1`, originalProjectID).Scan(&engine, &aepComp, &remotionComp) + if err != nil { + return TemplateRenderConfig{}, err + } + cfg := TemplateRenderConfig{Engine: "AfterEffects"} + if engine != nil && *engine != "" { + cfg.Engine = *engine + } + if cfg.Engine == "Remotion" { + if remotionComp != nil { + cfg.CompName = *remotionComp + } + } else if aepComp != nil { + cfg.CompName = *aepComp + } + return cfg, nil +} + // GetRenderBindings returns the user's edited input values for a saved project so the // node can write them into the AE project before rendering (the render binder). Only // inputs with a non-empty value are returned (defaults are already in the template). diff --git a/services/render/internal/handlers/internal.go b/services/render/internal/handlers/internal.go index b69b450..4dff721 100644 --- a/services/render/internal/handlers/internal.go +++ b/services/render/internal/handlers/internal.go @@ -282,10 +282,14 @@ func (h *InternalHandler) Claim(c *gin.Context) { // and reused by every render of that template. A .zip is a full AE project // bundle (.aep + footage/fonts) the node must extract before rendering. // Errors are non-fatal — the node agent falls back to mock render when URL is empty. + // Resolve the render engine + composition for this template. Remotion + // templates are code-based and need no .aep download. + rcfg, _ := h.store.GetTemplateRenderConfig(c.Request.Context(), job.OriginalProjectID) + aepURL := "" isBundle := false bundleMD5 := "" - if h.minio != nil { + if rcfg.Engine != "Remotion" && h.minio != nil { candidates := []struct { name string bundle bool @@ -311,11 +315,12 @@ func (h *InternalHandler) Claim(c *gin.Context) { } } - // Composition to render (-comp). Non-fatal: empty → node uses the render queue. - compName, _ := h.store.GetTemplateCompName(c.Request.Context(), job.OriginalProjectID) + // Composition to render: AE comp (-comp) or the Remotion composition id. + // Non-fatal: empty → AE node uses the render queue. + compName := rcfg.CompName // User's edited input values → the node writes them into the AE project before - // rendering (render binder). Non-fatal: empty → renders template defaults. + // rendering, or passes them as Remotion --props. Non-fatal: empty → template defaults. bindings, _ := h.store.GetRenderBindings(c.Request.Context(), job.SavedProjectID) c.JSON(http.StatusOK, models.ClaimedJob{ @@ -326,6 +331,7 @@ func (h *InternalHandler) Claim(c *gin.Context) { FrameRate: job.FrameRate, HasMusic: job.HasMusic, HasVoiceover: job.HasVoiceover, + Engine: rcfg.Engine, AEPDownloadURL: aepURL, IsBundle: isBundle, BundleMD5: bundleMD5, diff --git a/services/render/internal/models/models.go b/services/render/internal/models/models.go index 40b2cf6..b0bdd92 100644 --- a/services/render/internal/models/models.go +++ b/services/render/internal/models/models.go @@ -422,6 +422,9 @@ type ClaimedJob struct { FrameRate int `json:"frame_rate"` HasMusic bool `json:"has_music"` HasVoiceover bool `json:"has_voiceover"` + // Engine selects the render engine: "AfterEffects" (default) or "Remotion". + // For Remotion jobs CompName is the composition id and AEPDownloadURL is empty. + Engine string `json:"engine,omitempty"` // AEPDownloadURL is a presigned MinIO GET URL for the .aep project file // (or .zip bundle). Valid for 2 hours. Empty when the template is not yet uploaded. AEPDownloadURL string `json:"aep_download_url,omitempty"` diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 0d70af1..557b499 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -4,6 +4,7 @@ import { notFound } from "next/navigation"; import { getMessages, getTranslations } from "next-intl/server"; import { NextIntlClientProvider } from "next-intl"; +import { ComingSoonOverlay } from "@/components/layout/ComingSoonOverlay"; import { DirectionProvider } from "@/components/layout/DirectionProvider"; import { SiteChrome } from "@/components/layout/SiteChrome"; import { GlobalRenderProgress } from "@/components/render/GlobalRenderProgress"; @@ -117,6 +118,7 @@ export default async function LocaleLayout({ {children} + diff --git a/src/app/[locale]/pricing/page.tsx b/src/app/[locale]/pricing/page.tsx index 014bbdb..1575660 100644 --- a/src/app/[locale]/pricing/page.tsx +++ b/src/app/[locale]/pricing/page.tsx @@ -6,7 +6,7 @@ import { createPageMetadata } from "@/lib/metadata"; export const metadata: Metadata = createPageMetadata({ title: "Pricing", description: - "Compare FlatRender Lite, Pro, and Business plans. Monthly or yearly billing with templates, exports, and AI tools for creators.", + "FlatRender pricing is by the second, not by the video. Every plan grants a monthly bucket of render-seconds; a render costs the video length × a quality multiplier.", path: "/pricing", }); diff --git a/src/app/api/checkout/route.ts b/src/app/api/checkout/route.ts index b9979a5..d0e60d9 100644 --- a/src/app/api/checkout/route.ts +++ b/src/app/api/checkout/route.ts @@ -7,25 +7,16 @@ import { getAccessToken } from "@/lib/auth/session"; export const dynamic = "force-dynamic"; const checkoutSchema = z.object({ - plan: z.enum(["pro", "business"]), - billing: z.enum(["monthly", "annual"]), + planId: z.string().uuid(), }); -interface V2Plan { - id: string; - code: string; - name: string; - billing_period: string; -} - /** - * Start a plan purchase through the V2 Identity/payments flow. + * Start a seconds-based plan purchase through the V2 Identity/payments flow. * - * Replaces the direct Stripe Checkout + Supabase profile loop. We resolve the - * requested plan ("pro"/"business" × "monthly"/"annual") to a plan GUID via - * `/v1/plans` (codes follow the `pro_monthly` / `business_annual` convention), - * then POST `/v1/users/me/plan/purchase`. The payments service owns the gateway - * (ZarinPal/Stripe) and returns a redirect URL we hand back to the client. + * The pricing page is data-driven (plans come from `/v1/plans`), so the client + * sends the chosen plan's GUID directly. We POST `/v1/users/me/plan/purchase`; + * the payments service owns the gateway (ZarinPal broker) and returns a redirect + * URL we hand back to the client. */ export async function POST(request: Request) { const token = await getAccessToken(); @@ -45,37 +36,7 @@ export async function POST(request: Request) { const parsed = checkoutSchema.safeParse(body); if (!parsed.success) { - return NextResponse.json( - { error: "Invalid plan or billing period." }, - { status: 400 } - ); - } - - const { plan, billing } = parsed.data; - const targetCode = `${plan}_${billing === "annual" ? "annual" : "monthly"}`; - - // Resolve plan code → GUID. Plans are public, but pass the token so tenant - // overrides resolve correctly. - const plansRes = await fetch(gatewayUrl("/v1/plans"), { - cache: "no-store", - headers: { Accept: "application/json", Authorization: `Bearer ${token}` }, - }); - const plansJson = plansRes.ok - ? ((await plansRes.json().catch(() => null)) as { data?: V2Plan[] } | null) - : null; - const match = plansJson?.data?.find( - (p) => p.code?.toLowerCase() === targetCode - ); - - if (!match) { - return NextResponse.json( - { - error: - "This plan is not available yet. Please try again later or contact support.", - code: "PLAN_NOT_AVAILABLE", - }, - { status: 503 } - ); + return NextResponse.json({ error: "Invalid plan." }, { status: 400 }); } const purchaseRes = await fetch(gatewayUrl("/v1/users/me/plan/purchase"), { @@ -86,7 +47,7 @@ export async function POST(request: Request) { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ plan_id: match.id }), + body: JSON.stringify({ plan_id: parsed.data.planId }), }); if (!purchaseRes.ok) { diff --git a/src/components/admin/TemplatesAdmin.tsx b/src/components/admin/TemplatesAdmin.tsx index bc98133..be69d07 100644 --- a/src/components/admin/TemplatesAdmin.tsx +++ b/src/components/admin/TemplatesAdmin.tsx @@ -34,10 +34,12 @@ interface Detail extends Container { interface Proj { id: string; name: string; aspect?: string | null; resolution?: string; aep_file_url?: string | null; aep_file_size_bytes?: number | null; render_aep_comp?: string; + render_engine?: string; render_remotion_comp?: string | null; } const PRIMARY_MODES = ["FIX", "FLEXIBLE", "MockUp", "MusicVisualizer", "VoiceOver"]; const RESOLUTIONS = ["HD", "FullHD", "TwoK", "FourK"]; +const RENDER_ENGINES = ["AfterEffects", "Remotion"]; const CHOOSE_MODES = ["FIX", "FLEXIBLE", "MockUp", "MusicVisualizer", "VoiceOver"]; const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]"; @@ -94,7 +96,11 @@ export function TemplatesAdmin() { const res = await fetch(api(`projects/${p.id}`), { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ aspect: p.aspect ?? "", resolution: p.resolution }), + body: JSON.stringify({ + aspect: p.aspect ?? "", resolution: p.resolution, + render_engine: p.render_engine || "AfterEffects", + render_remotion_comp: p.render_remotion_comp ?? null, + }), }); if (!res.ok) { const d = await res.json().catch(() => null); @@ -139,7 +145,11 @@ export function TemplatesAdmin() { setSavingProj(p.id); await fetch(api(`projects/${p.id}/aep`), { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ render_aep_comp: p.render_aep_comp || "flatrender" }), + body: JSON.stringify({ + render_aep_comp: p.render_aep_comp || "flatrender", + render_engine: p.render_engine || "AfterEffects", + render_remotion_comp: p.render_remotion_comp ?? null, + }), }); setSavingProj(null); }; @@ -352,9 +362,14 @@ export function TemplatesAdmin() {
{p.name} - {p.aep_file_url - ? AE ✓ - : بدون فایل AE} + {(p.render_engine || "AfterEffects") === "Remotion" + ? Remotion + : p.aep_file_url + ? AE ✓ + : بدون فایل AE} + updateProj(p.id, { aspect: e.target.value })} /> updateProj(p.id, { render_aep_comp: e.target.value })} /> - + {(p.render_engine || "AfterEffects") === "Remotion" ? ( +
+ +
+ updateProj(p.id, { render_remotion_comp: e.target.value })} /> + +
+

قالب کدمحور — بدون نیاز به فایل افترافکت.

-
+ ) : ( + <> +
+ + attachAep(p, u)} accept=".aep,.aepx,.zip" /> +
+
+ +
+ updateProj(p.id, { render_aep_comp: e.target.value })} /> + +
+
+ + )}
+

+ می‌توانید همین حالا نسخهٔ آزمایشی را ببینید +

+
+ + + )} + + ); +} diff --git a/src/components/sections/Pricing.tsx b/src/components/sections/Pricing.tsx index aa108e3..3d958ff 100644 --- a/src/components/sections/Pricing.tsx +++ b/src/components/sections/Pricing.tsx @@ -1,53 +1,17 @@ -"use client"; - -import { useState } from "react"; -import { useLocale, useTranslations } from "next-intl"; - -import { cfgVal } from "@/lib/home-layout"; -import { PricingBillingToggle } from "@/components/sections/PricingBillingToggle"; -import { PricingCard } from "@/components/sections/PricingCard"; -import { PricingCompareTable } from "@/components/sections/PricingCompareTable"; -import { PricingFreeBanner } from "@/components/sections/PricingFreeBanner"; -import { PricingSectionShell } from "@/components/sections/PricingBackground"; -import { SectionReveal } from "@/components/sections/SectionReveal"; -import type { BillingPeriod } from "@/components/sections/pricing-data"; -import { PRICING_TIERS } from "@/components/sections/pricing-data"; +import { fetchPlans } from "@/lib/plans-catalog"; +import { PricingPlans } from "@/components/sections/PricingPlans"; export interface PricingProps { className?: string; config?: Record; } -export function Pricing({ className, config }: PricingProps) { - const t = useTranslations("pricing"); - const locale = useLocale(); - const [billing, setBilling] = useState("annual"); - - return ( - - -

- {cfgVal(config, "heading", locale) ?? t("heading")} -

-
- - - - - - - - - - - {PRICING_TIERS.map((tier) => ( - - ))} - - - - - -
- ); +/** + * Pricing section (server). Reads the seconds-based plans from the Identity + * service and renders the data-driven pricing UI. Used both on the homepage + * (via the section manager `config`) and the standalone /pricing page. + */ +export async function Pricing({ className, config }: PricingProps) { + const plans = await fetchPlans(); + return ; } diff --git a/src/components/sections/PricingBuyButton.tsx b/src/components/sections/PricingBuyButton.tsx new file mode 100644 index 0000000..eee6a57 --- /dev/null +++ b/src/components/sections/PricingBuyButton.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import type { SecondsPlan } from "@/lib/plans-catalog"; + +interface Props { + plan: SecondsPlan; + className?: string; +} + +/** + * Starts a plan purchase. Free plans send the user into onboarding; paid plans + * POST /api/checkout (which resolves the plan → broker redirect URL) and + * forward the browser to the payment gateway. A 401 bounces to sign-in. + */ +export function PricingBuyButton({ plan, className }: Props) { + const t = useTranslations("pricing"); + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const isFree = plan.priceTomans <= 0; + + const onClick = async () => { + setError(null); + if (isFree) { + router.push("/dashboard"); + return; + } + setLoading(true); + try { + const res = await fetch("/api/checkout", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ planId: plan.id }), + }); + if (res.status === 401) { + router.push("/auth?next=/pricing"); + return; + } + const data = (await res.json().catch(() => null)) as { + url?: string; + error?: string; + } | null; + if (res.ok && data?.url) { + window.location.href = data.url; + return; + } + setError(data?.error ?? "خطا"); + setLoading(false); + } catch { + setError("خطا"); + setLoading(false); + } + }; + + return ( +
+ + {error && ( +

{error}

+ )} +
+ ); +} diff --git a/src/components/sections/PricingPlans.tsx b/src/components/sections/PricingPlans.tsx new file mode 100644 index 0000000..00055c9 --- /dev/null +++ b/src/components/sections/PricingPlans.tsx @@ -0,0 +1,251 @@ +"use client"; + +import { useLocale, useTranslations } from "next-intl"; + +import { cfgVal } from "@/lib/home-layout"; +import { SectionReveal } from "@/components/sections/SectionReveal"; +import { PricingSectionShell } from "@/components/sections/PricingBackground"; +import { PricingBuyButton } from "@/components/sections/PricingBuyButton"; +import { SecondsCalculator } from "@/components/sections/SecondsCalculator"; +import { + RESOLUTION_MULTIPLIERS, + RESOLUTION_ORDER, + formatToman, + type SecondsPlan, +} from "@/lib/plans-catalog"; + +export interface PricingPlansProps { + plans: SecondsPlan[]; + className?: string; + config?: Record; + currentPlanCode?: string | null; +} + +export function PricingPlans({ + plans, + className, + config, + currentPlanCode, +}: PricingPlansProps) { + const t = useTranslations("pricing"); + const locale = useLocale(); + + return ( + + +

+ {cfgVal(config, "heading", locale) ?? t("heading")} +

+

+ {cfgVal(config, "subheading", locale) ?? t("subheading")} +

+
+ + {plans.length === 0 ? ( +

{t("emptyState")}

+ ) : ( + + {plans.map((plan) => ( + + ))} + + )} + + {/* Seconds calculator + quality multiplier */} + +
+ +
+
+ +
+
+ + {/* FAQ */} + +

+ {t("faqTitle")} +

+
+ {[1, 2, 3].map((i) => ( +
+ + {t(`faqQ${i}` as "faqQ1")} + +

+ {t(`faqA${i}` as "faqA1")} +

+
+ ))} +
+
+
+ ); +} + +// ── Plan card ──────────────────────────────────────────────────────────────── + +function PlanCard({ + plan, + locale, + isCurrent, +}: { + plan: SecondsPlan; + locale: string; + isCurrent: boolean; +}) { + const t = useTranslations("pricing"); + const isFree = plan.priceTomans <= 0; + + const features: string[] = [ + t("featSeconds", { seconds: plan.secondsCharge.toLocaleString(locale === "fa" ? "fa-IR" : "en-US") }), + t("featResolution", { res: plan.maxResolution }), + plan.parallelRenders > 1 + ? t("featParallel", { n: plan.parallelRenders }) + : t("featParallelOne"), + t("featStorage", { gb: plan.storageGb }), + plan.renderSpeedFactor > 1 + ? t("featSpeed", { factor: plan.renderSpeedFactor }) + : null, + plan.watermark ? t("featWatermarkOn") : t("featWatermarkOff"), + ].filter(Boolean) as string[]; + + return ( +
+ {plan.isFeatured && ( + + {t("mostPopular")} + + )} + +
+ +

+ {plan.name} +

+
+ {plan.description && ( +

+ {plan.description} +

+ )} + + {/* Price */} +
+ {isFree ? ( +

{t("free")}

+ ) : ( +
+ + {formatToman(plan.priceTomans, locale)} + + + {t("toman")} {t("perMonthSuffix")} + +
+ )} + {plan.beforePriceTomans && plan.beforePriceTomans > plan.priceTomans && ( +

+ {formatToman(plan.beforePriceTomans, locale)} {t("toman")} +

+ )} +
+ + {/* Features */} +
    + {features.map((f, i) => ( +
  • + + {f} +
  • + ))} +
+ + {/* CTA */} +
+ {isCurrent ? ( +
+ {t("currentPlan")} +
+ ) : ( + + )} +
+
+ ); +} + +// ── Quality multiplier table ───────────────────────────────────────────────── + +function MultiplierTable() { + const t = useTranslations("pricing"); + return ( +
+

+ {t("multiplierTitle")} +

+

{t("multiplierDesc")}

+ + + + + + + + + {RESOLUTION_ORDER.map((r) => ( + + + + + ))} + +
{t("multiplierColRes")}{t("multiplierColMul")}
{r} + ×{RESOLUTION_MULTIPLIERS[r]} +
+
+ ); +} + +function CheckIcon() { + return ( + + + + ); +} diff --git a/src/components/sections/SecondsCalculator.tsx b/src/components/sections/SecondsCalculator.tsx new file mode 100644 index 0000000..2b2cf15 --- /dev/null +++ b/src/components/sections/SecondsCalculator.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; + +import { + RESOLUTION_ORDER, + renderSecondsCost, + type SecondsPlan, +} from "@/lib/plans-catalog"; + +interface Props { + plans: SecondsPlan[]; +} + +/** + * Interactive "how many seconds do I need" helper. The user picks a video length + * and resolution; we show the per-render cost (length × resolution multiplier) + * and how many such videos each paid plan's monthly seconds would cover. + */ +export function SecondsCalculator({ plans }: Props) { + const t = useTranslations("pricing"); + const [length, setLength] = useState(15); + const [resolution, setResolution] = useState("720p"); + + const cost = useMemo( + () => renderSecondsCost(length, resolution), + [length, resolution] + ); + + const paidPlans = plans.filter((p) => p.priceTomans > 0); + + return ( +
+

+ {t("calcTitle")} +

+

{t("calcDesc")}

+ +
+ {/* Length */} +
+ + setLength(Number(e.target.value))} + className="w-full accent-indigo-600" + /> +
+ + {/* Resolution */} +
+ +
+ {RESOLUTION_ORDER.map((r) => ( + + ))} +
+
+
+ + {/* Cost */} +
+
+

{t("calcCost")}

+

+ {cost}{" "} + + {t("calcSecondsUnit")} + +

+
+ {paidPlans.length > 0 && ( +
+

+ {t("calcRendersWith")} +

+
+ {paidPlans.map((p) => ( + + + {p.name} + + {": "} + {t("calcVideosFmt", { + count: Math.floor(p.secondsCharge / Math.max(cost, 1)), + })} + + ))} +
+
+ )} +
+
+ ); +} diff --git a/src/components/templates/TemplateDetailPreview.tsx b/src/components/templates/TemplateDetailPreview.tsx index 950dd31..97e9b9f 100644 --- a/src/components/templates/TemplateDetailPreview.tsx +++ b/src/components/templates/TemplateDetailPreview.tsx @@ -35,8 +35,8 @@ export function TemplateDetailPreview({ const t = useTranslations("auto.componentsTemplatesTemplateDetailPreview"); const [isPlaying, setIsPlaying] = useState(false); const aspectOptions = getTemplateDetailAspectRatios(template); - const posterSrc = getVideoTemplateImageSrc(template.id); - const videoSrc = getTemplatePreviewVideoSrc(template.id); + const posterSrc = template.coverImageUrl ?? getVideoTemplateImageSrc(template.id); + const videoSrc = template.previewVideoUrl ?? getTemplatePreviewVideoSrc(template.id); return (
diff --git a/src/components/templates/video/VideoTemplateCompactCard.tsx b/src/components/templates/video/VideoTemplateCompactCard.tsx index 1487841..05e44a1 100644 --- a/src/components/templates/video/VideoTemplateCompactCard.tsx +++ b/src/components/templates/video/VideoTemplateCompactCard.tsx @@ -27,8 +27,8 @@ export function VideoTemplateCompactCard({ const t = useTranslations("auto.componentsTemplatesVideoVideoTemplateCompactCard"); const [isHovered, setIsHovered] = useState(false); const videoRef = useRef(null); - const imageSrc = getVideoTemplateImageSrc(template.id); - const videoSrc = getTemplatePreviewVideoSrc(template.id); + const imageSrc = template.coverImageUrl ?? getVideoTemplateImageSrc(template.id); + const videoSrc = template.previewVideoUrl ?? getTemplatePreviewVideoSrc(template.id); const detailHref = `/templates/${template.id}`; const handleEnter = useCallback(() => { diff --git a/src/lib/plans-catalog.ts b/src/lib/plans-catalog.ts new file mode 100644 index 0000000..178faea --- /dev/null +++ b/src/lib/plans-catalog.ts @@ -0,0 +1,131 @@ +/** + * Server-side catalog of the V2 seconds-based subscription plans. + * + * FlatRender charges by **render-seconds**, not by number of videos. Each plan + * grants a monthly bucket of render-seconds (`secondsCharge`); a render consumes + * seconds equal to the video's length × a resolution multiplier (see + * RESOLUTION_MULTIPLIERS). Plans live in the Identity service and are read here + * from the gateway so prices/quotas are editable in admin without code changes. + */ + +import { gatewayUrl } from "@/lib/api/gateway"; + +// ── Resolution multipliers: render-seconds = videoLengthSec × multiplier ─────── +// Baseline is 720p (×1). Higher resolutions cost proportionally more seconds. +export const RESOLUTION_MULTIPLIERS: Record = { + "360p": 0.5, + "540p": 0.75, + "720p": 1, + "1080p": 2, + "2K": 3, + "4K": 4, +}; + +export const RESOLUTION_ORDER = ["360p", "540p", "720p", "1080p", "2K", "4K"]; + +/** Multiplier for a resolution label, defaulting to 1 for unknown labels. */ +export function resolutionMultiplier(resolution: string): number { + return RESOLUTION_MULTIPLIERS[resolution] ?? 1; +} + +/** Render-seconds a single render consumes at the given length + resolution. */ +export function renderSecondsCost(lengthSec: number, resolution: string): number { + return Math.ceil(lengthSec * resolutionMultiplier(resolution)); +} + +// ── Types ────────────────────────────────────────────────────────────────── + +export interface SecondsPlan { + id: string; + code: string; + name: string; + description?: string | null; + /** Display price in Toman (price_minor is stored in Rial = Toman × 10). */ + priceTomans: number; + beforePriceTomans?: number | null; + currency: string; + /** Render-seconds granted per billing period. */ + secondsCharge: number; + monthlyRendersQuota?: number | null; + storageGb: number; + parallelRenders: number; + maxResolution: string; + renderSpeedFactor: number; + isFeatured: boolean; + color?: string | null; + /** True when renders are watermarked (free tier). */ + watermark: boolean; +} + +interface V2PlanRow { + id: string; + code: string; + name: string; + description?: string | null; + price_minor: number; + before_price_minor?: number | null; + currency: string; + seconds_charge: number; + monthly_renders_quota?: number | null; + storage_gb: number; + parallel_renders: number; + max_resolution: string; + render_speed_factor: number | string; + is_featured: boolean; + color?: string | null; + features?: Record | null; +} + +function mapPlan(p: V2PlanRow): SecondsPlan { + return { + id: p.id, + code: p.code, + name: p.name, + description: p.description, + priceTomans: Math.round((p.price_minor ?? 0) / 10), + beforePriceTomans: + p.before_price_minor != null ? Math.round(p.before_price_minor / 10) : null, + currency: p.currency, + secondsCharge: p.seconds_charge, + monthlyRendersQuota: p.monthly_renders_quota, + storageGb: p.storage_gb, + parallelRenders: p.parallel_renders, + maxResolution: p.max_resolution, + renderSpeedFactor: Number(p.render_speed_factor), + isFeatured: p.is_featured, + color: p.color, + watermark: Boolean(p.features?.watermark), + }; +} + +/** + * Fetch the active plans from the Identity service (public, ISR-cached). + * Returns an empty array when the gateway is unset/unreachable so the page can + * render a graceful empty state instead of throwing. + */ +export async function fetchPlans(): Promise { + // Retry once: a single slow/cold gateway response shouldn't blank the page. + for (let attempt = 0; attempt < 2; attempt++) { + try { + const res = await fetch(gatewayUrl("/v1/plans"), { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(6000), + next: { revalidate: 60 }, + }); + if (!res.ok) continue; + const json = (await res.json().catch(() => null)) as { + data?: V2PlanRow[]; + } | null; + const rows = json?.data ?? []; + return rows.map(mapPlan).sort((a, b) => a.priceTomans - b.priceTomans); + } catch { + // fall through to the next attempt, then to the empty fallback + } + } + return []; +} + +/** Format a Toman amount with locale digit grouping. */ +export function formatToman(amount: number, locale: string): string { + return new Intl.NumberFormat(locale === "fa" ? "fa-IR" : "en-US").format(amount); +} diff --git a/src/lib/video-templates-catalog.ts b/src/lib/video-templates-catalog.ts index e74e6d3..0be0cf2 100644 --- a/src/lib/video-templates-catalog.ts +++ b/src/lib/video-templates-catalog.ts @@ -84,6 +84,10 @@ export interface VideoCatalogTemplate { scriptToVideo: boolean; description?: string; isNew?: boolean; + /** Real thumbnail + preview video URLs (from the admin catalog). When present, + * used instead of the generated placeholder. */ + coverImageUrl?: string; + previewVideoUrl?: string; } export function getVideoTemplateCategoryLabel( @@ -517,9 +521,11 @@ export function adminProjectToCatalogTemplate( premium: false, sceneCount: 0, supports4k: false, - colorChange: false, + colorChange: true, scriptToVideo: false, description: p.description, isNew: true, + coverImageUrl: p.coverImageUrl, + previewVideoUrl: p.previewVideoUrl, }; } diff --git a/src/middleware.ts b/src/middleware.ts index 538b768..be22ca2 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -132,6 +132,8 @@ export async function middleware(request: NextRequest) { export const config = { matcher: [ - "/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + // Skip API, Next internals, and any path with a file extension (static + // assets: images, video, fonts, etc. served from public/). + "/((?!api|_next/static|_next/image|favicon.ico|.*\\.[a-zA-Z0-9]+$).*)", ], }; diff --git a/tsconfig.json b/tsconfig.json index 7b28589..ffa253d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "services", "coming-soon", "deploy"] }