Merge pull request #23 from Anil-matcha/master

Cinema studio feature supported
This commit is contained in:
Anil Chandra Naidu Matcha 2026-02-14 15:03:34 +05:30 committed by GitHub
commit c9ae73077d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 850 additions and 19 deletions

View file

@ -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` | — |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View file

@ -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 = `<div class="w-3 h-3 bg-white/20 rounded-full"></div>`;
}
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;
}

View file

@ -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 = `
<div class="mb-4 text-xs font-bold text-white/40 tracking-[0.2em] uppercase">Cinema Studio 2.0</div>
<h1 class="text-4xl md:text-6xl font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-white/50 tracking-tight leading-tight mb-2">
What would you shoot<br>with infinite budget?
</h1>
`;
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 = `
<div class="flex gap-4">
<button class="px-4 py-2 bg-white text-black text-xs font-bold rounded-full">All</button>
</div>
<button id="close-overlay-btn" class="text-white/50 hover:text-white transition-colors">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
`;
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 = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="10" rx="2" ry="2"/></svg> ${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 = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> ${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 = `
<img src="${entry.url}" class="w-full h-full object-cover opacity-80 group-hover/thumb:opacity-100 transition-opacity">
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center">
<span class="text-[8px] font-bold text-white uppercase">Load</span>
</div>
`;
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;
}

View file

@ -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 += ' <span class="bg-primary/10 text-primary text-[8px] px-1.5 py-0.5 rounded-full ml-1 border border-primary/20">New</span>';
}
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);
});

View file

@ -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;

74
src/lib/promptUtils.js Normal file
View file

@ -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(", ");
}

View file

@ -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 = '<div class="flex items-center justify-center h-full text-secondary">Video Studio Coming Soon 🎬</div>';
} else if (page === 'cinema') {
contentArea.innerHTML = '<div class="flex items-center justify-center h-full text-secondary">Cinema Studio Coming Soon 🎥</div>';
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