Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GODSRODS AI Visualizer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom styles for a premium, dark 'GODSRODS' theme */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700;900&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #121212; /* Deep Black */ | |
| min-height: 100vh; | |
| color: #ffffff; | |
| padding: 0; | |
| } | |
| /* === MODAL STYLES === */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.9); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .modal-content { | |
| background-color: #121212; | |
| border-radius: 1rem; | |
| max-width: 1250px; /* Increased max width to fit sidebar */ | |
| width: 95%; | |
| height: 95vh; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| border: 2px solid #3a3a3a; | |
| } | |
| .modal-header { | |
| padding: 1rem 1.5rem; | |
| border-bottom: 1px solid #3a3a3a; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background-color: #212121; | |
| } | |
| .modal-body-flex { | |
| flex-grow: 1; | |
| display: flex; | |
| overflow: hidden; /* Prevent horizontal scroll from inner content */ | |
| } | |
| /* === HISTORY SIDEBAR STYLES === */ | |
| #historySidebar { | |
| background-color: #1a1a1a; | |
| width: 4rem; /* Initial narrow width (64px) */ | |
| transition: width 0.3s ease-in-out; | |
| overflow: hidden; | |
| border-right: 1px solid #3a3a3a; | |
| flex-shrink: 0; | |
| padding: 0.5rem 0; | |
| } | |
| #historySidebar.is-expanded { | |
| width: 14rem; /* Expanded width (224px) */ | |
| } | |
| .sidebar-header { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 0.5rem 0; | |
| cursor: pointer; | |
| color: #fbc02d; | |
| border-bottom: 1px solid #333; | |
| margin-bottom: 0.5rem; | |
| } | |
| .sidebar-icon-text { | |
| display: none; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| } | |
| #historySidebar.is-expanded .sidebar-icon-text { | |
| display: block; | |
| margin-left: 0.5rem; | |
| } | |
| .sidebar-list { | |
| overflow-y: auto; | |
| height: calc(100% - 3rem); /* Adjust based on header height */ | |
| padding: 0 0.5rem; | |
| } | |
| .car-thumbnail-container { | |
| margin-bottom: 0.5rem; | |
| cursor: pointer; | |
| transition: transform 0.1s, box-shadow 0.2s; | |
| border-radius: 0.375rem; | |
| overflow: hidden; | |
| background-color: #2c2c2c; | |
| border: 2px solid transparent; | |
| } | |
| .car-thumbnail-container:hover { | |
| transform: scale(1.02); | |
| border-color: #fbc02d; | |
| } | |
| .car-thumbnail-img { | |
| aspect-ratio: 16 / 9; | |
| object-fit: cover; | |
| width: 100%; | |
| } | |
| .car-thumbnail-name { | |
| display: none; | |
| padding: 0.25rem; | |
| font-size: 0.75rem; | |
| text-align: center; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| color: #ddd; | |
| } | |
| #historySidebar.is-expanded .car-thumbnail-name { | |
| display: block; | |
| } | |
| /* === MAIN CONTENT STYLES === */ | |
| .modal-main-content { | |
| flex-grow: 1; | |
| overflow-y: auto; | |
| padding: 1.5rem; | |
| } | |
| .container-inner { | |
| max-width: 100%; | |
| margin: 0 auto; | |
| } | |
| /* === APP COMPONENT STYLES === */ | |
| .panel { | |
| background-color: #212121; | |
| border-radius: 0.75rem; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); | |
| border: 1px solid #3a3a3a; | |
| } | |
| .accordion-header { | |
| background-color: #2c2c2c; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| } | |
| .accordion-header:hover { | |
| background-color: #383838; | |
| } | |
| .accordion-content { | |
| background-color: #212121; | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.4s ease-out; | |
| } | |
| .accordion-content.active { | |
| max-height: 800px; /* Increased max-height for content visibility */ | |
| } | |
| .input-style { | |
| background-color: #333333; | |
| border: 1px solid #4f4f4f; | |
| color: #ffffff; | |
| border-radius: 0.375rem; | |
| } | |
| .input-style:focus { | |
| ring-color: #fbc02d; /* Gold accent */ | |
| border-color: #fbc02d; | |
| } | |
| .btn-gold { | |
| background-color: #fbc02d; /* Gold accent */ | |
| color: #121212; | |
| transition: all 0.2s; | |
| font-weight: 700; | |
| } | |
| .btn-gold:hover:not(:disabled) { | |
| background-color: #e0a800; | |
| transform: translateY(-1px); | |
| } | |
| .btn-gold:disabled { | |
| background-color: #ffeb3b; | |
| cursor: not-allowed; | |
| opacity: 0.6; | |
| } | |
| .accent-text { | |
| color: #fbc02d; | |
| } | |
| /* Responsive Grid for Media-Text Layout */ | |
| @media (min-width: 768px) { | |
| .media-text-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1.5fr; /* 1:1.5 ratio for controls and visualizer */ | |
| gap: 1.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Floating Button to Open the Garage --> | |
| <button id="openGarageBtn" | |
| class="fixed bottom-4 right-4 z-[1001] bg-gray-800 text-white p-3 rounded-full shadow-lg font-bold hover:bg-gray-700 transition duration-200 focus:outline-none focus:ring-4 focus:ring-yellow-500 focus:ring-opacity-50"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="#fbc02d" class="w-6 h-6 inline-block mr-1"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 0 1-3 0 1.5 1.5 0 0 1 3 0ZM15.75 18.75a1.5 1.5 0 0 1-3 0 1.5 1.5 0 0 1 3 0ZM19.5 7.5a1.5 1.5 0 0 0-3 0m3 0a1.5 1.5 0 0 1-3 0m3 0h-3m-3 0h-3m-1.5 0h-3V6a3 3 0 0 1 3-3h15.5a1.5 1.5 0 0 1 1.5 1.5v3.627a1.5 1.5 0 0 1-.988 1.411L18 13.5m-9 3.75h3M3 13.5H.75v-6h11.559L12 7.5a1.5 1.5 0 0 1 1.5-1.5h1.5M10.5 7.5V6a1.5 1.5 0 0 0-1.5-1.5h-3A1.5 1.5 0 0 0 4.5 6v1.5" /> | |
| </svg> | |
| Garage | |
| </button> | |
| <!-- Full-Screen Modal/Garage Wrapper --> | |
| <div id="garageModal" class="modal-overlay"> | |
| <div class="modal-content"> | |
| <!-- Modal Header --> | |
| <div class="modal-header"> | |
| <h1 class="text-3xl font-black accent-text">GODSRODS â¡ VISUALIZER GARAGE</h1> | |
| <button id="closeModalBtn" class="text-4xl text-gray-400 hover:text-white transition duration-200 p-2 leading-none"> | |
| × | |
| </button> | |
| </div> | |
| <!-- Modal Body (History Sidebar + Main Content) --> | |
| <div class="modal-body-flex"> | |
| <!-- HISTORY SIDEBAR --> | |
| <div id="historySidebar"> | |
| <div id="sidebarHeader" class="sidebar-header"> | |
| <!-- Car Icon --> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-6 h-6"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 21v-8.25m0 0l-3 3m3-3l3 3m-11.25 0V3m14.5 18v-8.25m0 0l3-3m-3 3l-3-3m-4.5 3h6m-12 0h.008v.008H3v-.008Zm12 0h.008v.008h-.008v-.008Zm4.5 4.5h.008v.008H19.5v-.008Z" /> | |
| </svg> | |
| <span class="sidebar-icon-text">BUILD HISTORY</span> | |
| </div> | |
| <div id="historyList" class="sidebar-list"> | |
| <!-- Thumbnails will be injected here --> | |
| <p id="historyPlaceholder" class="text-gray-500 text-xs text-center p-2 hidden"> | |
| Generate your first car to start your history! | |
| </p> | |
| </div> | |
| </div> | |
| <!-- MAIN VISUALIZER CONTENT --> | |
| <div class="modal-main-content"> | |
| <div id="app" class="container-inner"> | |
| <p class="text-gray-400 mt-2 mb-8 text-center">Design your ultimate fantasy machine and render it with AI.</p> | |
| <div class="media-text-grid"> | |
| <!-- CONTROL PANEL (LEFT/TOP) --> | |
| <div class="space-y-4"> | |
| <div class="panel"> | |
| <div class="accordion-header p-4 flex justify-between items-center rounded-t-xl" data-target="exteriorContent"> | |
| <h2 class="text-xl font-semibold accent-text">1. Exterior Customization</h2> | |
| <span class="text-3xl font-bold">+</span> | |
| </div> | |
| <div id="exteriorContent" class="accordion-content active"> | |
| <div class="p-4 grid grid-cols-2 gap-4"> | |
| <div> | |
| <label for="bodyStyle" class="block text-xs font-medium mb-1">Body Style</label> | |
| <select id="bodyStyle" class="w-full p-2 input-style"> | |
| <option value="Extreme Widebody Kit">Extreme Widebody Kit</option> | |
| <option value="Sleek Hypercar">Sleek Hypercar</option> | |
| <option value="Off-Road Rally Truck">Off-Road Rally Truck</option> | |
| <option value="1969 Classic Muscle Car">1969 Classic Muscle Car</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="color" class="block text-xs font-medium mb-1">Primary Color</label> | |
| <select id="color" class="w-full p-2 input-style"> | |
| <option value="Matte Dark Chrome">Matte Dark Chrome</option> | |
| <option value="Deep Candy Red">Deep Candy Red</option> | |
| <option value="Molten Gold Metallic">Molten Gold Metallic</option> | |
| <option value="Battle-Scarred Grey">Battle-Scarred Grey</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="aero" class="block text-xs font-medium mb-1">Aero/Spoiler</label> | |
| <select id="aero" class="w-full p-2 input-style"> | |
| <option value="Twin Blade GT Wing">Twin Blade GT Wing</option> | |
| <option value="Aggressive Diffuser">Aggressive Diffuser</option> | |
| <option value="No Spoiler">No Spoiler</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="finish" class="block text-xs font-medium mb-1">Finish/Material</label> | |
| <select id="finish" class="w-full p-2 input-style"> | |
| <option value="Forged Carbon Fiber panels">Forged Carbon Fiber panels</option> | |
| <option value="High-Gloss Wet Paint">High-Gloss Wet Paint</option> | |
| <option value="Worn, Rusty Metal">Worn, Rusty Metal</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="accordion-header p-4 flex justify-between items-center" data-target="performanceContent"> | |
| <h2 class="text-xl font-semibold accent-text">2. Performance & Wheels</h2> | |
| <span class="text-3xl font-bold">+</span> | |
| </div> | |
| <div id="performanceContent" class="accordion-content"> | |
| <div class="p-4 grid grid-cols-2 gap-4"> | |
| <div> | |
| <label for="wheels" class="block text-xs font-medium mb-1">Wheel Type</label> | |
| <select id="wheels" class="w-full p-2 input-style"> | |
| <option value="Forged Bronze Monoblock Rims">Forged Bronze Monoblock Rims</option> | |
| <option value="Wide Drag Slicks">Wide Drag Slicks</option> | |
| <option value="Chunky All-Terrain Tires">Chunky All-Terrain Tires</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="suspension" class="block text-xs font-medium mb-1">Ride Height</label> | |
| <select id="suspension" class="w-full p-2 input-style"> | |
| <option value="Slammed on Air Suspension">Slammed on Air Suspension</option> | |
| <option value="High Suspension Lift">High Suspension Lift</option> | |
| <option value="Perfect Track Stance">Perfect Track Stance</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="exhaust" class="block text-xs font-medium mb-1">Exhaust System</label> | |
| <select id="exhaust" class="w-full p-2 input-style"> | |
| <option value="Quad Tips, Backfiring">Quad Tips, Backfiring</option> | |
| <option value="Massive Upward Stacks">Massive Upward Stacks</option> | |
| <option value="Single Cannon Exit">Single Cannon Exit</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="brakes" class="block text-xs font-medium mb-1">Brake Calipers</label> | |
| <select id="brakes" class="w-full p-2 input-style"> | |
| <option value="Gold Carbon Ceramic Brakes">Gold Carbon Ceramic Brakes</option> | |
| <option value="Blood Red Calipers">Blood Red Calipers</option> | |
| <option value="Oversized Drilled Rotors">Oversized Drilled Rotors</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="accordion-header p-4 flex justify-between items-center rounded-b-xl" data-target="stagingContent"> | |
| <h2 class="text-xl font-semibold accent-text">3. Staging & Environment</h2> | |
| <span class="text-3xl font-bold">+</span> | |
| </div> | |
| <div id="stagingContent" class="accordion-content"> | |
| <div class="p-4 grid grid-cols-2 gap-4"> | |
| <div> | |
| <label for="setting" class="block text-xs font-medium mb-1">Scene Setting</label> | |
| <select id="setting" class="w-full p-2 input-style"> | |
| <option value="Abandoned Neo-Tokyo Alley at Night">Abandoned Neo-Tokyo Alley at Night</option> | |
| <option value="Mountain Pass, Heavy Fog">Mountain Pass, Heavy Fog</option> | |
| <option value="Modern Art Gallery">Modern Art Gallery</option> | |
| <option value="Dusty Post-Apocalyptic Road">Dusty Post-Apocalyptic Road</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="style" class="block text-xs font-medium mb-1">Art Style</label> | |
| <select id="style" class="w-full p-2 input-style"> | |
| <option value="Cinematic Photo, Nikon Z9, F/1.4">Cinematic Photo, Nikon Z9, F/1.4</option> | |
| <option value="Concept Art, Digital Painting">Concept Art, Digital Painting</option> | |
| <option value="Vaporwave, Synthwave lighting">Vaporwave, Synthwave lighting</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="camera" class="block text-xs font-medium mb-1">Camera Angle</label> | |
| <select id="camera" class="w-full p-2 input-style"> | |
| <option value="Ground Level, dramatic wide-angle">Ground Level, dramatic wide-angle</option> | |
| <option value="Dynamic Action Shot">Dynamic Action Shot</option> | |
| <option value="Front 3/4 View, bokeh">Front 3/4 View, bokeh</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="artist" class="block text-xs font-medium mb-1">Artistic Influence</label> | |
| <select id="artist" class="w-full p-2 input-style"> | |
| <option value="by Mike Winkelmann, highly detailed">by Mike Winkelmann, highly detailed</option> | |
| <option value="by Simon Stålenhag, retro sci-fi">by Simon Stålenhag, retro sci-fi</option> | |
| <option value="Unreal Engine 5 render, ray tracing">Unreal Engine 5 render, ray tracing</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Generation Button --> | |
| <button id="generateBtn" | |
| class="btn-gold w-full px-8 py-3 rounded-lg font-bold shadow-2xl" | |
| onclick="generateImage()"> | |
| GENERATE GODSROD (AI Render) | |
| </button> | |
| </div> | |
| <!-- VISUALIZER PANEL (RIGHT/BOTTOM) --> | |
| <div class="panel p-6 space-y-4"> | |
| <h2 class="text-2xl font-black accent-text border-b border-gray-700 pb-2">Visualization Output</h2> | |
| <!-- Image Result --> | |
| <div id="imageResult" class="text-center"> | |
| <img id="generatedImage" class="w-full h-auto max-h-[70vh] object-contain rounded-lg border border-gray-600 shadow-xl" | |
| src="https://placehold.co/800x450/212121/fbc02d?text=Configure+and+Generate+Your+GODSROD" | |
| alt="Generated AI Image of Custom Car"> | |
| <p id="errorMsg" class="text-red-500 mt-4 hidden font-semibold">Image generation failed. Please check your network or try a different combination.</p> | |
| <!-- Loading Indicator --> | |
| <div id="loading" class="text-center text-gray-400 font-medium pt-4 hidden"> | |
| <svg class="animate-spin -ml-1 mr-3 h-6 w-6 accent-text inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| Generating image (approx. 10-20 seconds)... | |
| </div> | |
| </div> | |
| <!-- Prompt Output --> | |
| <div class="pt-4 border-t border-gray-700"> | |
| <h3 class="text-sm font-semibold mb-2 text-gray-400">Generated Prompt for AI:</h3> | |
| <textarea id="outputPrompt" rows="4" readonly class="w-full p-2 input-style text-xs opacity-70"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- End Main Content --> | |
| </div> | |
| <!-- End Modal Body --> | |
| </div> | |
| </div> | |
| <!-- End Modal/Garage Wrapper --> | |
| <script> | |
| // In-memory array to store car history (prompt, config, image URL). | |
| // This data is lost when the page is reloaded. | |
| let generatedCars = []; | |
| // --- Modal Logic --- | |
| const garageModal = document.getElementById('garageModal'); | |
| const openBtn = document.getElementById('openGarageBtn'); | |
| const closeBtn = document.getElementById('closeModalBtn'); | |
| function openModal() { | |
| garageModal.style.display = 'flex'; | |
| openBtn.style.display = 'none'; | |
| // Ensure history sidebar is rendered when modal opens | |
| renderHistorySidebar(); | |
| } | |
| function closeModal() { | |
| garageModal.style.display = 'none'; | |
| openBtn.style.display = 'block'; | |
| } | |
| openBtn.addEventListener('click', openModal); | |
| closeBtn.addEventListener('click', closeModal); | |
| garageModal.addEventListener('click', (e) => { | |
| if (e.target === garageModal) { | |
| closeModal(); | |
| } | |
| }); | |
| // --- Sidebar Logic --- | |
| const historySidebar = document.getElementById('historySidebar'); | |
| const sidebarHeader = document.getElementById('sidebarHeader'); | |
| const historyList = document.getElementById('historyList'); | |
| const historyPlaceholder = document.getElementById('historyPlaceholder'); | |
| sidebarHeader.addEventListener('click', () => { | |
| historySidebar.classList.toggle('is-expanded'); | |
| // Re-render to show/hide text content correctly | |
| renderHistorySidebar(); | |
| }); | |
| function saveCarToHistory(prompt, imageUrl, config) { | |
| const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); | |
| const newCar = { | |
| id: Date.now(), | |
| name: `${config.bodyStyle.split(' ')[0]}-${config.color.split(' ')[0]}`, | |
| prompt: prompt, | |
| imageUrl: imageUrl, | |
| config: config, | |
| timestamp: timestamp | |
| }; | |
| // Add new car to the beginning of the array | |
| generatedCars.unshift(newCar); | |
| renderHistorySidebar(); | |
| } | |
| function renderHistorySidebar() { | |
| historyList.innerHTML = ''; // Clear previous content | |
| if (generatedCars.length === 0) { | |
| historyPlaceholder.classList.remove('hidden'); | |
| return; | |
| } | |
| historyPlaceholder.classList.add('hidden'); | |
| generatedCars.forEach((car, index) => { | |
| const container = document.createElement('div'); | |
| container.className = 'car-thumbnail-container'; | |
| container.setAttribute('data-index', index); | |
| container.onclick = () => loadCarFromHistory(index); | |
| const img = document.createElement('img'); | |
| img.className = 'car-thumbnail-img'; | |
| img.src = car.imageUrl; | |
| img.alt = car.name; | |
| const name = document.createElement('p'); | |
| name.className = 'car-thumbnail-name'; | |
| name.textContent = car.name; | |
| container.appendChild(img); | |
| container.appendChild(name); | |
| historyList.appendChild(container); | |
| }); | |
| } | |
| function loadCarFromHistory(index) { | |
| const car = generatedCars[index]; | |
| if (!car) return; | |
| // 1. Update form controls | |
| // Map the stored config back to the select elements | |
| Object.keys(car.config).forEach(key => { | |
| const selectEl = document.getElementById(key); | |
| if (selectEl) { | |
| selectEl.value = car.config[key]; | |
| } | |
| }); | |
| // 2. Update visualizer output | |
| generatedImageEl.src = car.imageUrl; | |
| outputPromptEl.value = car.prompt; | |
| // 3. Close and reopen the first accordion to visually confirm load | |
| setupAccordions(true); | |
| } | |
| // --- Accordion Logic --- | |
| const headers = document.querySelectorAll('.accordion-header'); | |
| function setupAccordions(forceOpenFirst = false) { | |
| headers.forEach(header => { | |
| // Remove existing listener to prevent duplicates after re-initialization | |
| header.onclick = null; | |
| const targetId = header.getAttribute('data-target'); | |
| const content = document.getElementById(targetId); | |
| if (forceOpenFirst && header.getAttribute('data-target') === 'exteriorContent') { | |
| // Force open the first accordion | |
| content.classList.add('active'); | |
| content.style.maxHeight = (content.scrollHeight + 50) + "px"; | |
| header.querySelector('span').textContent = 'â'; | |
| } else if (forceOpenFirst) { | |
| // Ensure others are closed | |
| content.classList.remove('active'); | |
| content.style.maxHeight = null; | |
| header.querySelector('span').textContent = '+'; | |
| } | |
| // Re-add the click listener | |
| header.addEventListener('click', () => { | |
| const isActive = content.classList.contains('active'); | |
| // Close all contents first | |
| document.querySelectorAll('.accordion-content').forEach(c => { | |
| c.style.maxHeight = null; | |
| c.classList.remove('active'); | |
| const h = c.previousElementSibling; | |
| if (h) h.querySelector('span').textContent = '+'; | |
| }); | |
| if (!isActive) { | |
| // Open the clicked content | |
| content.classList.add('active'); | |
| content.style.maxHeight = (content.scrollHeight + 50) + "px"; | |
| header.querySelector('span').textContent = 'â'; | |
| } | |
| }); | |
| }); | |
| } | |
| // --- Image Generation Logic --- | |
| const generateBtn = document.getElementById('generateBtn'); | |
| const loadingEl = document.getElementById('loading'); | |
| const outputPromptEl = document.getElementById('outputPrompt'); | |
| const generatedImageEl = document.getElementById('generatedImage'); | |
| const errorMsgEl = document.getElementById('errorMsg'); | |
| function getCurrentConfig() { | |
| return { | |
| bodyStyle: document.getElementById('bodyStyle')?.value || '', | |
| color: document.getElementById('color')?.value || '', | |
| aero: document.getElementById('aero')?.value || '', | |
| finish: document.getElementById('finish')?.value || '', | |
| wheels: document.getElementById('wheels')?.value || '', | |
| suspension: document.getElementById('suspension')?.value || '', | |
| exhaust: document.getElementById('exhaust')?.value || '', | |
| brakes: document.getElementById('brakes')?.value || '', | |
| setting: document.getElementById('setting')?.value || '', | |
| style: document.getElementById('style')?.value || '', | |
| camera: document.getElementById('camera')?.value || '', | |
| artist: document.getElementById('artist')?.value || '' | |
| }; | |
| } | |
| /** | |
| * Gathers all selected values and formats them into a high-quality prompt string. | |
| */ | |
| function collectPromptParts(config) { | |
| // Construct the detailed, high-quality prompt string | |
| const promptParts = [ | |
| 'A custom GODSROD vehicle', | |
| config.color, | |
| config.bodyStyle, | |
| config.finish, | |
| config.aero, | |
| config.wheels, | |
| config.suspension, | |
| config.exhaust, | |
| config.brakes, | |
| 'highly detailed', | |
| 'photorealistic', | |
| config.camera, | |
| config.setting, | |
| config.style, | |
| 'cinematic lighting, film grain', | |
| config.artist | |
| ]; | |
| // Clean up the prompt by removing empty or duplicate values and joining with commas | |
| return promptParts.filter(p => p).join(', '); | |
| } | |
| /** | |
| * Triggers the Pollinations.ai API call by setting the image source URL. | |
| */ | |
| window.generateImage = function() { | |
| const config = getCurrentConfig(); | |
| const rawPrompt = collectPromptParts(config); | |
| outputPromptEl.value = rawPrompt; | |
| // Encode the prompt for the URL, replacing spaces with underscores (common Pollinations practice) | |
| const encodedPrompt = encodeURIComponent(rawPrompt).replace(/%20/g, '_'); | |
| // Pollinations.ai API URL structure | |
| const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=800&height=450&model=sdxl_turbo&nologo=true`; | |
| // UI state management | |
| generateBtn.disabled = true; | |
| loadingEl.classList.remove('hidden'); | |
| errorMsgEl.classList.add('hidden'); | |
| // Show a loading image | |
| generatedImageEl.src = "https://placehold.co/800x450/212121/fbc02d?text=GENERATING...+(Please+Wait)"; | |
| const tempImage = new Image(); | |
| tempImage.crossOrigin = "anonymous"; | |
| tempImage.onload = () => { | |
| const finalImageUrl = tempImage.src; // Use the final loaded URL (which might be redirected) | |
| generatedImageEl.src = finalImageUrl; | |
| // Save successful generation to history | |
| saveCarToHistory(rawPrompt, finalImageUrl, config); | |
| generateBtn.disabled = false; | |
| loadingEl.classList.add('hidden'); | |
| }; | |
| tempImage.onerror = () => { | |
| generatedImageEl.src = "https://placehold.co/800x450/331a1a/ff0000?text=GENERATION+ERROR"; | |
| errorMsgEl.classList.remove('hidden'); | |
| generateBtn.disabled = false; | |
| loadingEl.classList.add('hidden'); | |
| }; | |
| // This initiates the image generation process via the URL | |
| tempImage.src = imageUrl; | |
| }; | |
| // Initialize: setup accordions and set default prompt | |
| window.onload = () => { | |
| setupAccordions(true); // Open first accordion on load | |
| outputPromptEl.value = collectPromptParts(getCurrentConfig()); // Show the default prompt | |
| }; | |
| </script> | |
| </body> | |
| </html> | |