diff --git a/README.md b/README.md
index 9e96096..4bb1ace 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,25 @@ An open-source AI image generation studio powered by [Muapi.ai](https://muapi.ai
## ✨ Features
+- **Cinema Studio** — specialized interface for photorealistic cinematic shots with pro camera controls (Lens, Focal Length, Aperture)
- **Multi-Model Support** — Switch between 20+ AI image generation models (Flux, Nano Banana, Ideogram, Midjourney, SDXL, and more)
- **Smart Controls** — Dynamic aspect ratio and resolution pickers that adapt to each model's capabilities
-- **Generation History** — Browse, revisit, and download all your past generations (persisted in browser storage)
-- **Image Download** — One-click download of generated images in full resolution
+- **Generation History** — Browse, revisit, and download all your past generations (persisted in browser storage). Now with a persistent sidebar in Cinema Studio.
+- **Image Download** — One-click download of generated images in full resolution (up to 4K)
- **API Key Management** — Secure API key storage in browser localStorage (never sent to any server except Muapi)
- **Responsive Design** — Works seamlessly on desktop and mobile with dark glassmorphism UI
+### 🎥 Cinema Studio Controls
+
+The **Cinema Studio** offers precise control over the virtual camera, translating your choices into optimized prompt modifiers:
+
+| Category | Available Options |
+| :--- | :--- |
+| **Cameras** | Modular 8K Digital, Full-Frame Cine Digital, Grand Format 70mm Film, Studio Digital S35, Classic 16mm Film, Premium Large Format Digital |
+| **Lenses** | Creative Tilt, Compact Anamorphic, Extreme Macro, 70s Cinema Prime, Classic Anamorphic, Premium Modern Prime, Warm Cinema Prime, Swirl Bokeh Portrait, Vintage Prime, Halation Diffusion, Clinical Sharp Prime |
+| **Focal Lengths** | 8mm (Ultra-Wide), 14mm, 24mm, 35mm (Human Eye), 50mm (Portrait), 85mm (Tight Portrait) |
+| **Apertures** | f/1.4 (Shallow DoF), f/4 (Balanced), f/11 (Deep Focus) |
+
## 🚀 Quick Start
### Prerequisites
@@ -48,7 +60,9 @@ npm run preview
```
src/
├── components/
-│ ├── ImageStudio.js # Main studio with prompt, pickers, canvas, history
+│ ├── ImageStudio.js # Standard studio with prompt, pickers, canvas, history
+│ ├── CinemaStudio.js # Pro studio with camera controls & infinite canvas flow
+│ ├── CameraControls.js # Scrollable picker for camera/lens/focal/aperture
│ ├── Header.js # App header with settings and controls
│ ├── AuthModal.js # API key input modal
│ ├── SettingsModal.js # Settings panel for API key management
@@ -78,7 +92,7 @@ Authentication uses the `x-api-key` header. During development, a Vite proxy han
| Model | Endpoint | Resolution Options |
|-------|----------|-------------------|
| Nano Banana | `nano-banana` | — |
-| Nano Banana Pro | `nano-banana-pro` | 1K, 2K, 4K |
+| Nano Banana Pro | `nano-banana-pro` | **up to 4K** (Cinema Studio) |
| Flux Schnell | `flux-schnell-image` | — |
| Flux Dev | `flux-dev-image` | — |
| Flux Dev LoRA | `flux-dev-lora` | — |
diff --git a/docs/assets/studio_view.webp b/docs/assets/studio_view.webp
deleted file mode 100644
index dc17702..0000000
Binary files a/docs/assets/studio_view.webp and /dev/null differ
diff --git a/public/assets/cinema/70s_cinema_prime.webp b/public/assets/cinema/70s_cinema_prime.webp
new file mode 100644
index 0000000..5e8428e
Binary files /dev/null and b/public/assets/cinema/70s_cinema_prime.webp differ
diff --git a/public/assets/cinema/classic_16mm_film.webp b/public/assets/cinema/classic_16mm_film.webp
new file mode 100644
index 0000000..c5d8e07
Binary files /dev/null and b/public/assets/cinema/classic_16mm_film.webp differ
diff --git a/public/assets/cinema/classic_anamorphic.webp b/public/assets/cinema/classic_anamorphic.webp
new file mode 100644
index 0000000..903c504
Binary files /dev/null and b/public/assets/cinema/classic_anamorphic.webp differ
diff --git a/public/assets/cinema/clinical_sharp_prime.webp b/public/assets/cinema/clinical_sharp_prime.webp
new file mode 100644
index 0000000..a09d930
Binary files /dev/null and b/public/assets/cinema/clinical_sharp_prime.webp differ
diff --git a/public/assets/cinema/compact_anamorphic.webp b/public/assets/cinema/compact_anamorphic.webp
new file mode 100644
index 0000000..3fbac99
Binary files /dev/null and b/public/assets/cinema/compact_anamorphic.webp differ
diff --git a/public/assets/cinema/creative_tilt_lens.webp b/public/assets/cinema/creative_tilt_lens.webp
new file mode 100644
index 0000000..913dbde
Binary files /dev/null and b/public/assets/cinema/creative_tilt_lens.webp differ
diff --git a/public/assets/cinema/extreme_macro.webp b/public/assets/cinema/extreme_macro.webp
new file mode 100644
index 0000000..388e662
Binary files /dev/null and b/public/assets/cinema/extreme_macro.webp differ
diff --git a/public/assets/cinema/f_11.webp b/public/assets/cinema/f_11.webp
new file mode 100644
index 0000000..ff84b3a
Binary files /dev/null and b/public/assets/cinema/f_11.webp differ
diff --git a/public/assets/cinema/f_1_4.webp b/public/assets/cinema/f_1_4.webp
new file mode 100644
index 0000000..fe80fef
Binary files /dev/null and b/public/assets/cinema/f_1_4.webp differ
diff --git a/public/assets/cinema/f_4.webp b/public/assets/cinema/f_4.webp
new file mode 100644
index 0000000..174a503
Binary files /dev/null and b/public/assets/cinema/f_4.webp differ
diff --git a/public/assets/cinema/full_frame_cine_digital.webp b/public/assets/cinema/full_frame_cine_digital.webp
new file mode 100644
index 0000000..01c985c
Binary files /dev/null and b/public/assets/cinema/full_frame_cine_digital.webp differ
diff --git a/public/assets/cinema/grand_format_70mm_film.webp b/public/assets/cinema/grand_format_70mm_film.webp
new file mode 100644
index 0000000..ca55e61
Binary files /dev/null and b/public/assets/cinema/grand_format_70mm_film.webp differ
diff --git a/public/assets/cinema/halation_diffusion.webp b/public/assets/cinema/halation_diffusion.webp
new file mode 100644
index 0000000..837da61
Binary files /dev/null and b/public/assets/cinema/halation_diffusion.webp differ
diff --git a/public/assets/cinema/modular_8k_digital.webp b/public/assets/cinema/modular_8k_digital.webp
new file mode 100644
index 0000000..a10efe2
Binary files /dev/null and b/public/assets/cinema/modular_8k_digital.webp differ
diff --git a/public/assets/cinema/premium_large_format_digital.webp b/public/assets/cinema/premium_large_format_digital.webp
new file mode 100644
index 0000000..10fad36
Binary files /dev/null and b/public/assets/cinema/premium_large_format_digital.webp differ
diff --git a/public/assets/cinema/premium_modern_prime.webp b/public/assets/cinema/premium_modern_prime.webp
new file mode 100644
index 0000000..d849f46
Binary files /dev/null and b/public/assets/cinema/premium_modern_prime.webp differ
diff --git a/public/assets/cinema/studio_digital_s35.webp b/public/assets/cinema/studio_digital_s35.webp
new file mode 100644
index 0000000..0f772d2
Binary files /dev/null and b/public/assets/cinema/studio_digital_s35.webp differ
diff --git a/public/assets/cinema/swirl_bokeh_portrait.webp b/public/assets/cinema/swirl_bokeh_portrait.webp
new file mode 100644
index 0000000..54829b1
Binary files /dev/null and b/public/assets/cinema/swirl_bokeh_portrait.webp differ
diff --git a/public/assets/cinema/vintage_prime.webp b/public/assets/cinema/vintage_prime.webp
new file mode 100644
index 0000000..90010be
Binary files /dev/null and b/public/assets/cinema/vintage_prime.webp differ
diff --git a/public/assets/cinema/warm_cinema_prime.webp b/public/assets/cinema/warm_cinema_prime.webp
new file mode 100644
index 0000000..7d5909c
Binary files /dev/null and b/public/assets/cinema/warm_cinema_prime.webp differ
diff --git a/src/components/CameraControls.js b/src/components/CameraControls.js
new file mode 100644
index 0000000..84f54b6
--- /dev/null
+++ b/src/components/CameraControls.js
@@ -0,0 +1,251 @@
+
+import { CAMERA_MAP, LENS_MAP, FOCAL_PERSPECTIVE, APERTURE_EFFECT } from '../lib/promptUtils.js';
+
+const ASSET_URLS = {
+ // CAMERA
+ "Modular 8K Digital": "/assets/cinema/modular_8k_digital.webp",
+ "Full-Frame Cine Digital": "/assets/cinema/full_frame_cine_digital.webp",
+ "Grand Format 70mm Film": "/assets/cinema/grand_format_70mm_film.webp",
+ "Studio Digital S35": "/assets/cinema/studio_digital_s35.webp",
+ "Classic 16mm Film": "/assets/cinema/classic_16mm_film.webp",
+ "Premium Large Format Digital": "/assets/cinema/premium_large_format_digital.webp",
+
+ // LENS
+ "Creative Tilt Lens": "/assets/cinema/creative_tilt_lens.webp",
+ "Compact Anamorphic": "/assets/cinema/compact_anamorphic.webp",
+ "Extreme Macro": "/assets/cinema/extreme_macro.webp",
+ "70s Cinema Prime": "/assets/cinema/70s_cinema_prime.webp",
+ "Classic Anamorphic": "/assets/cinema/classic_anamorphic.webp",
+ "Premium Modern Prime": "/assets/cinema/premium_modern_prime.webp",
+ "Warm Cinema Prime": "/assets/cinema/warm_cinema_prime.webp",
+ "Swirl Bokeh Portrait": "/assets/cinema/swirl_bokeh_portrait.webp",
+ "Vintage Prime": "/assets/cinema/vintage_prime.webp",
+ "Halation Diffusion": "/assets/cinema/halation_diffusion.webp",
+ "Clinical Sharp Prime": "/assets/cinema/clinical_sharp_prime.webp",
+
+ // APERTURE
+ "f/1.4": "/assets/cinema/f_1_4.webp",
+ "f/4": "/assets/cinema/f_4.webp",
+ "f/11": "/assets/cinema/f_11.webp"
+};
+
+export function CameraControls(onChange) {
+ const container = document.createElement('div');
+ // Added padding-bottom to ensure scrollbar doesn't overlap content if visible
+ // Changed justify-center to justify-start md:justify-center to allow left-aligned scrolling on mobile
+ container.className = 'w-full flex justify-start md:justify-center gap-3 md:gap-6 py-4 md:py-8 overflow-x-auto no-scrollbar snap-x px-4 md:px-0';
+
+ let state = {
+ camera: Object.keys(CAMERA_MAP)[0],
+ lens: Object.keys(LENS_MAP)[0],
+ focal: 35,
+ aperture: "f/1.4"
+ };
+
+ const updateState = (key, value) => {
+ state[key] = value;
+ if (onChange) onChange(state);
+ };
+
+ const createColumn = (title, items, key, initialValue) => {
+ const colWrapper = document.createElement('div');
+ colWrapper.className = 'flex flex-col items-center relative w-[140px] md:w-[160px] shrink-0 snap-center group';
+
+ const viewport = document.createElement('div');
+ // Responsive height: h-[50vh] on mobile, h-[320px] on desktop
+ viewport.className = 'relative overflow-hidden w-full h-[40vh] md:h-[320px] bg-[#1a1a1a]/80 rounded-[2rem] border border-white/5 shadow-2xl backdrop-blur-xl transition-transform duration-300 hover:scale-[1.02] hover:border-white/10';
+
+ const list = document.createElement('div');
+ list.className = 'h-full overflow-y-auto no-scrollbar snap-y snap-mandatory relative z-10';
+
+ // Spacer to allow first item to be centered
+ const topSpacer = document.createElement('div');
+ topSpacer.style.height = 'calc(50% - 50px)'; // Half viewport - half item height
+ list.appendChild(topSpacer);
+
+ const topMask = document.createElement('div');
+ topMask.className = 'absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-[#1a1a1a] via-[#1a1a1a]/80 to-transparent z-20 pointer-events-none rounded-t-[2rem]';
+ viewport.appendChild(topMask);
+
+ const bottomMask = document.createElement('div');
+ bottomMask.className = 'absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-[#1a1a1a] via-[#1a1a1a]/80 to-transparent z-20 pointer-events-none rounded-b-[2rem]';
+ viewport.appendChild(bottomMask);
+
+ const glow = document.createElement('div');
+ glow.className = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4/5 h-[80px] bg-primary/5 blur-xl rounded-full pointer-events-none z-0';
+ viewport.appendChild(glow);
+
+ // DRAG TO SCROLL LOGIC
+ let isDown = false;
+ let startY;
+ let scrollTop;
+
+ list.addEventListener('mousedown', (e) => {
+ isDown = true;
+ list.classList.add('cursor-grabbing');
+ list.classList.remove('cursor-pointer', 'snap-y'); // Disable snap while dragging
+ startY = e.pageY - list.offsetTop;
+ scrollTop = list.scrollTop;
+ e.preventDefault(); // Prevent text selection
+ });
+
+ list.addEventListener('mouseleave', () => {
+ isDown = false;
+ list.classList.remove('cursor-grabbing');
+ list.classList.add('snap-y');
+ });
+
+ list.addEventListener('mouseup', () => {
+ isDown = false;
+ list.classList.remove('cursor-grabbing');
+ list.classList.add('snap-y');
+ });
+
+ list.addEventListener('mousemove', (e) => {
+ if (!isDown) return;
+ e.preventDefault();
+ const y = e.pageY - list.offsetTop;
+ const walk = (y - startY) * 1.5; // Scroll speed multiplier
+ list.scrollTop = scrollTop - walk;
+ });
+
+ items.forEach(item => {
+ const itemEl = document.createElement('div');
+ itemEl.className = `
+ h-[100px] flex flex-col items-center justify-center gap-3
+ snap-center cursor-pointer transition-all duration-500 ease-out
+ text-white p-2 select-none opacity-30 scale-75 blur-[1px]
+ `;
+
+ const imageUrl = ASSET_URLS[item];
+
+ // Image Container
+ const imgContainer = document.createElement('div');
+ imgContainer.className = `w-14 h-14 rounded-xl border border-white/10 bg-white/5 flex items-center justify-center transition-all duration-500 shadow-inner group-hover/item:border-primary/30 overflow-hidden relative`;
+
+ if (imageUrl) {
+ const img = document.createElement('img');
+ img.src = imageUrl;
+ img.className = 'w-full h-full object-cover opacity-80';
+ imgContainer.appendChild(img);
+ } else if (key === 'focal') {
+ // For Focal Length (Numbers), use text/simple graphics
+ const focalText = document.createElement('span');
+ focalText.textContent = item;
+ focalText.className = 'text-lg font-bold text-white/50';
+ imgContainer.appendChild(focalText);
+ } else {
+ // Fallback for missing images
+ imgContainer.innerHTML = `
`;
+ }
+
+ const text = document.createElement('span');
+ text.textContent = item;
+ text.className = 'text-[9px] md:text-[10px] font-bold uppercase text-center leading-tight max-w-full truncate px-1 tracking-wider';
+
+ itemEl.appendChild(imgContainer);
+ itemEl.appendChild(text);
+
+ itemEl.onclick = () => {
+ itemEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ };
+
+ itemEl.dataset.value = item;
+ list.appendChild(itemEl);
+ });
+
+ // Spacer to allow last item to be centered
+ const bottomSpacer = document.createElement('div');
+ bottomSpacer.style.height = 'calc(50% - 50px)';
+ list.appendChild(bottomSpacer);
+
+ viewport.appendChild(list);
+
+ const label = document.createElement('div');
+ label.className = 'mb-3 text-[9px] font-black text-white/40 uppercase tracking-[0.2em] text-center';
+ label.textContent = title;
+
+ colWrapper.appendChild(label);
+ colWrapper.appendChild(viewport);
+
+ // Scroll-based selection logic (Guarantees one active item)
+ const handleScroll = () => {
+ const centerY = list.scrollTop + (list.clientHeight / 2);
+ let closest = null;
+ let minDist = Infinity;
+
+ const children = Array.from(list.children).filter(c => c.dataset.value); // Ignore spacers
+
+ // 1. Find closest item first
+ children.forEach(child => {
+ const childCenter = child.offsetTop + (child.offsetHeight / 2);
+ const dist = Math.abs(centerY - childCenter);
+ if (dist < minDist) {
+ minDist = dist;
+ closest = child;
+ }
+ });
+
+ // 2. Apply styles based on closest match
+ children.forEach(child => {
+ const imgBox = child.querySelector('div');
+ const label = child.querySelector('span:last-child');
+ const isClosest = child === closest;
+
+ if (isClosest) {
+ // Active Item
+ child.classList.remove('opacity-30', 'scale-75', 'blur-[1px]');
+ child.classList.add('opacity-100', 'scale-100', 'blur-0', 'z-30');
+
+ imgBox.classList.add('border-primary/50', 'shadow-glow-sm', 'scale-110');
+ imgBox.classList.remove('border-white/10', 'bg-white/5');
+
+ if (key === 'focal') {
+ const fText = imgBox.querySelector('span');
+ if (fText) fText.classList.add('text-primary');
+ }
+
+ label.classList.add('text-primary', 'text-shadow-sm');
+ } else {
+ // Inactive Items
+ child.classList.add('opacity-30', 'scale-75', 'blur-[1px]');
+ child.classList.remove('opacity-100', 'scale-100', 'blur-0', 'z-30');
+
+ imgBox.classList.remove('border-primary/50', 'shadow-glow-sm', 'scale-110');
+ imgBox.classList.add('border-white/10', 'bg-white/5');
+
+ if (key === 'focal') {
+ const fText = imgBox.querySelector('span');
+ if (fText) fText.classList.remove('text-primary');
+ }
+
+ label.classList.remove('text-primary', 'text-shadow-sm');
+ }
+ });
+
+ if (closest && closest.dataset.value !== state[key]) {
+ updateState(key, closest.dataset.value);
+ }
+ };
+
+ list.addEventListener('scroll', handleScroll);
+ // Initial check
+ setTimeout(handleScroll, 150);
+
+
+
+ setTimeout(() => {
+ const initialItem = Array.from(list.children).find(c => c.dataset.value == initialValue);
+ if (initialItem) initialItem.scrollIntoView({ block: 'center' });
+ }, 100);
+
+ return colWrapper;
+ };
+
+ container.appendChild(createColumn('Camera', Object.keys(CAMERA_MAP), 'camera', state.camera));
+ container.appendChild(createColumn('Lens', Object.keys(LENS_MAP), 'lens', state.lens));
+ container.appendChild(createColumn('Focal Length', Object.keys(FOCAL_PERSPECTIVE).map(k => parseInt(k)), 'focal', state.focal));
+ container.appendChild(createColumn('Aperture', Object.keys(APERTURE_EFFECT), 'aperture', state.aperture));
+
+ return container;
+}
diff --git a/src/components/CinemaStudio.js b/src/components/CinemaStudio.js
new file mode 100644
index 0000000..b68992e
--- /dev/null
+++ b/src/components/CinemaStudio.js
@@ -0,0 +1,474 @@
+
+import { muapi } from '../lib/muapi.js';
+import { CameraControls } from './CameraControls.js';
+import { buildNanoBananaPrompt, CAMERA_MAP, LENS_MAP } from '../lib/promptUtils.js';
+import { AuthModal } from './AuthModal.js';
+
+export function CinemaStudio() {
+ const container = document.createElement('div');
+ container.className = 'w-full h-full flex flex-col items-center justify-center bg-black relative overflow-hidden';
+
+ // --- State ---
+ const currentSettings = {
+ prompt: '',
+ aspect_ratio: '16:9',
+ camera: Object.keys(CAMERA_MAP)[0],
+ lens: Object.keys(LENS_MAP)[0],
+ focal: 35,
+ aperture: "f/1.4"
+ };
+
+ // ==========================================
+ // 1. HERO SECTION (Empty State)
+ // ==========================================
+ const heroSection = document.createElement('div');
+ heroSection.className = 'flex flex-col items-center justify-center text-center px-4 animate-fade-in-up';
+ heroSection.innerHTML = `
+ Cinema Studio 2.0
+
+ What would you shoot
with infinite budget?
+
+ `;
+ container.appendChild(heroSection);
+
+ // ==========================================
+ // 2. CAMERA CONTROLS OVERLAY
+ // ==========================================
+ const overlayBackdrop = document.createElement('div');
+ overlayBackdrop.className = 'fixed inset-0 bg-black/80 backdrop-blur-md z-40 opacity-0 pointer-events-none transition-opacity duration-300 flex items-center justify-center';
+
+ const overlayContent = document.createElement('div');
+ // Reduced padding for mobile (p-4) and added max-height/overflow handling
+ overlayContent.className = 'w-full max-w-4xl bg-[#141414] border border-white/10 rounded-3xl p-4 md:p-8 shadow-2xl transform scale-95 transition-transform duration-300 flex flex-col max-h-[90vh]';
+ overlayBackdrop.appendChild(overlayContent);
+
+ // Header for Overlay
+ const overlayHeader = document.createElement('div');
+ overlayHeader.className = 'flex items-center justify-between mb-8';
+ overlayHeader.innerHTML = `
+
+
+
+
+ `;
+ overlayContent.appendChild(overlayHeader);
+
+ // Controls Component
+ const cameraControls = CameraControls((state) => {
+ currentSettings.camera = state.camera;
+ currentSettings.lens = state.lens;
+ currentSettings.focal = state.focal;
+ currentSettings.aperture = state.aperture;
+ updateSummaryCard();
+ });
+ overlayContent.appendChild(cameraControls);
+
+ document.body.appendChild(overlayBackdrop); // Append to body to sit above everything
+
+ // Overlay Logic
+ const openOverlay = () => {
+ overlayBackdrop.classList.remove('opacity-0', 'pointer-events-none');
+ overlayContent.classList.remove('scale-95');
+ overlayContent.classList.add('scale-100');
+ };
+ const closeOverlay = () => {
+ overlayBackdrop.classList.add('opacity-0', 'pointer-events-none');
+ overlayContent.classList.add('scale-95');
+ overlayContent.classList.remove('scale-100');
+ };
+ overlayContent.querySelector('#close-overlay-btn').onclick = closeOverlay;
+ overlayBackdrop.onclick = (e) => { if (e.target === overlayBackdrop) closeOverlay(); };
+
+
+ // ==========================================
+ // 3. FLOATING PROMPT BAR
+ // ==========================================
+ const promptBarWrapper = document.createElement('div');
+ promptBarWrapper.className = 'absolute bottom-8 left-4 right-4 md:left-1/2 md:-translate-x-1/2 md:w-full md:max-w-4xl z-30';
+
+ const promptBar = document.createElement('div');
+ promptBar.className = 'bg-[#1a1a1a] border border-white/10 rounded-[2rem] p-4 flex justify-between shadow-3xl items-end relative';
+
+ // --- LEFT COLUMN (Input + Settings) ---
+ const leftColumn = document.createElement('div');
+ leftColumn.className = 'flex-1 flex flex-col gap-3 min-h-[80px] justify-between py-1 px-1';
+
+ // 1. Input Area
+ const inputRow = document.createElement('div');
+ inputRow.className = 'flex items-start gap-3 w-full';
+
+
+
+ // Textarea
+ const textarea = document.createElement('textarea');
+ textarea.placeholder = 'Describe your scene - use @ to add characters & props';
+ textarea.className = 'flex-1 bg-transparent border-none text-white text-lg font-medium placeholder:text-white/20 focus:outline-none resize-none h-[28px] leading-relaxed overflow-hidden';
+ textarea.style.height = 'auto'; // Auto-grow check
+ textarea.rows = 1;
+ textarea.oninput = function () {
+ this.style.height = 'auto';
+ this.style.height = (this.scrollHeight) + 'px';
+ };
+ inputRow.appendChild(textarea);
+
+ leftColumn.appendChild(inputRow);
+
+ // 2. Settings Toolbar (Bottom Left)
+ // 2. Settings Toolbar (Bottom Left)
+ const settingsToolbar = document.createElement('div');
+ settingsToolbar.className = 'flex items-center gap-3'; // Removed pl-11 to align left
+
+ // Helper: Create Dropdown
+ const createDropdown = (items, selected, onSelect, trigger) => {
+ const existing = document.querySelectorAll('.custom-dropdown');
+ existing.forEach(el => el.remove());
+
+ const rect = trigger.getBoundingClientRect();
+ const menu = document.createElement('div');
+ menu.className = 'custom-dropdown fixed bg-[#1a1a1a] border border-white/10 rounded-xl py-1 shadow-2xl z-50 flex flex-col min-w-[100px] animate-fade-in';
+ menu.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
+ menu.style.left = rect.left + 'px';
+
+ items.forEach(item => {
+ const btn = document.createElement('button');
+ btn.className = `px-3 py-2 text-xs font-bold text-left hover:bg-white/10 transition-colors ${item === selected ? 'text-primary' : 'text-white'}`;
+ btn.textContent = item;
+ btn.onclick = (e) => {
+ e.stopPropagation();
+ onSelect(item);
+ menu.remove();
+ };
+ menu.appendChild(btn);
+ });
+
+ const closeHandler = (e) => {
+ if (!menu.contains(e.target) && e.target !== trigger) {
+ menu.remove();
+ document.removeEventListener('click', closeHandler);
+ }
+ };
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
+ document.body.appendChild(menu);
+ };
+
+ // Aspect Ratio
+ const arBtn = document.createElement('button');
+ arBtn.className = 'flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-bold text-white/50 hover:text-white transition-colors bg-white/5 hover:bg-white/10 rounded-lg border border-white/5';
+ const updateArBtn = () => {
+ arBtn.innerHTML = ` ${currentSettings.aspect_ratio}`;
+ };
+ updateArBtn();
+ arBtn.onclick = () => {
+ createDropdown(['16:9', '21:9', '9:16', '1:1', '4:5'], currentSettings.aspect_ratio, (val) => {
+ currentSettings.aspect_ratio = val;
+ updateArBtn();
+ }, arBtn);
+ };
+ settingsToolbar.appendChild(arBtn);
+
+ // Resolution
+ const resBtn = document.createElement('button');
+ resBtn.className = 'flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-bold text-white/50 hover:text-white transition-colors bg-white/5 hover:bg-white/10 rounded-lg border border-white/5';
+ const updateResBtn = (val) => {
+ resBtn.dataset.value = val || '2K';
+ resBtn.innerHTML = ` ${resBtn.dataset.value}`;
+ };
+ updateResBtn('2K');
+ resBtn.onclick = () => {
+ createDropdown(['1K', '2K', '4K'], resBtn.dataset.value, (val) => { updateResBtn(val); }, resBtn);
+ };
+ settingsToolbar.appendChild(resBtn);
+
+ leftColumn.appendChild(settingsToolbar);
+ promptBar.appendChild(leftColumn);
+
+
+ // --- RIGHT GROUP (Summary + Generate) ---
+ const rightGroup = document.createElement('div');
+ rightGroup.className = 'flex items-center gap-2 h-full self-end mb-1';
+
+ // Summary Card (Triggers Overlay)
+ const summaryCard = document.createElement('button');
+ // Removed 'hidden' class, added 'flex' and refined width constraints for mobile
+ summaryCard.className = 'flex flex-col items-start justify-center px-4 py-2 bg-[#2a2a2a] rounded-xl border border-white/5 hover:border-white/20 transition-colors text-left flex-1 min-w-[100px] md:min-w-[140px] max-w-[240px] h-[56px] relative group overflow-hidden';
+
+ // Dot indicator
+ const dot = document.createElement('div');
+ dot.className = 'absolute top-2 right-2 w-2 h-2 bg-primary rounded-full shadow-glow-sm';
+ summaryCard.appendChild(dot);
+
+ const summaryTitle = document.createElement('span');
+ summaryTitle.className = 'text-[10px] font-bold text-white uppercase truncate w-full tracking-wide';
+ summaryTitle.textContent = currentSettings.camera;
+
+ const summaryValue = document.createElement('span');
+ summaryValue.className = 'text-[10px] font-medium text-white/60 truncate w-full';
+ summaryValue.textContent = formatSummaryValue();
+
+ summaryCard.appendChild(summaryTitle);
+ summaryCard.appendChild(summaryValue);
+
+ summaryCard.onclick = openOverlay;
+
+ function formatSummaryValue() {
+ return `${currentSettings.lens}, ${currentSettings.focal}mm, ${currentSettings.aperture}`;
+ }
+
+ function updateSummaryCard() {
+ summaryTitle.textContent = currentSettings.camera;
+ summaryValue.textContent = formatSummaryValue();
+ }
+
+ // Generate Button
+ const generateBtn = document.createElement('button');
+ generateBtn.className = 'h-[56px] px-8 bg-[#d9ff00] text-black rounded-xl font-black text-xs uppercase hover:bg-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed';
+ generateBtn.innerHTML = `GENERATE ✨`;
+
+ rightGroup.appendChild(summaryCard);
+ rightGroup.appendChild(generateBtn);
+ promptBar.appendChild(rightGroup);
+
+ promptBarWrapper.appendChild(promptBar);
+ container.appendChild(promptBarWrapper);
+
+
+ // ==========================================
+ // 3. HISTORY SIDEBAR
+ // ==========================================
+ const generationHistory = [];
+
+ // History Sidebar - VISIBLE BY DEFAULT (removed translate-x-full opacity-0)
+ const historySidebar = document.createElement('div');
+ historySidebar.className = 'fixed right-0 top-0 h-full w-20 md:w-24 bg-black/60 backdrop-blur-xl border-l border-white/5 z-50 flex flex-col items-center py-4 gap-3 overflow-y-auto transition-all duration-500';
+
+ const historyLabel = document.createElement('div');
+ historyLabel.className = 'text-[9px] font-bold text-white/40 uppercase tracking-widest mb-2';
+ historyLabel.textContent = 'History';
+ historySidebar.appendChild(historyLabel);
+
+ const historyList = document.createElement('div');
+ historyList.className = 'flex flex-col gap-2 w-full px-2';
+ historySidebar.appendChild(historyList);
+
+ container.appendChild(historySidebar);
+
+ // ==========================================
+ // 4. CANVAS AREA (Result View)
+ // ==========================================
+ const canvas = document.createElement('div');
+ canvas.className = 'absolute inset-0 flex flex-col items-center justify-center p-4 min-[800px]:p-16 z-30 opacity-0 pointer-events-none transition-all duration-1000 translate-y-10 scale-95 bg-black/90 backdrop-blur-3xl';
+
+ const imageContainer = document.createElement('div');
+ imageContainer.className = 'relative group max-w-full max-h-[70vh] flex items-center justify-center';
+
+ const resultImg = document.createElement('img');
+ resultImg.className = 'max-h-[60vh] max-w-[90vw] rounded-2xl shadow-2xl border border-white/10 object-contain';
+ imageContainer.appendChild(resultImg);
+ canvas.appendChild(imageContainer);
+
+ // Canvas Controls
+ const canvasControls = document.createElement('div');
+ canvasControls.className = 'mt-8 flex gap-3 opacity-0 transition-opacity delay-500 duration-500 justify-center';
+
+ const createActionBtn = (label, primary = false) => {
+ const btn = document.createElement('button');
+ btn.className = primary
+ ? 'bg-[#d9ff00] text-black px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-wide hover:bg-white transition-colors shadow-glow-sm hover:scale-105 active:scale-95'
+ : 'bg-white/10 hover:bg-white/20 px-6 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wide transition-all border border-white/5 backdrop-blur-lg text-white hover:border-white/20';
+ btn.textContent = label;
+ return btn;
+ };
+
+ const regenerateBtn = createActionBtn('↻ Regenerate');
+ const downloadBtn = createActionBtn('↓ Download', true);
+ const newPromptBtn = createActionBtn('+ New Shot');
+
+ canvasControls.appendChild(regenerateBtn);
+ canvasControls.appendChild(downloadBtn);
+ canvasControls.appendChild(newPromptBtn);
+ canvas.appendChild(canvasControls);
+
+ container.appendChild(canvas);
+
+ // --- History Logic ---
+ const renderHistory = () => {
+ historyList.innerHTML = '';
+ generationHistory.forEach((entry, idx) => {
+ const thumb = document.createElement('div');
+ thumb.className = `relative group/thumb cursor-pointer rounded-lg overflow-hidden border-2 transition-all duration-300 aspect-square ${idx === 0 ? 'border-[#d9ff00] shadow-glow-sm' : 'border-white/10 hover:border-white/30'}`;
+
+ thumb.innerHTML = `
+
+
+ Load
+
+ `;
+
+ thumb.onclick = () => loadHistoryItem(entry, thumb);
+ historyList.appendChild(thumb);
+ });
+ };
+
+ const addToHistory = (entry) => {
+ generationHistory.unshift(entry);
+ localStorage.setItem('cinema_history', JSON.stringify(generationHistory.slice(0, 50)));
+ renderHistory();
+ };
+
+ const loadHistoryItem = (entry, thumbElement) => {
+ // Restore Settings
+ if (entry.settings) {
+ currentSettings.camera = entry.settings.camera;
+ currentSettings.lens = entry.settings.lens;
+ currentSettings.focal = entry.settings.focal;
+ currentSettings.aperture = entry.settings.aperture;
+ currentSettings.aspect_ratio = entry.settings.aspect_ratio;
+
+ // Update UI elements
+ textarea.value = entry.settings.prompt || '';
+ updateSummaryCard();
+ updateArBtn();
+ updateResBtn(entry.settings.resolution || '2K');
+ }
+
+ showCanvas(entry.url);
+
+ // Highlight active history item
+ if (thumbElement) {
+ historyList.querySelectorAll('div').forEach(t => {
+ t.classList.remove('border-[#d9ff00]', 'shadow-glow-sm');
+ t.classList.add('border-white/10');
+ });
+ thumbElement.classList.remove('border-white/10');
+ thumbElement.classList.add('border-[#d9ff00]', 'shadow-glow-sm');
+ }
+ };
+
+ const showCanvas = (url) => {
+ resultImg.src = url;
+
+ // Hide Input UI
+ heroSection.classList.add('opacity-0', 'pointer-events-none', 'scale-95');
+ promptBarWrapper.classList.add('opacity-0', 'pointer-events-none', 'translate-y-20');
+
+ // Show Canvas
+ canvas.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
+ canvas.classList.add('opacity-100', 'translate-y-0', 'scale-100');
+ canvasControls.classList.remove('opacity-0');
+ canvasControls.classList.add('opacity-100');
+ };
+
+ const resetToPrompt = () => {
+ // Hide Canvas
+ canvas.classList.add('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
+ canvas.classList.remove('opacity-100', 'translate-y-0', 'scale-100');
+
+ // Show Input UI
+ heroSection.classList.remove('opacity-0', 'pointer-events-none', 'scale-95');
+ promptBarWrapper.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-20');
+
+ // Clear prompt for new shot?
+ textarea.value = '';
+ textarea.focus();
+ };
+
+ // Load saved history
+ try {
+ const saved = JSON.parse(localStorage.getItem('cinema_history') || '[]');
+ if (saved.length > 0) {
+ saved.forEach(e => generationHistory.push(e));
+ renderHistory();
+ }
+ } catch (e) { }
+
+ // Actions
+ newPromptBtn.onclick = resetToPrompt;
+
+ regenerateBtn.onclick = () => {
+ resetToPrompt();
+ setTimeout(() => {
+ generateBtn.click();
+ }, 300);
+ };
+
+ downloadBtn.onclick = async () => {
+ try {
+ const response = await fetch(resultImg.src);
+ const blob = await response.blob();
+ const blobUrl = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = blobUrl;
+ a.download = `cinema-shot-${Date.now()}.jpg`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(blobUrl);
+ } catch (err) {
+ window.open(resultImg.src, '_blank');
+ }
+ };
+
+ // ==========================================
+ // 5. GENERATION LOGIC UPDATE
+ // ==========================================
+ generateBtn.onclick = async () => {
+ const basePrompt = textarea.value.trim();
+ if (!basePrompt) return;
+
+ const apiKey = localStorage.getItem('muapi_key');
+ if (!apiKey) {
+ AuthModal(() => generateBtn.click());
+ return;
+ }
+
+ generateBtn.disabled = true;
+ generateBtn.innerHTML = "SHOOTING...";
+
+ // Compile Prompt
+ const finalPrompt = buildNanoBananaPrompt(
+ basePrompt,
+ currentSettings.camera,
+ currentSettings.lens,
+ currentSettings.focal,
+ currentSettings.aperture
+ );
+
+ try {
+ const res = await muapi.generateImage({
+ model: 'nano-banana-pro',
+ prompt: finalPrompt,
+ aspect_ratio: currentSettings.aspect_ratio,
+ resolution: resBtn.dataset.value || '1k',
+ negative_prompt: "blurry, low quality, distortion, bad composition"
+ });
+
+ if (res && res.url) {
+ // Save to history
+ addToHistory({
+ url: res.url,
+ timestamp: Date.now(),
+ settings: {
+ prompt: basePrompt,
+ ...currentSettings,
+ resolution: resBtn.dataset.value
+ }
+ });
+
+ showCanvas(res.url);
+ } else {
+ throw new Error('No Data');
+ }
+
+ } catch (e) {
+ console.error(e);
+ alert('Generation Failed: ' + e.message);
+ } finally {
+ generateBtn.disabled = false;
+ generateBtn.innerHTML = `GENERATE ✨`;
+ }
+ };
+
+ return container;
+}
diff --git a/src/components/Header.js b/src/components/Header.js
index d8b05e7..86d41b0 100644
--- a/src/components/Header.js
+++ b/src/components/Header.js
@@ -1,4 +1,4 @@
-export function Header() {
+export function Header(navigate) {
const header = document.createElement('header');
header.className = 'w-full flex flex-col z-50 sticky top-0';
@@ -42,6 +42,18 @@ export function Header() {
if (item === 'Contests') {
link.innerHTML += ' New';
}
+
+ link.onclick = () => {
+ // Remove active state from all
+ Array.from(menu.children).forEach(child => child.classList.remove('text-white'));
+ // Add to current
+ link.classList.add('text-white');
+
+ if (item === 'Image') navigate('image');
+ else if (item === 'Video') navigate('video');
+ else if (item === 'Cinema Studio') navigate('cinema');
+ };
+
menu.appendChild(link);
});
diff --git a/src/lib/muapi.js b/src/lib/muapi.js
index 0305f10..d66c28a 100644
--- a/src/lib/muapi.js
+++ b/src/lib/muapi.js
@@ -42,6 +42,11 @@ export class MuapiClient {
finalPayload.aspect_ratio = params.aspect_ratio;
}
+ // Resolution
+ if (params.resolution) {
+ finalPayload.resolution = params.resolution;
+ }
+
// Image-to-Image
if (params.image_url) {
finalPayload.image_url = params.image_url;
diff --git a/src/lib/promptUtils.js b/src/lib/promptUtils.js
new file mode 100644
index 0000000..6c657c9
--- /dev/null
+++ b/src/lib/promptUtils.js
@@ -0,0 +1,74 @@
+export const CAMERA_MAP = {
+ "Modular 8K Digital": "modular 8K digital cinema camera",
+ "Full-Frame Cine Digital": "full-frame digital cinema camera",
+ "Grand Format 70mm Film": "grand format 70mm film camera",
+ "Studio Digital S35": "Super 35 studio digital camera",
+ "Classic 16mm Film": "classic 16mm film camera",
+ "Premium Large Format Digital": "premium large-format digital cinema camera"
+};
+
+export const LENS_MAP = {
+ "Creative Tilt Lens": "creative tilt lens effect",
+ "Compact Anamorphic": "compact anamorphic lens",
+ "Extreme Macro": "extreme macro lens",
+ "70s Cinema Prime": "1970s cinema prime lens",
+ "Classic Anamorphic": "classic anamorphic lens",
+ "Premium Modern Prime": "premium modern prime lens",
+ "Warm Cinema Prime": "warm-toned cinema prime lens",
+ "Swirl Bokeh Portrait": "swirl bokeh portrait lens",
+ "Vintage Prime": "vintage prime lens",
+ "Halation Diffusion": "halation diffusion filter",
+ "Clinical Sharp Prime": "ultra-sharp clinical prime lens"
+};
+
+export const FOCAL_PERSPECTIVE = {
+ 8: "ultra-wide perspective",
+ 14: "wide-angle perspective",
+ 24: "wide-angle dynamic perspective",
+ 35: "natural cinematic perspective",
+ 50: "standard portrait perspective",
+ 85: "classic portrait perspective"
+};
+
+export const APERTURE_EFFECT = {
+ "f/1.4": "shallow depth of field, creamy bokeh",
+ "f/4": "balanced depth of field",
+ "f/11": "deep focus clarity, sharp foreground to background"
+};
+
+/**
+ * Compiles a cinematic prompt based on camera settings.
+ * @param {string} basePrompt
+ * @param {string} camera
+ * @param {string} lens
+ * @param {number} focalLength
+ * @param {string} aperture
+ * @returns {string} The compiled prompt
+ */
+export function buildNanoBananaPrompt(basePrompt, camera, lens, focalLength, aperture) {
+ const cameraDesc = CAMERA_MAP[camera] || camera;
+ const lensDesc = LENS_MAP[lens] || lens;
+ const perspective = FOCAL_PERSPECTIVE[focalLength] || "";
+ const depthEffect = APERTURE_EFFECT[aperture] || "";
+
+ const qualityTags = [
+ "professional photography",
+ "ultra-detailed",
+ "8K resolution"
+ ];
+
+ const parts = [
+ basePrompt,
+ `shot on a ${cameraDesc}`,
+ `using a ${lensDesc} at ${focalLength}mm ${perspective ? `(${perspective})` : ''}`,
+ `aperture ${aperture}`,
+ depthEffect,
+ "cinematic lighting",
+ "natural color science",
+ "high dynamic range",
+ qualityTags.join(", ")
+ ];
+
+ // Filter out empty strings and join
+ return parts.filter(p => p && p.trim() !== "").join(", ");
+}
diff --git a/src/main.js b/src/main.js
index 1c27927..6ff8dde 100644
--- a/src/main.js
+++ b/src/main.js
@@ -3,21 +3,11 @@ import { Header } from './components/Header.js';
import { ImageStudio } from './components/ImageStudio.js';
const app = document.querySelector('#app');
-let contentArea; // Declare contentArea globally so navigate can access it
-
-app.innerHTML = '';
-app.appendChild(Header());
-
-contentArea = document.createElement('main'); // Assign to global contentArea
-contentArea.id = 'content-area';
-contentArea.className = 'flex-1 relative w-full overflow-hidden flex flex-col bg-app-bg';
-app.appendChild(contentArea);
-
-// Initial Route
-navigate('image');
+let contentArea;
// Router
function navigate(page) {
+ if (!contentArea) return;
contentArea.innerHTML = '';
if (page === 'image') {
@@ -25,11 +15,22 @@ function navigate(page) {
} else if (page === 'video') {
contentArea.innerHTML = 'Video Studio Coming Soon 🎬
';
} else if (page === 'cinema') {
- contentArea.innerHTML = 'Cinema Studio Coming Soon 🎥
';
+ import('./components/CinemaStudio.js').then(({ CinemaStudio }) => {
+ contentArea.appendChild(CinemaStudio());
+ });
}
}
-// Initial Load
+app.innerHTML = '';
+// Pass navigate to Header so links work
+app.appendChild(Header(navigate));
+
+contentArea = document.createElement('main');
+contentArea.id = 'content-area';
+contentArea.className = 'flex-1 relative w-full overflow-hidden flex flex-col bg-app-bg';
+app.appendChild(contentArea);
+
+// Initial Route
navigate('image');
// Event Listener for Navigation