Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Cinematic Light & FX Engine</title> | |
| <style> | |
| :root { | |
| --bg-color: #050505; | |
| --panel-bg: rgba(20, 20, 25, 0.85); | |
| --accent: #00f2ff; | |
| --accent-secondary: #ff0055; | |
| --text-main: #e0e0e0; | |
| --font-tech: 'Segoe UI', 'Roboto', monospace; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| user-select: none; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background-color: var(--bg-color); | |
| color: var(--text-main); | |
| font-family: var(--font-tech); | |
| overflow: hidden; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 10px 20px; | |
| display: flex; | |
| justify; | |
| align-items-content: space-between: center; | |
| background: linear-gradient(90deg, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0) 100%); | |
| z-index: 100; | |
| border-bottom: 1px solid #333; | |
| } | |
| .brand { | |
| font-weight: 900; | |
| font-size: 1.2rem; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| background: linear-gradient(45deg, var(--accent), #fff); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .coder-link { | |
| font-size: 0.8rem; | |
| color: #888; | |
| text-decoration: none; | |
| border: 1px solid #444; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| } | |
| .coder-link:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| /* Main Layout */ | |
| .container { | |
| display: flex; | |
| flex: 1; | |
| height: calc(100vh - 50px); | |
| } | |
| /* Canvas Area */ | |
| .viewport { | |
| flex: 1; | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: radial-gradient(circle at center, #1a1a1a 0%, #000 100%); | |
| overflow: hidden; | |
| } | |
| canvas { | |
| box-shadow: 0 0 50px rgba(0,0,0,0.8); | |
| border: 1px solid #333; | |
| max-width: 100%; | |
| max-height: 100%; | |
| } | |
| /* HUD Overlay */ | |
| .hud { | |
| position: absolute; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| pointer-events: none; | |
| background: | |
| linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), | |
| linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); | |
| background-size: 100% 2px, 3px 100%; | |
| z-index: 10; | |
| } | |
| .fps-counter { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| font-family: monospace; | |
| color: var(--accent); | |
| font-size: 1.5rem; | |
| text-shadow: 0 0 10px var(--accent); | |
| z-index: 20; | |
| } | |
| /* Controls Sidebar */ | |
| .controls { | |
| width: 320px; | |
| background: var(--panel-bg); | |
| backdrop-filter: blur(10px); | |
| border-left: 1px solid #333; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| overflow-y: auto; | |
| box-shadow: -5px 0 15px rgba(0,0,0,0.5); | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #333; | |
| } | |
| .control-group:last-child { | |
| border-bottom: none; | |
| } | |
| label { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: #888; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .value-display { | |
| color: var(--accent); | |
| font-weight: bold; | |
| } | |
| /* Custom Range Sliders */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 16px; | |
| width: 16px; | |
| border-radius: 50%; | |
| background: var(--text-main); | |
| cursor: pointer; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px rgba(255,255,255,0.5); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: #444; | |
| border-radius: 2px; | |
| } | |
| /* Specific Slider Colors */ | |
| #fpsInput::-webkit-slider-thumb { background: var(--accent); } | |
| #satInput::-webkit-slider-thumb { background: #ff00aa; } | |
| #lightInput::-webkit-slider-thumb { background: #ffaa00; } | |
| #particleInput::-webkit-slider-thumb { background: #aaff00; } | |
| .section-title { | |
| font-size: 0.9rem; | |
| color: #fff; | |
| font-weight: 600; | |
| margin-bottom: 5px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .section-title::before { | |
| content: ''; | |
| display: block; | |
| width: 4px; | |
| height: 16px; | |
| background: var(--accent-secondary); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .container { | |
| flex-direction: column; | |
| } | |
| .controls { | |
| width: 100%; | |
| height: 40%; | |
| border-left: none; | |
| border-top: 1px solid #333; | |
| } | |
| .viewport { | |
| height: 60%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand">LUMINA ENGINE <span style="font-size:0.5em; color:#666">v1.0</span></div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="coder-link" target="_blank">Built with anycoder</a> | |
| </header> | |
| <div class="container"> | |
| <div class="viewport"> | |
| <div class="hud"></div> | |
| <div class="fps-counter">FPS: <span id="fpsVal">60</span></div> | |
| <canvas id="mainCanvas"></canvas> | |
| </div> | |
| <div class="controls"> | |
| <div class="section-title">Core Settings</div> | |
| <div class="control-group"> | |
| <label>Target FPS <span class="value-display" id="fpsVal2">60</span></label> | |
| <input type="range" id="fpsInput" min="10" max="144" value="60"> | |
| </div> | |
| <div class="section-title">Color Calibration</div> | |
| <div class="control-group"> | |
| <label>Saturation <span class="value-display" id="satVal">120%</span></label> | |
| <input type="range" id="satInput" min="0" max="300" value="120"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Brightness (Exp) <span class="value-display" id="brightVal">1.0</span></label> | |
| <input type="range" id="brightInput" min="50" max="200" value="100"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Contrast <span class="value-display" id="contVal">100%</span></label> | |
| <input type="range" id="contInput" min="0" max="200" value="100"> | |
| </div> | |
| <div class="section-title">Lighting & FX</div> | |
| <div class="control-group"> | |
| <label>Light Intensity <span class="value-display" id="lightVal">50</span></label> | |
| <input type="range" id="lightInput" min="0" max="100" value="50"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Ray Length <span class="value-display" id="rayVal">50</span></label> | |
| <input type="range" id="rayInput" min="0" max="100" value="50"> | |
| </div> | |
| <div class="section-title">Particles</div> | |
| <div class="control-group"> | |
| <label>Dust Density <span class="value-display" id="partVal">100</span></label> | |
| <input type="range" id="particleInput" min="0" max="500" value="100"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Particle Speed <span class="value-display" id="speedVal">1.0</span></label> | |
| <input type="range" id="speedInput" min="0" max="500" value="100"> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * SYSTEM CORE | |
| * Handles Canvas setup, Resize logic, and FPS throttling. | |
| */ | |
| const canvas = document.getElementById('mainCanvas'); | |
| const ctx = canvas.getContext('2d', { alpha: false }); // Optimize for no transparency on canvas bg | |
| let width, height; | |
| function resize() { | |
| // Maintain 16:9 aspect ratio within the viewport, but fit within container | |
| const container = document.querySelector('.viewport'); | |
| const aspect = 16 / 9; | |
| let w = container.clientWidth; | |
| let h = container.clientHeight; | |
| if (w / h > aspect) { | |
| w = h * aspect; | |
| } else { | |
| h = w / aspect; | |
| } | |
| canvas.width = 1920; // Internal render resolution (4k ready) | |
| canvas.height = 1080; | |
| // Scale via CSS | |
| canvas.style.width = `${w}px`; | |
| canvas.style.height = `${h}px`; | |
| // Scale Context | |
| const scaleX = canvas.width / w; | |
| const scaleY = canvas.height / h; | |
| // We actually render to full 1920x1080 coordinates for sharpness, | |
| // then CSS scales it down. | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| /** | |
| * STATE MANAGEMENT | |
| */ | |
| const settings = { | |
| fps: 60, | |
| saturation: 120, | |
| brightness: 100, // CSS filter uses 0-300+ usually, but standard is 100% | |
| contrast: 100, | |
| lightIntensity: 0.5, | |
| rayLength: 0.5, | |
| particleCount: 100, | |
| particleSpeed: 1.0 | |
| }; | |
| // Input Event Listeners | |
| function bindInput(id, key, suffix = '') { | |
| const input = document.getElementById(id); | |
| const display = document.getElementById(id.replace('Input', 'Val') + (id.includes('Input') ? '' : 'Val')); | |
| // Note: Simplified ID mapping logic for brevity | |
| input.addEventListener('input', (e) => { | |
| settings[key] = parseFloat(e.target.value); | |
| // Update display text if exists | |
| const disp = document.getElementById(key + 'Val') || document.getElementById(id.replace('Input', 'Val')); | |
| if(disp) disp.innerText = settings[key] + suffix; | |
| }); | |
| } | |
| // Map inputs specifically | |
| document.getElementById('fpsInput').addEventListener('input', (e) => { | |
| settings.fps = parseFloat(e.target.value); | |
| document.getElementById('fpsVal').innerText = settings.fps; | |
| document.getElementById('fpsVal2').innerText = settings.fps; | |
| }); | |
| ['satInput', 'brightInput', 'contInput'].forEach(id => { | |
| document.getElementById(id).addEventListener('input', (e) => { | |
| const key = id.replace('Input', ''); // sat, bright, cont | |
| const map = { satInput: 'saturation', brightInput: 'brightness', contInput: 'contrast' }; | |
| settings[map[id]] = parseFloat(e.target.value); | |
| document.getElementById(key + 'Val').innerText = settings[map[id]] + '%'; | |
| }); | |
| }); | |
| ['lightInput', 'rayInput'].forEach(id => { | |
| document.getElementById(id).addEventListener('input', (e) => { | |
| const key = id.replace('Input', ''); | |
| const map = { lightInput: 'lightIntensity', rayInput: 'rayLength' }; | |
| settings[map[id]] = parseFloat(e.target.value) / 100; | |
| document.getElementById(key + 'Val').innerText = e.target.value; | |
| }); | |
| }); | |
| document.getElementById('particleInput').addEventListener('input', (e) => { | |
| settings.particleCount = parseInt(e.target.value); | |
| document.getElementById('partVal').innerText = settings.particleCount; | |
| initParticles(); // Re-init on count change | |
| }); | |
| document.getElementById('speedInput').addEventListener('input', (e) => { | |
| settings.particleSpeed = parseFloat(e.target.value) / 100; | |
| document.getElementById('speedVal').innerText = settings.particleSpeed.toFixed(2); | |
| }); | |
| /** | |
| * SCENE OBJECTS | |
| */ | |
| let particles = []; | |
| class Particle { | |
| constructor() { | |
| this.reset(true); | |
| } | |
| reset(randomX = false) { | |
| this.x = randomX ? Math.random() * canvas.width : canvas.width + 50; | |
| this.y = Math.random() * canvas.height; | |
| this.z = Math.random() * 2 + 0.5; // Depth factor | |
| this.size = Math.random() * 2; | |
| this.speedX = (Math.random() * 5 + 2) * settings.particleSpeed; | |
| this.opacity = Math.random() * 0.5 + 0.1; | |
| } | |
| update() { | |
| // Physics: Move left | |
| this.x -= this.speedX * (this.z * 0.5); // Parallax effect based on depth | |
| // Volumetric Light Influence (Simulated) | |
| // If particle is within the "light cone" (center screen), it glows | |
| const centerX = canvas.width / 2; | |
| const distToCenter = Math.abs(this.x - centerX); | |
| // Reset if off screen | |
| if (this.x < -50) { | |
| this.reset(); | |
| } | |
| } | |
| draw(ctx) { | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| // Distance from the "Light Source" | |
| const dx = this.x - centerX; | |
| const dy = this.y - centerY; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| // Volumetric Ray Logic: | |
| // Determine if particle is "caught" in the light rays | |
| // We simulate rays as angles emanating from center | |
| const angle = Math.atan2(dy, dx); | |
| // A "Light Cone" moving back and forth | |
| const time = Date.now() * 0.001; | |
| const lightAngle = Math.sin(time) * 0.5; // Oscillate light | |
| // Simple ray check: Is particle close to the light angle and within range? | |
| let inLight = false; | |
| if (dist < canvas.height * settings.rayLength) { | |
| // Normalize angle -PI to PI | |
| let normAngle = angle; | |
| if(normAngle < 0) normAngle += Math.PI * 2; | |
| // We fake the raycasting by checking if it aligns with the oscillating light | |
| // This is computationally cheaper than real raytracing for 500 particles | |
| const lightDir = Math.PI / 2; // Light coming from right usually in this tunnel | |
| const deviation = Math.abs(Math.sin(angle - lightDir)); | |
| if (deviation < 0.1) { // Narrow beam | |
| inLight = true; | |
| } | |
| } | |
| // Render | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size * this.z, 0, Math.PI * 2); | |
| // Color logic | |
| if (inLight && settings.lightIntensity > 0.1) { | |
| // Caught in ray: Bright, colored | |
| ctx.fillStyle = `rgba(255, 255, 200, ${this.opacity})`; | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = "white"; | |
| } else { | |
| // Ambient dust | |
| ctx.fillStyle = `rgba(100, 120, 150, ${this.opacity * 0.5})`; | |
| ctx.shadowBlur = 0; | |
| } | |
| ctx.fill(); | |
| } | |
| } | |
| function initParticles() { | |
| particles = []; | |
| for(let i=0; i<settings.particleCount; i++) { | |
| particles.push(new Particle()); | |
| } | |
| } | |
| /** | |
| * MAIN RENDER LOOP | |
| */ | |
| let then = Date.now(); | |
| let fpsInterval = 1000 / settings.fps; | |
| let frames = 0; | |
| let lastFpsTime = 0; | |
| function drawTunnel() { | |
| // 1. Background - Dark Gradient | |
| const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); | |
| grad.addColorStop(0, '#050505'); | |
| grad.addColorStop(1, '#101015'); | |
| ctx.fillStyle = grad; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // 2. Grid Lines (Retro Sci-Fi Floor) | |
| ctx.strokeStyle = `rgba(0, 242, 255, ${0.1 * settings.lightIntensity})`; | |
| ctx.lineWidth = 2; | |
| const time = Date.now() * 0.001; | |
| const perspective = canvas.height / 2; | |
| const horizon = canvas.height * 0.4; | |
| // Vertical Lines (Converging to center) | |
| ctx.beginPath(); | |
| for(let i = -10; i <= 10; i++) { | |
| const x = (canvas.width / 2) + (i * 150); | |
| // Simple perspective math | |
| ctx.moveTo(x, horizon); | |
| ctx.lineTo(x * 3 - (canvas.width*1.5), canvas.height); | |
| } | |
| ctx.stroke(); | |
| // Horizontal Lines (Moving towards camera) | |
| const speed = (time * 100) % 100; | |
| for(let i=0; i<20; i++) { | |
| const y = horizon + Math.pow(i/2, 2.5) * 10 + (i < 2 ? speed : -speed); | |
| if (y > horizon && y < canvas.height) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(canvas.width, y); | |
| ctx.stroke(); | |
| } | |
| } | |
| // 3. Central Light Source (The "Sun" / Core) | |
| const lightX = canvas.width / 2; | |
| const lightY = horizon - 50; | |
| // Bloom effect | |
| ctx.globalCompositeOperation = 'lighter'; | |
| const rayGrad = ctx.createRadialGradient(lightX, lightY, 10, lightX, lightY, 400 * settings.lightIntensity + 100); | |
| rayGrad.addColorStop(0, `rgba(255, 255, 255, 1)`); | |
| rayGrad.addColorStop(0.1, `rgba(0, 242, 255, 0.8)`); | |
| rayGrad.addColorStop(1, `rgba(0, 0, 0, 0)`); | |
| ctx.fillStyle = rayGrad; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.globalCompositeOperation = 'source-over'; | |
| } | |
| function render() { | |
| requestAnimationFrame(render); | |
| const now = Date.now(); | |
| const elapsed = now - then; | |
| // FPS Throttling | |
| if (elapsed > fpsInterval) { | |
| // Adjust for next frame (handle drift) | |
| then = now - (elapsed % fpsInterval); | |
| // --- PROCESSING START --- | |
| // Clear | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // 1. Draw Base Scene (The "Video") | |
| drawTunnel(); | |
| // 2. Update & Draw Particles (Behind light core) | |
| // Sort particles by Z to fake depth sorting isn't strictly necessary for dust, | |
| // but good practice. | |
| particles.forEach(p => { | |
| p.update(); | |
| p.draw(ctx); | |
| }); | |
| // 3. Apply Post-Processing Filters (Simulating Color Calibration) | |
| // This affects the entire frame buffer | |
| const f_saturate = settings.saturation; | |
| const f_brightness = settings.brightness; | |
| const f_contrast = settings.contrast; | |
| ctx.filter = `saturate(${f_saturate}%) brightness(${f_brightness}%) contrast(${f_contrast}%)`; | |
| // We redraw a transparent rect to apply the filter to the existing canvas content | |
| // Note: standard canvas filters apply to drawings, but applying to the *result* | |
| // requires drawing the canvas onto itself or using the filter on specific layers. | |
| // Since we want to affect the "Video Feed", we simply apply ctx.filter | |
| // before drawing the particles, BUT for full post-processing: | |
| // We actually need to redraw the scene with the filter active, or use a CSS filter on the canvas element. | |
| // Let's use the ctx.filter property which is supported in modern browsers for subsequent draws. | |
| // Actually, to affect the "Base Layer" (Tunnel), we should set filter before drawing it. | |
| // But we want particles to react differently? | |
| // Let's keep it simple: Apply filter to everything. | |
| ctx.filter = `saturate(${f_saturate}%) contrast(${f_contrast}%)`; | |
| // Re-draw base scene with filter (expensive but accurate for "Calibration") | |
| // Optimization: In a real engine, we'd use shaders. Here, we just rely on the filter being active | |
| // for the final composition if we used layers. | |
| // Given the JS limitations, we will rely on CSS filter on the <canvas> element | |
| // for the "Brightness" (exposure) and "Contrast", while keeping internal colors rich. | |
| // Let's do the "Video Color Calibration" math manually on the particle colors | |
| // and rely on CSS for global exposure to save performance. | |
| // --- PROCESSING END --- | |
| } | |
| } | |
| // Start | |
| initParticles(); | |
| render(); | |
| // Reactive CSS Filters for Performance | |
| // We hook into the loop to update CSS variables or styles | |
| setInterval(() => { | |
| // Update CSS filter for brightness (Exposure) dynamically | |
| // This mimics a hardware exposure adjustment | |
| canvas.style.filter = `brightness(${settings.brightness}%) contrast(${settings.contrast}%) saturate(${settings.saturation}%)`; | |
| }, 100); | |
| </script> | |
| </body> | |
| </html> |