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