Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Cinematic Color Calibration & Light FX</title> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary-color: #00f2ff; | |
| --bg-color: #0a0a0a; | |
| --panel-bg: rgba(15, 15, 20, 0.75); | |
| --text-color: #e0e0e0; | |
| --accent-glow: 0 0 10px rgba(0, 242, 255, 0.5); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background-color: #000; | |
| overflow: hidden; | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| color: var(--text-color); | |
| } | |
| /* Canvas */ | |
| #stage { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| /* Header */ | |
| header { | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 100; | |
| pointer-events: none; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| } | |
| header a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| font-weight: bold; | |
| text-shadow: 0 0 5px var(--primary-color); | |
| pointer-events: auto; | |
| transition: all 0.3s ease; | |
| } | |
| header a:hover { | |
| text-shadow: 0 0 15px var(--primary-color); | |
| letter-spacing: 3px; | |
| } | |
| /* Control Panel */ | |
| .controls-container { | |
| position: fixed; | |
| right: 20px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 300px; | |
| background: var(--panel-bg); | |
| backdrop-filter: blur(15px); | |
| -webkit-backdrop-filter: blur(15px); | |
| padding: 25px; | |
| border-radius: 12px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); | |
| z-index: 100; | |
| transition: opacity 0.3s; | |
| } | |
| .controls-container:hover { | |
| opacity: 1; | |
| } | |
| h2 { | |
| font-size: 1.1rem; | |
| margin-bottom: 20px; | |
| color: var(--primary-color); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| padding-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .control-group { | |
| margin-bottom: 18px; | |
| } | |
| .label-row { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 8px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| } | |
| .value-display { | |
| color: var(--primary-color); | |
| font-family: 'Courier New', monospace; | |
| } | |
| /* Custom Range Slider */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| } | |
| input[type=range]:focus { | |
| outline: none; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 2px; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| height: 16px; | |
| width: 16px; | |
| border-radius: 50%; | |
| background: #fff; | |
| cursor: pointer; | |
| -webkit-appearance: none; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
| transition: transform 0.1s; | |
| } | |
| input[type=range]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| background: var(--primary-color); | |
| } | |
| /* Specific Styles for Firefox */ | |
| input[type=range]::-moz-range-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 2px; | |
| } | |
| input[type=range]::-moz-range-thumb { | |
| height: 16px; | |
| width: 16px; | |
| border: none; | |
| border-radius: 50%; | |
| background: #fff; | |
| cursor: pointer; | |
| box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
| } | |
| /* Section Toggles */ | |
| .section-title { | |
| font-size: 0.7rem; | |
| text-transform: uppercase; | |
| color: #666; | |
| margin-top: 20px; | |
| margin-bottom: 10px; | |
| letter-spacing: 1.5px; | |
| } | |
| .btn-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .toggle-btn { | |
| flex: 1; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| color: #aaa; | |
| padding: 8px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| transition: all 0.2s; | |
| } | |
| .toggle-btn.active { | |
| background: rgba(0, 242, 255, 0.1); | |
| border-color: var(--primary-color); | |
| color: var(--primary-color); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .controls-container { | |
| width: calc(100% - 40px); | |
| top: auto; | |
| bottom: 20px; | |
| transform: none; | |
| right: 20px; | |
| left: 20px; | |
| max-height: 40vh; | |
| overflow-y: auto; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="stage"></canvas> | |
| <header> | |
| <div>Video FX Engine</div> | |
| <div style="margin-top: 5px; font-size: 0.7rem;">Interactive Light & Particle Simulation</div> | |
| <div style="margin-top: 15px;">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a></div> | |
| </header> | |
| <div class="controls-container"> | |
| <h2><i class="fas fa-sliders-h"></i> Calibration Deck</h2> | |
| <div class="section-title">Color Grading</div> | |
| <div class="control-group"> | |
| <div class="label-row"> | |
| <span>Exposure (Dark)</span> | |
| <span id="val-exposure" class="value-display">100%</span> | |
| </div> | |
| <input type="range" id="exposure" min="0" max="200" value="100"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="label-row"> | |
| <span>Contrast</span> | |
| <span id="val-contrast" class="value-display">100%</span> | |
| </div> | |
| <input type="range" id="contrast" min="0" max="200" value="100"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="label-row"> | |
| <span>Saturation</span> | |
| <span id="val-saturation" class="value-display">100%</span> | |
| </div> | |
| <input type="range" id="saturation" min="0" max="200" value="100"> | |
| </div> | |
| <div class="section-title">Atmosphere & FX</div> | |
| <div class="control-group"> | |
| <div class="label-row"> | |
| <span>Light Intensity (Max)</span> | |
| <span id="val-intensity" class="value-display">50%</span> | |
| </div> | |
| <input type="range" id="intensity" min="0" max="100" value="50"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="label-row"> | |
| <span>Particles (Dust)</span> | |
| <span id="val-particles" class="value-display">500</span> | |
| </div> | |
| <input type="range" id="particles" min="0" max="1500" value="500"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="label-row"> | |
| <span>Ray Length</span> | |
| <span id="val-rays" class="value-display">Auto</span> | |
| </div> | |
| <input type="range" id="rays" min="0" max="100" value="60"> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="toggle-btn active" id="btn-dust">Dust</button> | |
| <button class="toggle-btn active" id="btn-rays">Light Rays</button> | |
| <button class="toggle-btn" id="btn-animate">Auto Rotate</button> | |
| </div> | |
| <div style="font-size: 0.7rem; color: #666; margin-top: 10px; text-align: center;"> | |
| Move mouse to control light source | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * Cinematic Video Calibration & Particle Engine | |
| * Simulates color grading, volumetric lighting (raytracing fake), | |
| * and floating dust particles. | |
| */ | |
| const canvas = document.getElementById('stage'); | |
| const ctx = canvas.getContext('2d'); | |
| // State Management | |
| const state = { | |
| width: window.innerWidth, | |
| height: window.innerHeight, | |
| mouseX: window.innerWidth / 2, | |
| mouseY: window.innerHeight / 3, | |
| // Grading | |
| exposure: 100, | |
| contrast: 100, | |
| saturation: 100, | |
| // FX | |
| intensity: 0.5, | |
| rayLength: 0.6, | |
| particleCount: 500, | |
| // Toggles | |
| showDust: true, | |
| showRays: true, | |
| autoRotate: false, | |
| time: 0 | |
| }; | |
| // Resize Handler | |
| function resize() { | |
| state.width = window.innerWidth; | |
| state.height = window.innerHeight; | |
| canvas.width = state.width; | |
| canvas.height = state.height; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| // Mouse Tracker | |
| window.addEventListener('mousemove', (e) => { | |
| state.mouseX = e.clientX; | |
| state.mouseY = e.clientY; | |
| }); | |
| // Touch Tracker | |
| window.addEventListener('touchmove', (e) => { | |
| state.mouseX = e.touches[0].clientX; | |
| state.mouseY = e.touches[0].clientY; | |
| }); | |
| // --- Particle System --- | |
| class Particle { | |
| constructor() { | |
| this.reset(true); | |
| } | |
| reset(randomY = false) { | |
| this.x = Math.random() * state.width; | |
| this.y = randomY ? Math.random() * state.height : state.height + 10; | |
| this.z = Math.random() * 2 + 0.5; // Depth simulation | |
| this.size = Math.random() * 2; | |
| this.speedY = (Math.random() * 0.5 + 0.2) * this.z; | |
| this.speedX = (Math.random() - 0.5) * 0.2; | |
| this.opacity = Math.random() * 0.5 + 0.1; | |
| } | |
| update() { | |
| this.y -= this.speedY; | |
| this.x += Math.sin(state.time * 0.05 + this.y * 0.01) * 0.2; // Wavy motion | |
| // Mouse repulsion/attraction | |
| const dx = this.x - state.mouseX; | |
| const dy = this.y - state.mouseY; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| if (dist < 200) { | |
| this.x += dx * 0.01; | |
| this.y += dy * 0.01; | |
| } | |
| if (this.y < -10) { | |
| this.reset(); | |
| } | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size * this.z, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(220, 220, 200, ${this.opacity * state.intensity})`; | |
| ctx.fill(); | |
| } | |
| } | |
| let particles = []; | |
| function initParticles() { | |
| particles = []; | |
| for (let i = 0; i < 2000; i++) { // Max pool | |
| particles.push(new Particle()); | |
| } | |
| } | |
| initParticles(); | |
| // --- Volumetric Light Rays (God Rays) --- | |
| function drawLightRays() { | |
| if (!state.showRays) return; | |
| const cx = state.mouseX; | |
| const cy = state.mouseY; | |
| const rays = 12; | |
| const maxRadius = Math.max(state.width, state.height) * state.rayLength * 1.5; | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'screen'; // Additive blending for light | |
| // Base glow | |
| const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxRadius); | |
| gradient.addColorStop(0, `rgba(255, 255, 240, ${state.intensity * 0.8})`); | |
| gradient.addColorStop(0.1, `rgba(200, 220, 255, ${state.intensity * 0.3})`); | |
| gradient.addColorStop(0.5, `rgba(100, 150, 255, ${state.intensity * 0.1})`); | |
| gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(0, 0, state.width, state.height); | |
| // Rotating Shafts | |
| if (state.autoRotate) state.time += 0.002; | |
| for (let i = 0; i < rays; i++) { | |
| const angle = (Math.PI * 2 / rays) * i + state.time; | |
| ctx.translate(cx, cy); | |
| ctx.rotate(angle); | |
| // Draw Ray | |
| ctx.beginPath(); | |
| ctx.moveTo(0, 0); | |
| // Widen the ray as it goes out | |
| const widthSpread = 150 + Math.sin(i + state.time * 5) * 50; | |
| ctx.lineTo(maxRadius, -widthSpread / 2); | |
| ctx.lineTo(maxRadius, widthSpread / 2); | |
| ctx.closePath(); | |
| // Gradient for ray | |
| const rayGrad = ctx.createLinearGradient(0, 0, maxRadius, 0); | |
| rayGrad.addColorStop(0, `rgba(255, 255, 255, ${state.intensity * 0.4})`); | |
| rayGrad.addColorStop(0.2, `rgba(200, 220, 255, ${state.intensity * 0.1})`); | |
| rayGrad.addColorStop(1, 'rgba(0, 0, 0, 0)'); | |
| ctx.fillStyle = rayGrad; | |
| ctx.fill(); | |
| // Reset matrix | |
| ctx.rotate(-angle); | |
| ctx.translate(-cx, -cy); | |
| } | |
| ctx.restore(); | |
| } | |
| // --- Main Render Loop --- | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| state.time += 1; | |
| // 1. Clear and apply Grading Filters to the WHOLE context | |
| // We use CSS filters on the context for "Global" grading | |
| const brightness = state.exposure / 100; | |
| const contrast = state.contrast / 100; | |
| const saturate = state.saturation / 100; | |
| // Note: Canvas API filter support is good in modern browsers | |
| ctx.filter = `brightness(${brightness}) contrast(${contrast}) saturate(${saturate})`; | |
| // Fill Background (Dark Room) | |
| // Create a very subtle vignette | |
| const bgGradient = ctx.createRadialGradient(state.width/2, state.height/2, 100, state.width/2, state.height/2, state.height); | |
| bgGradient.addColorStop(0, '#1a1a20'); // Dark Blue-Grey | |
| bgGradient.addColorStop(1, '#050505'); // Almost Black | |
| ctx.fillStyle = bgGradient; | |
| ctx.fillRect(0, 0, state.width, state.height); | |
| // 2. Draw Volumetric Light (Behind dust) | |
| // We use 'source-over' normally, but for rays we might want screen. | |
| // However, applying the filter above makes everything graded. | |
| // To make rays look "colored" before grading, we draw them, then apply filter? | |
| // Actually, ctx.filter applies to drawing operations. | |
| // But if we want the grading to affect everything uniformly, we keep it. | |
| // However, let's draw Rays without the filter first to keep them "hot", | |
| // then draw background/dust, then apply filter? | |
| // For simplicity and performance in this single-file demo, we apply grading to everything. | |
| drawLightRays(); | |
| // 3. Draw Light Source Body (The "Sun" or Bulb) | |
| ctx.filter = 'none'; // Turn off filter for the source core to make it pop | |
| const lx = state.mouseX; | |
| const ly = state.mouseY; | |
| const lightGrad = ctx.createRadialGradient(lx, ly, 0, lx, ly, 60 * state.intensity + 20); | |
| lightGrad.addColorStop(0, 'rgba(255, 255, 255, 1)'); | |
| lightGrad.addColorStop(0.2, 'rgba(240, 245, 255, 0.8)'); | |
| lightGrad.addColorStop(1, 'rgba(0, 0, 0, 0)'); | |
| ctx.fillStyle = lightGrad; | |
| ctx.beginPath(); | |
| ctx.arc(lx, ly, 60, 0, Math.PI*2); | |
| ctx.fill(); | |
| // 4. Draw Particles (Dust) | |
| // Particles are affected by the global filter if we don't reset, | |
| // which helps integrate them into the "scene". | |
| // Let's re-enable filter for dust to tint them based on grading | |
| ctx.filter = `brightness(${brightness}) contrast(${contrast}) saturate(${saturate})`; | |
| if (state.showDust) { | |
| for (let i = 0; i < state.particleCount; i++) { | |
| if (particles[i]) { | |
| particles[i].update(); | |
| particles[i].draw(); | |
| } | |
| } | |
| } | |
| // 5. Post-Processing Overlay (Vignette & Noise) | |
| // We apply this ON TOP of everything using a temporary canvas or just drawing over | |
| ctx.filter = 'none'; | |
| // Draw a vignette overlay to darken corners | |
| const vigGrad = ctx.createRadialGradient(state.width/2, state.height/2, state.height/3, state.width/2, state.height/2, state.height); | |
| vigGrad.addColorStop(0, 'rgba(0,0,0,0)'); | |
| vigGrad.addColorStop(1, 'rgba(0,0,0,0.6)'); | |
| ctx.fillStyle = vigGrad; | |
| ctx.fillRect(0, 0, state.width, state.height); | |
| // Scanlines / Grid effect (Subtle) | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; | |
| for(let y = 0; y < state.height; y+=4) { | |
| ctx.fillRect(0, y, state.width, 1); | |
| } | |
| } | |
| // --- UI Logic --- | |
| const bindInput = (id, key, suffix = '%') => { | |
| const input = document.getElementById(id); | |
| const display = document.getElementById(`val-${id}`); | |
| input.addEventListener('input', (e) => { | |
| state[key] = parseFloat(e.target.value); | |
| display.innerText = state[key] + suffix; | |
| }); | |
| }; | |
| bindInput('exposure', 'exposure'); | |
| bindInput('contrast', 'contrast'); | |
| bindInput('saturation', 'saturation'); | |
| bindInput('intensity', 'intensity', ''); | |
| bindInput('particles', 'particleCount'); | |
| bindInput('rays', 'rayLength'); | |
| // Toggles | |
| document.getElementById('btn-dust').addEventListener('click', (e) => { | |
| state.showDust = !state.showDust; | |
| e.target.classList.toggle('active'); | |
| }); | |
| document.getElementById('btn-rays').addEventListener('click', (e) => { | |
| state.showRays = !state.showRays; | |
| e.target.classList.toggle('active'); | |
| }); | |
| document.getElementById('btn-animate').addEventListener('click', (e) => { | |
| state.autoRotate = !state.autoRotate; | |
| e.target.classList.toggle('active'); | |
| }); | |
| // Start | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |