Merge pull request #23 from Anil-matcha/master
Cinema studio feature supported
22
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` | — |
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.5 MiB |
BIN
public/assets/cinema/70s_cinema_prime.webp
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
public/assets/cinema/classic_16mm_film.webp
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
public/assets/cinema/classic_anamorphic.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/assets/cinema/clinical_sharp_prime.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
public/assets/cinema/compact_anamorphic.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/assets/cinema/creative_tilt_lens.webp
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/assets/cinema/extreme_macro.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/assets/cinema/f_11.webp
Normal file
|
After Width: | Height: | Size: 383 KiB |
BIN
public/assets/cinema/f_1_4.webp
Normal file
|
After Width: | Height: | Size: 409 KiB |
BIN
public/assets/cinema/f_4.webp
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
public/assets/cinema/full_frame_cine_digital.webp
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/assets/cinema/grand_format_70mm_film.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/assets/cinema/halation_diffusion.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/assets/cinema/modular_8k_digital.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/assets/cinema/premium_large_format_digital.webp
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
public/assets/cinema/premium_modern_prime.webp
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
public/assets/cinema/studio_digital_s35.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/assets/cinema/swirl_bokeh_portrait.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/assets/cinema/vintage_prime.webp
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/assets/cinema/warm_cinema_prime.webp
Normal file
|
After Width: | Height: | Size: 125 KiB |
251
src/components/CameraControls.js
Normal 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;
|
||||
}
|
||||
474
src/components/CinemaStudio.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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(", ");
|
||||
}
|
||||
29
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 = '<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
|
||||
|
|
|
|||