feat: Integrate prompt tools and hover tooltips (PR #52)
- Add Quick Tools Panel to ImageStudio (Quick Starters + Prompt Enhancer) - Add Camera Builder Panel to CinemaStudio - Add Advanced Options panel to ImageStudio - Move ENHANCE_TAGS, QUICK_PROMPTS, FOCAL_PERSPECTIVE, APERTURE_EFFECT to promptUtils.js - Add hover tooltips to platform buttons in ImageStudio, VideoStudio, CinemaStudio Co-Authored-By: Dean Gilmore <deangilmoreremix@users.noreply.github.com>
This commit is contained in:
commit
776a325e77
5 changed files with 785 additions and 13 deletions
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
import { muapi } from '../lib/muapi.js';
|
||||
import { CameraControls } from './CameraControls.js';
|
||||
import { buildNanoBananaPrompt, CAMERA_MAP, LENS_MAP } from '../lib/promptUtils.js';
|
||||
import { buildNanoBananaPrompt, CAMERA_MAP, LENS_MAP, FOCAL_PERSPECTIVE, APERTURE_EFFECT } from '../lib/promptUtils.js';
|
||||
import { AuthModal } from './AuthModal.js';
|
||||
|
||||
export function CinemaStudio() {
|
||||
|
|
@ -17,6 +17,9 @@ export function CinemaStudio() {
|
|||
focal: 35,
|
||||
aperture: "f/1.4"
|
||||
};
|
||||
|
||||
// Camera builder panel state
|
||||
let showCameraBuilder = false;
|
||||
|
||||
// ==========================================
|
||||
// 1. HERO SECTION (Empty State)
|
||||
|
|
@ -180,6 +183,13 @@ export function CinemaStudio() {
|
|||
createDropdown(['1K', '2K', '4K'], resBtn.dataset.value, (val) => { updateResBtn(val); }, resBtn);
|
||||
};
|
||||
settingsToolbar.appendChild(resBtn);
|
||||
|
||||
// Camera Builder Toggle Button
|
||||
const cameraBuilderBtn = document.createElement('button');
|
||||
cameraBuilderBtn.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';
|
||||
cameraBuilderBtn.setAttribute('data-tooltip', 'Quick camera builder');
|
||||
cameraBuilderBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="3"/></svg> Builder`;
|
||||
settingsToolbar.appendChild(cameraBuilderBtn);
|
||||
|
||||
leftColumn.appendChild(settingsToolbar);
|
||||
promptBar.appendChild(leftColumn);
|
||||
|
|
@ -193,6 +203,7 @@ export function CinemaStudio() {
|
|||
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';
|
||||
summaryCard.setAttribute('data-tooltip', 'Open camera settings');
|
||||
|
||||
// Dot indicator
|
||||
const dot = document.createElement('div');
|
||||
|
|
@ -224,6 +235,7 @@ export function CinemaStudio() {
|
|||
// 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.setAttribute('data-tooltip', 'Generate cinema shot');
|
||||
generateBtn.innerHTML = `GENERATE ✨`;
|
||||
|
||||
rightGroup.appendChild(summaryCard);
|
||||
|
|
@ -233,6 +245,114 @@ export function CinemaStudio() {
|
|||
promptBarWrapper.appendChild(promptBar);
|
||||
container.appendChild(promptBarWrapper);
|
||||
|
||||
// ==========================================
|
||||
// 3B. CAMERA BUILDER PANEL (Collapsible)
|
||||
// ==========================================
|
||||
const cameraBuilderPanel = document.createElement('div');
|
||||
cameraBuilderPanel.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-20';
|
||||
cameraBuilderPanel.style.display = 'none'; // Hidden by default
|
||||
|
||||
const builderCard = document.createElement('div');
|
||||
builderCard.className = 'bg-[#1a1a1a] border border-white/10 rounded-2xl p-4 shadow-3xl';
|
||||
|
||||
builderCard.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-xs font-bold text-white">Camera Builder</h4>
|
||||
<button id="close-builder-btn" class="text-white/40 hover:text-white transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-[10px] font-bold text-muted uppercase">Camera</label>
|
||||
<select id="builder-camera" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-primary/50">
|
||||
${Object.keys(CAMERA_MAP).map(c => `<option value="${c}" ${c === currentSettings.camera ? 'selected' : ''}>${c}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-[10px] font-bold text-muted uppercase">Lens</label>
|
||||
<select id="builder-lens" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-primary/50">
|
||||
${Object.keys(LENS_MAP).map(l => `<option value="${l}" ${l === currentSettings.lens ? 'selected' : ''}>${l}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-[10px] font-bold text-muted uppercase">Focal</label>
|
||||
<select id="builder-focal" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-primary/50">
|
||||
${Object.keys(FOCAL_PERSPECTIVE).map(f => `<option value="${f}" ${f === currentSettings.focal ? 'selected' : ''}>${f}mm</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-[10px] font-bold text-muted uppercase">Aperture</label>
|
||||
<select id="builder-aperture" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-primary/50">
|
||||
${Object.keys(APERTURE_EFFECT).map(a => `<option value="${a}" ${a === currentSettings.aperture ? 'selected' : ''}>${a}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-[10px] font-bold text-muted uppercase">Preview</label>
|
||||
<div id="builder-preview" class="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2.5 text-white text-xs min-h-[40px]"></div>
|
||||
<button id="apply-builder-btn" class="px-4 py-2 bg-primary text-black rounded-lg text-xs font-bold hover:shadow-glow transition-all">
|
||||
Use This Setup
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
cameraBuilderPanel.appendChild(builderCard);
|
||||
container.appendChild(cameraBuilderPanel);
|
||||
|
||||
// Camera Builder toggle logic
|
||||
cameraBuilderBtn.onclick = () => {
|
||||
showCameraBuilder = !showCameraBuilder;
|
||||
cameraBuilderPanel.style.display = showCameraBuilder ? 'block' : 'none';
|
||||
if (showCameraBuilder) updateBuilderPreview();
|
||||
};
|
||||
|
||||
const closeBuilderBtn = cameraBuilderPanel.querySelector('#close-builder-btn');
|
||||
if (closeBuilderBtn) closeBuilderBtn.onclick = () => {
|
||||
showCameraBuilder = false;
|
||||
cameraBuilderPanel.style.display = 'none';
|
||||
};
|
||||
|
||||
// Update builder preview
|
||||
const updateBuilderPreview = () => {
|
||||
const camera = builderCard.querySelector('#builder-camera')?.value || currentSettings.camera;
|
||||
const lens = builderCard.querySelector('#builder-lens')?.value || currentSettings.lens;
|
||||
const focal = parseInt(builderCard.querySelector('#builder-focal')?.value || currentSettings.focal);
|
||||
const aperture = builderCard.querySelector('#builder-aperture')?.value || currentSettings.aperture;
|
||||
|
||||
const preview = buildNanoBananaPrompt('', camera, lens, focal, aperture);
|
||||
const previewEl = builderCard.querySelector('#builder-preview');
|
||||
if (previewEl) {
|
||||
previewEl.textContent = preview || 'Select camera settings to see preview...';
|
||||
}
|
||||
};
|
||||
|
||||
// Builder event listeners
|
||||
const builderCamera = builderCard.querySelector('#builder-camera');
|
||||
const builderLens = builderCard.querySelector('#builder-lens');
|
||||
const builderFocal = builderCard.querySelector('#builder-focal');
|
||||
const builderAperture = builderCard.querySelector('#builder-aperture');
|
||||
|
||||
if (builderCamera) builderCamera.onchange = updateBuilderPreview;
|
||||
if (builderLens) builderLens.onchange = updateBuilderPreview;
|
||||
if (builderFocal) builderFocal.onchange = updateBuilderPreview;
|
||||
if (builderAperture) builderAperture.onchange = updateBuilderPreview;
|
||||
|
||||
const applyBuilderBtn = builderCard.querySelector('#apply-builder-btn');
|
||||
if (applyBuilderBtn) {
|
||||
applyBuilderBtn.onclick = () => {
|
||||
currentSettings.camera = builderCamera?.value || currentSettings.camera;
|
||||
currentSettings.lens = builderLens?.value || currentSettings.lens;
|
||||
currentSettings.focal = parseInt(builderFocal?.value || currentSettings.focal);
|
||||
currentSettings.aperture = builderAperture?.value || currentSettings.aperture;
|
||||
updateSummaryCard();
|
||||
showCameraBuilder = false;
|
||||
cameraBuilderPanel.style.display = 'none';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 3. HISTORY SIDEBAR
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
i2iModels, getAspectRatiosForI2IModel, getResolutionsForI2IModel, getQualityFieldForI2IModel,
|
||||
getMaxImagesForI2IModel
|
||||
} from '../lib/models.js';
|
||||
import { ENHANCE_TAGS, QUICK_PROMPTS } from '../lib/promptUtils.js';
|
||||
import { AuthModal } from './AuthModal.js';
|
||||
import { createUploadPicker } from './UploadPicker.js';
|
||||
import { savePendingJob, removePendingJob, getPendingJobs } from '../lib/pendingJobs.js';
|
||||
|
|
@ -21,6 +22,25 @@ export function ImageStudio() {
|
|||
let uploadedImageUrls = []; // array of uploaded image URLs (multi-image support)
|
||||
let imageMode = false; // false = t2i models, true = i2i models
|
||||
|
||||
// Advanced parameters state
|
||||
let negativePrompt = '';
|
||||
let guidanceScale = 7.5;
|
||||
let steps = 25;
|
||||
let seed = -1;
|
||||
let showAdvanced = false;
|
||||
let selectedStyle = 'None';
|
||||
let batchCount = 1;
|
||||
|
||||
// New advanced controls
|
||||
let customWidth = 0; // 0 means use default (aspect ratio based)
|
||||
let customHeight = 0;
|
||||
let referenceStrength = 50; // 0-100, for style reference models
|
||||
let selectedLora = ''; // LoRA model ID from Civitai
|
||||
let loraWeight = 1.0;
|
||||
|
||||
// Quick tools panel state
|
||||
let showToolsPanel = false;
|
||||
|
||||
const getCurrentModels = () => imageMode ? i2iModels : t2iModels;
|
||||
const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2IModel(id) : getAspectRatiosForModel(id);
|
||||
const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2IModel(id) : getResolutionsForModel(id);
|
||||
|
|
@ -127,10 +147,11 @@ export function ImageStudio() {
|
|||
const controlsLeft = document.createElement('div');
|
||||
controlsLeft.className = 'flex items-center gap-1.5 md:gap-2.5 relative overflow-x-auto no-scrollbar pb-1 md:pb-0';
|
||||
|
||||
const createControlBtn = (icon, label, id) => {
|
||||
const createControlBtn = (icon, label, id, tooltip) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.id = id;
|
||||
btn.className = 'flex items-center gap-1.5 md:gap-2.5 px-3 md:px-4 py-2 md:py-2.5 bg-white/5 hover:bg-white/10 rounded-xl md:rounded-2xl transition-all border border-white/5 group whitespace-nowrap';
|
||||
if (tooltip) btn.setAttribute('data-tooltip', tooltip);
|
||||
btn.innerHTML = `
|
||||
${icon}
|
||||
<span id="${id}-label" class="text-xs font-bold text-white group-hover:text-primary transition-colors">${label}</span>
|
||||
|
|
@ -143,26 +164,42 @@ export function ImageStudio() {
|
|||
<div class="w-5 h-5 bg-primary rounded-md flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<span class="text-[10px] font-black text-black">G</span>
|
||||
</div>
|
||||
`, selectedModelName, 'model-btn');
|
||||
`, selectedModelName, 'model-btn', 'Select AI generation model');
|
||||
|
||||
const arBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>
|
||||
`, selectedAr, 'ar-btn');
|
||||
`, selectedAr, 'ar-btn', 'Change aspect ratio');
|
||||
|
||||
const qualityBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z"/></svg>
|
||||
`, '720p', 'quality-btn');
|
||||
`, '720p', 'quality-btn', 'Set output quality');
|
||||
|
||||
controlsLeft.appendChild(modelBtn);
|
||||
controlsLeft.appendChild(arBtn);
|
||||
controlsLeft.appendChild(qualityBtn);
|
||||
|
||||
// Advanced options toggle button
|
||||
const advancedBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 001.82-.33 1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-1.82.33A1.65 1.65 0 0019.4 9a1.65 1.65 0 00-1.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||||
`, 'Advanced', 'advanced-btn', 'Show advanced options');
|
||||
controlsLeft.appendChild(advancedBtn);
|
||||
|
||||
// Quick Tools toggle button
|
||||
const toolsBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>
|
||||
`, 'Tools', 'tools-btn', 'Quick starters & prompt enhancer');
|
||||
controlsLeft.appendChild(toolsBtn);
|
||||
// Show quality button if the default model has quality/resolution options
|
||||
const _initResolutions = getResolutionsForModel(defaultModel.id);
|
||||
qualityBtn.style.display = _initResolutions.length > 0 ? 'flex' : 'none';
|
||||
if (_initResolutions.length > 0) document.getElementById('quality-btn-label').textContent = _initResolutions[0];
|
||||
if (_initResolutions.length > 0) {
|
||||
const qlabel = qualityBtn.querySelector('#quality-btn-label');
|
||||
if (qlabel) qlabel.textContent = _initResolutions[0];
|
||||
}
|
||||
|
||||
const generateBtn = document.createElement('button');
|
||||
generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg';
|
||||
generateBtn.setAttribute('data-tooltip', 'Generate AI image from prompt');
|
||||
generateBtn.innerHTML = `Generate ✨`;
|
||||
|
||||
bottomRow.appendChild(controlsLeft);
|
||||
|
|
@ -171,6 +208,419 @@ export function ImageStudio() {
|
|||
promptWrapper.appendChild(bar);
|
||||
container.appendChild(promptWrapper);
|
||||
|
||||
const inlineInstructions = createInlineInstructions('image');
|
||||
inlineInstructions.classList.add('max-w-4xl', 'mt-8');
|
||||
container.appendChild(inlineInstructions);
|
||||
|
||||
// ==========================================
|
||||
// 3. QUICK TOOLS PANEL (Prompt Enhancer + Quick Starters)
|
||||
// ==========================================
|
||||
const toolsPanel = document.createElement('div');
|
||||
toolsPanel.className = 'w-full max-w-4xl mt-6 animate-fade-in-up hidden';
|
||||
toolsPanel.id = 'tools-panel';
|
||||
|
||||
// Build tools panel HTML
|
||||
toolsPanel.innerHTML = `
|
||||
<div class="bg-[#111]/90 backdrop-blur-xl border border-white/10 rounded-2xl p-5 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-white/5">
|
||||
<h3 class="text-sm font-bold text-white">Quick Tools</h3>
|
||||
<button id="close-tools-btn" class="text-white/40 hover:text-white transition-colors">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- Quick Starters Section -->
|
||||
<div class="flex-1">
|
||||
<h4 class="text-xs font-bold text-secondary uppercase tracking-wider mb-3">Quick Starters</h4>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
${QUICK_PROMPTS.map(q => `
|
||||
<button class="quick-starter-btn px-3 py-2 rounded-lg text-xs font-bold bg-white/5 text-secondary hover:bg-white/10 hover:text-primary transition-all text-left border border-white/5 hover:border-primary/30" data-prompt="${q.prompt}">
|
||||
${q.label}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Enhancer Section -->
|
||||
<div class="flex-1">
|
||||
<h4 class="text-xs font-bold text-secondary uppercase tracking-wider mb-3">Prompt Enhancer</h4>
|
||||
<div class="flex flex-col gap-3">
|
||||
<input type="text" id="base-prompt-input"
|
||||
placeholder="Enter base prompt..."
|
||||
class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-sm placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors">
|
||||
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-muted uppercase tracking-wider mb-2 block">Enhancement Tags</label>
|
||||
<div id="enhance-tags-area" class="flex flex-wrap gap-1.5">
|
||||
${Object.entries(ENHANCE_TAGS).map(([category, tags]) =>
|
||||
tags.map(tag => `<button class="enhance-tag-btn px-2 py-1 rounded-full text-[10px] font-bold bg-white/5 text-secondary hover:bg-white/10 transition-all" data-tag="${tag}">${tag}</button>`).join('')
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-[10px] font-bold text-muted uppercase tracking-wider">Enhanced Prompt</label>
|
||||
<div id="enhanced-prompt-display" class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-xs min-h-[40px]"></div>
|
||||
<div class="flex gap-2">
|
||||
<button id="copy-enhanced-btn" class="px-3 py-1.5 rounded-lg text-xs font-bold bg-white/5 text-secondary hover:bg-white/10 transition-all">
|
||||
Copy
|
||||
</button>
|
||||
<button id="use-enhanced-btn" class="px-3 py-1.5 rounded-lg text-xs font-bold bg-primary text-black hover:shadow-glow transition-all">
|
||||
Use in Generator
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(toolsPanel);
|
||||
|
||||
// ==========================================
|
||||
// 4. ADVANCED OPTIONS PANEL
|
||||
// ==========================================
|
||||
const STYLE_PRESETS = ['None', 'Photorealistic', 'Anime', 'Cinematic', 'Oil Painting', 'Watercolor', 'Digital Art', 'Concept Art', 'Cyberpunk'];
|
||||
|
||||
const advancedPanel = document.createElement('div');
|
||||
advancedPanel.className = 'w-full max-w-4xl mt-6 animate-fade-in-up hidden';
|
||||
advancedPanel.id = 'advanced-panel';
|
||||
advancedPanel.innerHTML = `
|
||||
<div class="bg-[#111]/90 backdrop-blur-xl border border-white/10 rounded-2xl p-5 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-white/5">
|
||||
<h3 class="text-sm font-bold text-white">Advanced Options</h3>
|
||||
<button id="close-adv-btn" class="text-white/40 hover:text-white transition-colors">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Style Presets -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Style Preset</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
${STYLE_PRESETS.map(s => `<button class="style-preset-btn px-3 py-1.5 rounded-lg text-xs font-bold bg-white/5 text-secondary hover:bg-white/10 transition-all" data-style="${s}">${s}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Negative Prompt -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Negative Prompt</label>
|
||||
<input type="text" id="negative-prompt-input"
|
||||
placeholder="What to exclude from the image (e.g., blurry, distorted, watermark)"
|
||||
class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-sm placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors">
|
||||
</div>
|
||||
|
||||
<!-- Guidance Scale & Steps Row -->
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<div class="flex-1 min-w-[200px] flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Guidance Scale</label>
|
||||
<span id="guidance-value" class="text-xs font-bold text-primary">7.5</span>
|
||||
</div>
|
||||
<input type="range" id="guidance-slider" min="1" max="20" step="0.5" value="7.5"
|
||||
class="w-full h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-[200px] flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Steps</label>
|
||||
<span id="steps-value" class="text-xs font-bold text-primary">25</span>
|
||||
</div>
|
||||
<input type="range" id="steps-slider" min="1" max="50" step="1" value="25"
|
||||
class="w-full h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seed -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Seed</label>
|
||||
<button id="randomize-seed-btn" class="text-xs font-bold text-primary hover:text-primary/80 transition-colors">Randomize</button>
|
||||
</div>
|
||||
<input type="number" id="seed-input"
|
||||
placeholder="-1 for random"
|
||||
value="-1"
|
||||
class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-sm placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors">
|
||||
</div>
|
||||
|
||||
<!-- Batch Count -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Batch Count</label>
|
||||
<span id="batch-value" class="text-xs font-bold text-primary">1</span>
|
||||
</div>
|
||||
<input type="range" id="batch-slider" min="1" max="4" step="1" value="1"
|
||||
class="w-full h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary">
|
||||
</div>
|
||||
|
||||
<!-- Width & Height -->
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<div class="flex-1 min-w-[120px] flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Width</label>
|
||||
<input type="number" id="width-input"
|
||||
placeholder="Auto"
|
||||
value=""
|
||||
class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-sm placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors">
|
||||
</div>
|
||||
<div class="flex-1 min-w-[120px] flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Height</label>
|
||||
<input type="number" id="height-input"
|
||||
placeholder="Auto"
|
||||
value=""
|
||||
class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-sm placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Strength (for I2I models) -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">Reference Strength</label>
|
||||
<span id="reference-strength-value" class="text-xs font-bold text-primary">50%</span>
|
||||
</div>
|
||||
<input type="range" id="reference-strength-slider" min="0" max="100" step="5" value="50"
|
||||
class="w-full h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary">
|
||||
<p class="text-xs text-muted">How much to preserve the reference image characteristics</p>
|
||||
</div>
|
||||
|
||||
<!-- LoRA Model Selection -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-secondary uppercase tracking-wider">LoRA Model (Optional)</label>
|
||||
<input type="text" id="lora-input"
|
||||
placeholder="e.g., civitai:1642876@1864626"
|
||||
class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white text-sm placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors">
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label class="text-xs font-bold text-secondary">LoRA Weight:</label>
|
||||
<input type="number" id="lora-weight-input"
|
||||
value="1.0" min="0" max="4" step="0.1"
|
||||
class="w-20 bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-white text-sm focus:outline-none focus:border-primary/50 transition-colors">
|
||||
</div>
|
||||
<p class="text-xs text-muted">Enter a LoRA model ID from Civitai (format: civitai:id@version)</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(advancedPanel);
|
||||
|
||||
// Advanced panel toggle logic
|
||||
const toggleAdvanced = () => {
|
||||
showAdvanced = !showAdvanced;
|
||||
advancedPanel.classList.toggle('hidden', !showAdvanced);
|
||||
document.getElementById('advanced-btn-label').textContent = showAdvanced ? 'Less' : 'Advanced';
|
||||
};
|
||||
|
||||
// Add tools panel and advanced panel to container first before accessing their elements
|
||||
container.appendChild(toolsPanel);
|
||||
container.appendChild(advancedPanel);
|
||||
|
||||
// Now set up event handlers after elements are in DOM
|
||||
advancedBtn.onclick = toggleAdvanced;
|
||||
const closeAdvBtn = advancedPanel.querySelector('#close-adv-btn');
|
||||
if (closeAdvBtn) closeAdvBtn.onclick = toggleAdvanced;
|
||||
|
||||
// Quick Tools Panel toggle
|
||||
const toggleTools = () => {
|
||||
showToolsPanel = !showToolsPanel;
|
||||
toolsPanel.classList.toggle('hidden', !showToolsPanel);
|
||||
if (showToolsPanel) {
|
||||
// Close advanced panel when opening tools
|
||||
if (!showAdvanced) {
|
||||
showAdvanced = true;
|
||||
advancedPanel.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
document.getElementById('tools-btn-label').textContent = showToolsPanel ? 'Tools' : 'Tools';
|
||||
};
|
||||
|
||||
toolsBtn.onclick = toggleTools;
|
||||
const closeToolsBtn = toolsPanel.querySelector('#close-tools-btn');
|
||||
if (closeToolsBtn) closeToolsBtn.onclick = toggleTools;
|
||||
|
||||
// Quick Starter buttons
|
||||
const quickStarterBtns = toolsPanel.querySelectorAll('.quick-starter-btn');
|
||||
quickStarterBtns.forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const prompt = btn.dataset.prompt;
|
||||
textarea.value = prompt;
|
||||
textarea.style.height = 'auto';
|
||||
const maxHeight = window.innerWidth < 768 ? 150 : 250;
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
||||
// Close tools panel after selection
|
||||
showToolsPanel = false;
|
||||
toolsPanel.classList.add('hidden');
|
||||
};
|
||||
});
|
||||
|
||||
// Prompt Enhancer - selected tags state
|
||||
const enhanceSelectedTags = new Set();
|
||||
const basePromptInput = toolsPanel.querySelector('#base-prompt-input');
|
||||
const enhancedPromptDisplay = toolsPanel.querySelector('#enhanced-prompt-display');
|
||||
|
||||
// Update enhanced prompt display
|
||||
const updateEnhancedPrompt = () => {
|
||||
const base = basePromptInput?.value?.trim() || '';
|
||||
const tags = Array.from(enhanceSelectedTags).join(', ');
|
||||
const enhanced = [base, tags].filter(p => p).join(', ');
|
||||
if (enhancedPromptDisplay) {
|
||||
enhancedPromptDisplay.textContent = enhanced || 'Your enhanced prompt will appear here...';
|
||||
enhancedPromptDisplay.classList.toggle('text-muted', !enhanced);
|
||||
}
|
||||
};
|
||||
|
||||
// Base prompt input handler
|
||||
if (basePromptInput) {
|
||||
basePromptInput.oninput = updateEnhancedPrompt;
|
||||
}
|
||||
|
||||
// Enhance tag buttons
|
||||
const enhanceTagBtns = toolsPanel.querySelectorAll('.enhance-tag-btn');
|
||||
enhanceTagBtns.forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const tag = btn.dataset.tag;
|
||||
if (enhanceSelectedTags.has(tag)) {
|
||||
enhanceSelectedTags.delete(tag);
|
||||
btn.classList.remove('bg-primary', 'text-black');
|
||||
btn.classList.add('bg-white/5', 'text-secondary');
|
||||
} else {
|
||||
enhanceSelectedTags.add(tag);
|
||||
btn.classList.remove('bg-white/5', 'text-secondary');
|
||||
btn.classList.add('bg-primary', 'text-black');
|
||||
}
|
||||
updateEnhancedPrompt();
|
||||
};
|
||||
});
|
||||
|
||||
// Copy enhanced button
|
||||
const copyEnhancedBtn = toolsPanel.querySelector('#copy-enhanced-btn');
|
||||
if (copyEnhancedBtn) {
|
||||
copyEnhancedBtn.onclick = () => {
|
||||
const text = enhancedPromptDisplay?.textContent || '';
|
||||
if (text && text !== 'Your enhanced prompt will appear here...') {
|
||||
navigator.clipboard.writeText(text);
|
||||
copyEnhancedBtn.textContent = 'Copied!';
|
||||
setTimeout(() => { copyEnhancedBtn.textContent = 'Copy'; }, 1500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Use enhanced button
|
||||
const useEnhancedBtn = toolsPanel.querySelector('#use-enhanced-btn');
|
||||
if (useEnhancedBtn) {
|
||||
useEnhancedBtn.onclick = () => {
|
||||
const text = enhancedPromptDisplay?.textContent || '';
|
||||
if (text && text !== 'Your enhanced prompt will appear here...') {
|
||||
textarea.value = text;
|
||||
textarea.style.height = 'auto';
|
||||
const maxHeight = window.innerWidth < 768 ? 150 : 250;
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
||||
// Close tools panel after use
|
||||
showToolsPanel = false;
|
||||
toolsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Negative prompt
|
||||
const negPromptInput = advancedPanel.querySelector('#negative-prompt-input');
|
||||
if (negPromptInput) negPromptInput.oninput = (e) => { negativePrompt = e.target.value; };
|
||||
|
||||
// Guidance scale slider
|
||||
const guidanceSlider = advancedPanel.querySelector('#guidance-slider');
|
||||
const guidanceValue = advancedPanel.querySelector('#guidance-value');
|
||||
if (guidanceSlider && guidanceValue) {
|
||||
guidanceSlider.oninput = (e) => {
|
||||
guidanceScale = parseFloat(e.target.value);
|
||||
guidanceValue.textContent = guidanceScale;
|
||||
};
|
||||
}
|
||||
|
||||
// Steps slider
|
||||
const stepsSlider = advancedPanel.querySelector('#steps-slider');
|
||||
const stepsValue = advancedPanel.querySelector('#steps-value');
|
||||
if (stepsSlider && stepsValue) {
|
||||
stepsSlider.oninput = (e) => {
|
||||
steps = parseInt(e.target.value);
|
||||
stepsValue.textContent = steps;
|
||||
};
|
||||
}
|
||||
|
||||
// Seed input
|
||||
const seedInput = advancedPanel.querySelector('#seed-input');
|
||||
if (seedInput) seedInput.oninput = (e) => { seed = parseInt(e.target.value) || -1; };
|
||||
|
||||
// Randomize seed button
|
||||
const randSeedBtn = advancedPanel.querySelector('#randomize-seed-btn');
|
||||
if (randSeedBtn) {
|
||||
randSeedBtn.onclick = () => {
|
||||
seed = Math.floor(Math.random() * 999999999);
|
||||
if (seedInput) seedInput.value = seed;
|
||||
};
|
||||
}
|
||||
|
||||
// Batch count slider
|
||||
const batchSlider = advancedPanel.querySelector('#batch-slider');
|
||||
const batchValueEl = advancedPanel.querySelector('#batch-value');
|
||||
if (batchSlider && batchValueEl) {
|
||||
batchSlider.oninput = (e) => {
|
||||
batchCount = parseInt(e.target.value);
|
||||
batchValueEl.textContent = batchCount;
|
||||
};
|
||||
}
|
||||
|
||||
// Width input
|
||||
const widthInput = advancedPanel.querySelector('#width-input');
|
||||
if (widthInput) {
|
||||
widthInput.oninput = (e) => {
|
||||
customWidth = parseInt(e.target.value) || 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Height input
|
||||
const heightInput = advancedPanel.querySelector('#height-input');
|
||||
if (heightInput) {
|
||||
heightInput.oninput = (e) => {
|
||||
customHeight = parseInt(e.target.value) || 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Reference strength slider
|
||||
const refStrengthSlider = advancedPanel.querySelector('#reference-strength-slider');
|
||||
const refStrengthValue = advancedPanel.querySelector('#reference-strength-value');
|
||||
if (refStrengthSlider && refStrengthValue) {
|
||||
refStrengthSlider.oninput = (e) => {
|
||||
referenceStrength = parseInt(e.target.value);
|
||||
refStrengthValue.textContent = referenceStrength + '%';
|
||||
};
|
||||
}
|
||||
|
||||
// LoRA input
|
||||
const loraInput = advancedPanel.querySelector('#lora-input');
|
||||
if (loraInput) {
|
||||
loraInput.oninput = (e) => {
|
||||
selectedLora = e.target.value.trim();
|
||||
};
|
||||
}
|
||||
|
||||
// LoRA weight input
|
||||
const loraWeightInput = advancedPanel.querySelector('#lora-weight-input');
|
||||
if (loraWeightInput) {
|
||||
loraWeightInput.oninput = (e) => {
|
||||
loraWeight = parseFloat(e.target.value) || 1.0;
|
||||
};
|
||||
}
|
||||
|
||||
// Style preset handlers
|
||||
advancedPanel.querySelectorAll('.style-preset-btn').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
selectedStyle = btn.dataset.style;
|
||||
advancedPanel.querySelectorAll('.style-preset-btn').forEach(b => {
|
||||
b.classList.remove('bg-primary/20', 'text-primary', 'border-primary/30');
|
||||
b.classList.add('bg-white/5', 'text-secondary');
|
||||
});
|
||||
btn.classList.add('bg-primary/20', 'text-primary', 'border-primary/30');
|
||||
btn.classList.remove('bg-white/5', 'text-secondary');
|
||||
};
|
||||
});
|
||||
// ==========================================
|
||||
// 3. DROPDOWNS (Professional implementation)
|
||||
// ==========================================
|
||||
|
|
|
|||
|
|
@ -252,10 +252,11 @@ export function VideoStudio() {
|
|||
const controlsLeft = document.createElement('div');
|
||||
controlsLeft.className = 'flex items-center gap-1.5 md:gap-2.5 relative overflow-x-auto no-scrollbar pb-1 md:pb-0';
|
||||
|
||||
const createControlBtn = (icon, label, id) => {
|
||||
const createControlBtn = (icon, label, id, tooltip) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.id = id;
|
||||
btn.className = 'flex items-center gap-1.5 md:gap-2.5 px-3 md:px-4 py-2 md:py-2.5 bg-white/5 hover:bg-white/10 rounded-xl md:rounded-2xl transition-all border border-white/5 group whitespace-nowrap';
|
||||
if (tooltip) btn.setAttribute('data-tooltip', tooltip);
|
||||
btn.innerHTML = `
|
||||
${icon}
|
||||
<span id="${id}-label" class="text-xs font-bold text-white group-hover:text-primary transition-colors">${label}</span>
|
||||
|
|
@ -268,23 +269,23 @@ export function VideoStudio() {
|
|||
<div class="w-5 h-5 bg-primary rounded-md flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<span class="text-[10px] font-black text-black">V</span>
|
||||
</div>
|
||||
`, selectedModelName, 'v-model-btn');
|
||||
`, selectedModelName, 'v-model-btn', 'Select AI video model');
|
||||
|
||||
const arBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>
|
||||
`, selectedAr, 'v-ar-btn');
|
||||
`, selectedAr, 'v-ar-btn', 'Change aspect ratio');
|
||||
|
||||
const durationBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
`, `${selectedDuration}s`, 'v-duration-btn');
|
||||
`, `${selectedDuration}s`, 'v-duration-btn', 'Set video duration');
|
||||
|
||||
const resolutionBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z"/></svg>
|
||||
`, selectedResolution || '720p', 'v-resolution-btn');
|
||||
`, selectedResolution || '720p', 'v-resolution-btn', 'Set output resolution');
|
||||
|
||||
const qualityBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
`, selectedQuality || 'basic', 'v-quality-btn');
|
||||
`, selectedQuality || 'basic', 'v-quality-btn', 'Set output quality');
|
||||
|
||||
const modeBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
|
|
@ -295,7 +296,12 @@ export function VideoStudio() {
|
|||
controlsLeft.appendChild(durationBtn);
|
||||
controlsLeft.appendChild(resolutionBtn);
|
||||
controlsLeft.appendChild(qualityBtn);
|
||||
controlsLeft.appendChild(modeBtn);
|
||||
|
||||
// Advanced options toggle button
|
||||
const advancedBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 001.82-.33 1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-1.82.33A1.65 1.65 0 0019.4 9a1.65 1.65 0 00-1.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||||
`, 'Advanced', 'v-advanced-btn', 'Show advanced options');
|
||||
controlsLeft.appendChild(advancedBtn);
|
||||
|
||||
// Initial visibility (t2v mode)
|
||||
const initDurations = getDurationsForModel(defaultModel.id);
|
||||
|
|
@ -307,6 +313,7 @@ export function VideoStudio() {
|
|||
|
||||
const generateBtn = document.createElement('button');
|
||||
generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg';
|
||||
generateBtn.setAttribute('data-tooltip', 'Generate AI video from prompt');
|
||||
generateBtn.innerHTML = `Generate ✨`;
|
||||
|
||||
bottomRow.appendChild(controlsLeft);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
export const ENHANCE_TAGS = {
|
||||
quality: ['professional photography', 'ultra-detailed', '8K resolution', 'high dynamic range', 'award-winning'],
|
||||
lighting: ['cinematic lighting', 'golden hour', 'dramatic studio lighting', 'soft diffused light', 'neon glow', 'volumetric rays'],
|
||||
mood: ['moody atmosphere', 'serene and peaceful', 'epic and dramatic', 'warm and cozy', 'dark and mysterious'],
|
||||
style: ['photorealistic', 'oil painting style', 'watercolor', 'digital art', 'concept art', 'anime style', 'cyberpunk aesthetic'],
|
||||
};
|
||||
|
||||
export const QUICK_PROMPTS = [
|
||||
{ label: 'Portrait', prompt: 'Professional portrait photograph, shallow depth of field, soft studio lighting, 85mm lens' },
|
||||
{ label: 'Landscape', prompt: 'Breathtaking landscape photograph, golden hour, wide angle, dramatic clouds, 4K' },
|
||||
{ label: 'Product', prompt: 'Commercial product photography, clean white background, studio lighting, professional' },
|
||||
{ label: 'Fantasy', prompt: 'Epic fantasy scene, magical atmosphere, volumetric lighting, highly detailed, concept art' },
|
||||
{ label: 'Sci-Fi', prompt: 'Futuristic sci-fi environment, neon lights, cyberpunk city, rain reflections, cinematic' },
|
||||
{ label: 'Food', prompt: 'Professional food photography, appetizing, warm lighting, shallow depth of field, editorial' },
|
||||
{ label: 'Architecture', prompt: 'Architectural photography, dramatic angles, clean lines, modern design, professional' },
|
||||
{ label: 'Fashion', prompt: 'High fashion editorial, avant-garde styling, studio lighting, Vogue aesthetic, professional' },
|
||||
];
|
||||
|
||||
export const CAMERA_MAP = {
|
||||
"Modular 8K Digital": "modular 8K digital cinema camera",
|
||||
"Full-Frame Cine Digital": "full-frame digital cinema camera",
|
||||
|
|
|
|||
|
|
@ -72,4 +72,181 @@
|
|||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
||||
}
|
||||
|
||||
/* Thumbnail cards */
|
||||
.thumb-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thumb-hero img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.thumb-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60%;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.thumb-hero:hover img,
|
||||
.group:hover .thumb-hero img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.thumb-skeleton {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.thumb-fallback .thumb-hero {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-banner img {
|
||||
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.hero-banner:hover img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in-up {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thumb-hero img {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.thumb-skeleton {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================
|
||||
TOOLTIP SYSTEM
|
||||
======================== */
|
||||
|
||||
/* Base tooltip container */
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Tooltip arrow */
|
||||
[data-tooltip]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #1a1a1a;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Tooltip body */
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
padding: 8px 14px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Show tooltip on hover */
|
||||
[data-tooltip]:hover::before,
|
||||
[data-tooltip]:hover::after {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(-6px);
|
||||
}
|
||||
|
||||
/* Tooltip positioning variants */
|
||||
[data-tooltip-bottom]::before {
|
||||
bottom: auto;
|
||||
top: 100%;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #1a1a1a;
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
}
|
||||
|
||||
[data-tooltip-bottom]::after {
|
||||
bottom: auto;
|
||||
top: 100%;
|
||||
transform: translateX(-50%) translateY(-12px);
|
||||
}
|
||||
|
||||
[data-tooltip-bottom]:hover::before,
|
||||
[data-tooltip-bottom]:hover::after {
|
||||
transform: translateX(-50%) translateY(6px);
|
||||
}
|
||||
|
||||
/* Tooltip for left-aligned elements */
|
||||
[data-tooltip-left]::before {
|
||||
left: 0;
|
||||
transform: translateX(-100%) translateY(-50%);
|
||||
}
|
||||
|
||||
[data-tooltip-left]::after {
|
||||
left: 0;
|
||||
transform: translateX(calc(-100% - 10px)) translateY(-50%);
|
||||
}
|
||||
|
||||
[data-tooltip-left]:hover::before,
|
||||
[data-tooltip-left]:hover::after {
|
||||
transform: translateX(calc(-100% - 6px)) translateY(-50%);
|
||||
}
|
||||
|
||||
/* Tooltip for right-aligned elements */
|
||||
[data-tooltip-right]::before {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(100%) translateY(-50%);
|
||||
}
|
||||
|
||||
[data-tooltip-right]::after {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(calc(100% + 10px)) translateY(-50%);
|
||||
}
|
||||
|
||||
[data-tooltip-right]:hover::before,
|
||||
[data-tooltip-right]:hover::after {
|
||||
transform: translateX(calc(100% + 6px)) translateY(-50%);
|
||||
}
|
||||
Loading…
Reference in a new issue