Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chroma-Forge AI | Material Synthesizer</title> | |
| <style> | |
| :root { | |
| --bg: #090a0f; | |
| --panel: #12141d; | |
| --accent: #ff3366; | |
| --text: #e2e8f0; | |
| --glow: rgba(255, 51, 102, 0.4); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Courier New', Courier, monospace; } | |
| body { background-color: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 20px; } | |
| header { width: 100%; max-width: 1000px; display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; } | |
| h1 { color: var(--accent); font-size: 24px; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px var(--glow); } | |
| .api-config { display: flex; gap: 10px; align-items: center; } | |
| .api-config input { background: #000; border: 1px solid #444; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px; width: 200px; outline: none;} | |
| .api-config input:focus { border-color: var(--accent); } | |
| .desk { width: 100%; max-width: 1000px; display: grid; grid-template-columns: 1fr 3fr 1fr; gap: 20px; background: var(--panel); padding: 30px; border-radius: 12px; border: 1px solid #222; box-shadow: 0 10px 50px rgba(0,0,0,0.8); } | |
| .fader-bank { display: flex; flex-direction: column; justify-content: space-between; gap: 20px; } | |
| .slider-wrapper { text-align: center; background: #0a0b10; padding: 15px 10px; border-radius: 8px; border: 1px solid #1a1c23; } | |
| .slider-label { font-size: 11px; color: #888; font-weight: bold; letter-spacing: 1px; margin-bottom: 15px; display: flex; justify-content: space-between; } | |
| .slider-label span.active { color: var(--text); text-shadow: 0 0 5px #fff; } | |
| input[type="range"] { -webkit-appearance: none; width: 100%; height: 4px; background: #333; border-radius: 2px; outline: none; } | |
| input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 24px; background: #555; border: 1px solid #888; border-radius: 2px; cursor: pointer; transition: 0.2s; } | |
| input[type="range"]::-webkit-slider-thumb:hover { background: var(--accent); border-color: #fff; box-shadow: 0 0 10px var(--glow); } | |
| .viewport { position: relative; width: 100%; aspect-ratio: 1/1; background: #000; border-radius: 8px; border: 2px solid #333; overflow: hidden; display: flex; justify-content: center; align-items: center; } | |
| .viewport img { width: 100%; height: 100%; object-fit: cover; display: none; } | |
| /* The Scanning Laser Loading Animation */ | |
| .scanner { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--accent); box-shadow: 0 0 15px var(--accent); animation: scan 2s linear infinite; display: none; } | |
| @keyframes scan { 0% { top: 0; opacity: 0;} 10% { opacity: 1; } 90% { opacity: 1; } 100% { top: 100%; opacity: 0;} } | |
| .status-text { color: #555; font-size: 14px; letter-spacing: 2px; } | |
| .controls { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; margin-top: 10px; border-top: 1px solid #222; padding-top: 20px; } | |
| .prompt-readout { flex: 1; background: #000; padding: 12px; font-size: 11px; color: #666; border-radius: 4px; border: 1px solid #222; margin-right: 20px; line-height: 1.5; } | |
| .btn-synth { background: transparent; border: 1px solid var(--accent); color: var(--accent); padding: 15px 30px; font-size: 16px; font-weight: bold; letter-spacing: 2px; cursor: pointer; border-radius: 4px; transition: all 0.3s; text-transform: uppercase; } | |
| .btn-synth:hover { background: var(--glow); box-shadow: 0 0 20px var(--glow); color: #fff; } | |
| .btn-synth:disabled { border-color: #444; color: #444; cursor: not-allowed; box-shadow: none; background: transparent; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div> | |
| <h1>Chroma-Forge AI</h1> | |
| <p style="color: #666; font-size: 12px; margin-top: 5px;">Latent Material Texture Synthesizer v2.0</p> | |
| </div> | |
| <div class="api-config"> | |
| <span style="font-size: 11px; color: #888;">HF TOKEN:</span> | |
| <input type="password" id="apiKey" placeholder="hf_xxxxxxxxxxxxxxxxxxx" /> | |
| </div> | |
| </header> | |
| <div class="desk"> | |
| <!-- Left Fader Bank --> | |
| <div class="fader-bank"> | |
| <div class="slider-wrapper"> | |
| <div class="slider-label"> | |
| <span id="lbl-org">ORGANIC</span> | |
| <span id="lbl-syn">SYNTHETIC</span> | |
| </div> | |
| <input type="range" id="sl-nature" min="0" max="100" value="50"> | |
| </div> | |
| <div class="slider-wrapper"> | |
| <div class="slider-label"> | |
| <span id="lbl-smo">SMOOTH</span> | |
| <span id="lbl-jag">JAGGED</span> | |
| </div> | |
| <input type="range" id="sl-surface" min="0" max="100" value="50"> | |
| </div> | |
| </div> | |
| <!-- Center Viewport --> | |
| <div class="viewport" id="viewportBox"> | |
| <span class="status-text" id="statusText">AWAITING SYNTHESIS</span> | |
| <img id="materialOutput" alt="Generated Material" /> | |
| <div class="scanner" id="scanner"></div> | |
| </div> | |
| <!-- Right Fader Bank --> | |
| <div class="fader-bank"> | |
| <div class="slider-wrapper"> | |
| <div class="slider-label"> | |
| <span id="lbl-ari">ARID</span> | |
| <span id="lbl-wet">WET</span> | |
| </div> | |
| <input type="range" id="sl-moisture" min="0" max="100" value="50"> | |
| </div> | |
| <div class="slider-wrapper"> | |
| <div class="slider-label"> | |
| <span id="lbl-ord">ORDER</span> | |
| <span id="lbl-cha">CHAOS</span> | |
| </div> | |
| <input type="range" id="sl-structure" min="0" max="100" value="50"> | |
| </div> | |
| </div> | |
| <!-- Bottom Controls --> | |
| <div class="controls"> | |
| <div class="prompt-readout" id="promptDisplay">Raw compiler translation will appear here...</div> | |
| <button class="btn-synth" id="btnSynth">Synthesize</button> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const apiKeyInput = document.getElementById('apiKey'); | |
| const btnSynth = document.getElementById('btnSynth'); | |
| const promptDisplay = document.getElementById('promptDisplay'); | |
| const materialOutput = document.getElementById('materialOutput'); | |
| const scanner = document.getElementById('scanner'); | |
| const statusText = document.getElementById('statusText'); | |
| const viewportBox = document.getElementById('viewportBox'); | |
| // Sliders | |
| const sNature = document.getElementById('sl-nature'); | |
| const sSurface = document.getElementById('sl-surface'); | |
| const sMoist = document.getElementById('sl-moisture'); | |
| const sStruct = document.getElementById('sl-structure'); | |
| // Load API Key from local storage if exists | |
| if(localStorage.getItem('hf_token')) { | |
| apiKeyInput.value = localStorage.getItem('hf_token'); | |
| } | |
| apiKeyInput.addEventListener('change', () => { | |
| localStorage.setItem('hf_token', apiKeyInput.value.trim()); | |
| }); | |
| // The Alchemist Engine: Turns slider math into AI Prompts | |
| function buildPrompt() { | |
| let n = parseInt(sNature.value); | |
| let s = parseInt(sSurface.value); | |
| let m = parseInt(sMoist.value); | |
| let st = parseInt(sStruct.value); | |
| // Interpolate keywords based on threshold values | |
| let natureTerm = n < 40 ? "biological tissue, flesh, alien flora, organic matter" : | |
| n > 60 ? "metallic, synthetic, hard-surface, cybernetic plating" : | |
| "biomechanical hybrid, techno-organic material"; | |
| let surfaceTerm = s < 40 ? "smooth, highly polished, glossy, flowing" : | |
| s > 60 ? "jagged, sharp, rough, crystalline, shattered" : | |
| "textured surface with subtle ridges"; | |
| let moistTerm = m < 40 ? "dry, cracked, arid, dusty, matte" : | |
| m > 60 ? "wet, slimy, dripping, moist, specular highlights" : | |
| "damp surface"; | |
| let structTerm = st < 40 ? "geometric, grid repeating pattern, highly ordered" : | |
| st > 60 ? "chaotic, tangled, asymmetric, distorted, noisy" : | |
| "semi-structured pattern"; | |
| // Final Prompt Construction | |
| const finalPrompt = `A seamless texture map of ${natureTerm}, ${surfaceTerm}, ${moistTerm}, ${structTerm}. Overhead flat view, high resolution, Unreal Engine 5 render, PBR material, highly detailed procedural texture.`; | |
| promptDisplay.innerText = finalPrompt; | |
| return finalPrompt; | |
| } | |
| // Live update the prompt readout when dragging sliders | |
| [sNature, sSurface, sMoist, sStruct].forEach(slider => { | |
| slider.addEventListener('input', buildPrompt); | |
| }); | |
| // Initial setup | |
| buildPrompt(); | |
| // The AI Synthesis Function | |
| async function synthesize() { | |
| const token = apiKeyInput.value.trim(); | |
| if (!token) { | |
| alert("Please enter your Hugging Face API Token in the top right."); | |
| return; | |
| } | |
| const prompt = buildPrompt(); | |
| // UI State: Loading | |
| btnSynth.disabled = true; | |
| btnSynth.innerText = "PROCESSING..."; | |
| viewportBox.style.borderColor = "var(--accent)"; | |
| statusText.style.display = "block"; | |
| statusText.innerText = "CALLING LATENT INFERENCE..."; | |
| materialOutput.style.display = "none"; | |
| scanner.style.display = "block"; | |
| try { | |
| // Calling the lightning-fast FLUX.1 model on Hugging Face API | |
| const response = await fetch( | |
| "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell", | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| "Content-Type": "application/json", | |
| }, | |
| method: "POST", | |
| body: JSON.stringify({ inputs: prompt }), | |
| } | |
| ); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.status} - Check if your token is valid.`); | |
| } | |
| const blob = await response.blob(); | |
| const imageUrl = URL.createObjectURL(blob); | |
| // UI State: Success | |
| materialOutput.src = imageUrl; | |
| materialOutput.style.display = "block"; | |
| statusText.style.display = "none"; | |
| } catch (error) { | |
| statusText.innerText = "SYNTHESIS FAILED"; | |
| alert(error.message); | |
| } finally { | |
| // UI State: Reset | |
| scanner.style.display = "none"; | |
| viewportBox.style.borderColor = "#333"; | |
| btnSynth.disabled = false; | |
| btnSynth.innerText = "SYNTHESIZE"; | |
| } | |
| } | |
| btnSynth.addEventListener('click', synthesize); | |
| </script> | |
| </body> | |
| </html> |