Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>WebGL Shader Lab</title> <script src="https://cdn.tailwindcss.com"></script> <style> @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600&display=swap'); body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #e2e8f0; overflow: hidden; /* Prevent scrolling on mobile */ } canvas { display: block; width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; z-index: 1; } /* Overlay UI */ #ui-layer { position: absolute; z-index: 10; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; } .interactive { pointer-events: auto; } /* Code block styling */ pre, textarea { font-family: 'JetBrains Mono', monospace; white-space: pre-wrap; word-wrap: break-word; } /* Custom scrollbar */ .custom-scroll::-webkit-scrollbar { width: 6px; height: 6px; } .custom-scroll::-webkit-scrollbar-track { background: #1e293b; } .custom-scroll::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; } /* Editor specific styles */ .editor-textarea { background-color: #0f172a; color: #a5f3fc; width: 100%; height: 100%; border: none; outline: none; resize: none; font-size: 13px; line-height: 1.5; } </style> </head> <body> <!-- Hidden video element for camera stream --> <video id="webcam" autoplay playsinline muted style="display: none;"></video> <!-- WebGL Canvas --> <canvas id="gl-canvas"></canvas> <!-- UI Layer --> <div id="ui-layer" class="p-4 safe-area-inset"> <!-- Header / Stats --> <div class="flex justify-between items-start interactive"> <div class="bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-slate-700 shadow-xl max-w-[200px] sm:max-w-md"> <h1 class="text-sm font-bold text-cyan-400 tracking-wider uppercase mb-1">WebGL Shader Lab</h1> <p id="fps-counter" class="text-xs text-slate-400 font-mono">FPS: 00 | GPU: Active</p> </div> <button id="toggle-info" class="bg-slate-800/90 text-white p-3 rounded-full hover:bg-slate-700 transition shadow-lg border border-slate-600 group"> <svg class="group-hover:text-cyan-400 transition-colors" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg> </button> </div> <!-- Educational Pane (Info) --> <div id="info-panel" class="absolute top-20 left-4 right-4 bottom-32 bg-slate-900/95 backdrop-blur-xl border border-slate-700 rounded-2xl p-0 shadow-2xl transform transition-transform duration-300 origin-top scale-y-0 opacity-0 pointer-events-none flex flex-col overflow-hidden z-20"> <div class="p-4 border-b border-slate-800 flex justify-between items-center bg-slate-950/50"> <h2 id="filter-title" class="font-bold text-lg text-white">Filter Name</h2> <button id="close-info" class="text-slate-400 hover:text-white interactive"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> </button> </div> <div class="p-4 overflow-y-auto custom-scroll flex-1 interactive"> <p id="filter-desc" class="text-sm text-slate-300 mb-4 leading-relaxed">Description goes here.</p> <div class="bg-black rounded-lg p-4 border border-slate-800 relative group"> <div class="absolute top-2 right-2 text-[10px] text-slate-500 font-mono uppercase">Fragment Shader Source</div> <pre><code id="filter-code" class="text-xs sm:text-sm text-green-400 language-glsl"></code></pre> </div> </div> </div> <!-- Custom Editor Modal --> <div id="editor-panel" class="absolute top-16 left-2 right-2 bottom-36 bg-slate-900/95 backdrop-blur-xl border border-cyan-700/50 rounded-2xl shadow-2xl transform transition-all duration-300 scale-95 opacity-0 pointer-events-none flex flex-col overflow-hidden z-30 hidden"> <div class="p-3 border-b border-slate-800 flex justify-between items-center bg-slate-950/80"> <div class="flex items-center gap-2"> <span class="text-cyan-400 font-bold text-sm">Custom Shader</span> <select id="boilerplate-select" class="interactive bg-slate-800 text-xs text-white border border-slate-700 rounded px-2 py-1 outline-none focus:border-cyan-500"> <option value="">-- Load Boilerplate --</option> <option value="basic">Basic (Pass-through)</option> <option value="invert">Invert Colors</option> <option value="wobble">Wobble Distortion</option> <option value="vignette">Vignette</option> </select> </div> <button id="close-editor" class="text-slate-400 hover:text-white interactive"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> </button> </div> <div class="flex-1 relative interactive"> <textarea id="custom-code" class="editor-textarea p-4 custom-scroll" spellcheck="false"></textarea> </div> <div class="p-3 border-t border-slate-800 bg-slate-950/80 flex justify-between items-center interactive"> <span id="compile-status" class="text-[10px] font-mono text-slate-400 truncate max-w-[60%]">Ready</span> <button id="apply-custom" class="bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-bold py-2 px-4 rounded transition shadow-lg shadow-cyan-900/20"> Apply Shader </button> </div> </div> <!-- Controls Bottom --> <div class="flex flex-col gap-3 interactive w-full max-w-2xl mx-auto"> <!-- Contextual Sliders (Dynamic) --> <div id="slider-container" class="bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-slate-700 hidden"> <div class="flex items-center justify-between mb-1"> <label id="slider-label" for="param-slider" class="text-xs font-semibold text-slate-300">Intensity</label> <span id="slider-val" class="text-xs font-mono text-cyan-400">1.0</span> </div> <input id="param-slider" type="range" min="0" max="100" value="50" class="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-cyan-500"> </div> <!-- Filter Selector Scroll --> <div class="flex overflow-x-auto gap-2 pb-2 hide-scrollbar snap-x"> <button class="filter-btn bg-cyan-600 text-white px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow-lg ring-2 ring-cyan-400" data-filter="0">Normal</button> <button class="filter-btn bg-slate-800 text-slate-300 px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow hover:bg-slate-700 transition" data-filter="1">Edge Detect</button> <button class="filter-btn bg-slate-800 text-slate-300 px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow hover:bg-slate-700 transition" data-filter="2">Pixelate</button> <button class="filter-btn bg-slate-800 text-slate-300 px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow hover:bg-slate-700 transition" data-filter="3">Chromatic</button> <button class="filter-btn bg-slate-800 text-slate-300 px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow hover:bg-slate-700 transition" data-filter="4">Scanline</button> <button class="filter-btn bg-slate-800 text-slate-300 px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow hover:bg-slate-700 transition" data-filter="5">Thermal</button> <button class="filter-btn bg-purple-900/80 text-purple-200 border border-purple-500/50 px-4 py-2 rounded-lg text-sm font-semibold whitespace-nowrap shadow hover:bg-purple-800 transition" id="btn-custom">✨ Custom</button> </div> </div> </div> <!-- Error Modal --> <div id="error-modal" class="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-6 hidden"> <div class="bg-slate-900 border border-red-500/50 p-6 rounded-2xl max-w-sm text-center"> <h3 class="text-xl font-bold text-white mb-2">Camera Access Required</h3> <p class="text-slate-400 text-sm mb-4">Please allow camera access to use the filters.</p> <button onclick="location.reload()" class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg font-bold transition">Try Again</button> </div> </div> <!-- SHADERS --> <script id="vertex-shader" type="x-shader/x-vertex"> attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; uniform vec2 u_textureScale; // Scale factor for Aspect Ratio correction void main() { gl_Position = vec4(a_position, 0, 1); // Standard UVs vec2 uv = vec2(a_texCoord.x, 1.0 - a_texCoord.y); // Apply scale relative to center (0.5, 0.5) to achieve "cover" fit // if u_textureScale is < 1.0, it zooms in (crops) v_texCoord = (uv - 0.5) * u_textureScale + 0.5; } </script> <!-- PRESET FRAGMENT SHADER (The monolithic one) --> <script id="fragment-shader-presets" type="x-shader/x-fragment"> precision mediump float; uniform sampler2D u_image; uniform vec2 u_resolution; uniform float u_time; uniform int u_filterType; uniform float u_param; varying vec2 v_texCoord; float luma(vec3 color) { return dot(color, vec3(0.299, 0.587, 0.114)); } // --- FILTERS --- vec3 sobel() { vec2 texel = 1.0 / u_resolution; float x = 0.0; float y = 0.0; // Gx x += luma(texture2D(u_image, v_texCoord + vec2(-1,-1)*texel).rgb)*-1.0; x += luma(texture2D(u_image, v_texCoord + vec2(-1, 0)*texel).rgb)*-2.0; x += luma(texture2D(u_image, v_texCoord + vec2(-1, 1)*texel).rgb)*-1.0; x += luma(texture2D(u_image, v_texCoord + vec2( 1,-1)*texel).rgb)* 1.0; x += luma(texture2D(u_image, v_texCoord + vec2( 1, 0)*texel).rgb)* 2.0; x += luma(texture2D(u_image, v_texCoord + vec2( 1, 1)*texel).rgb)* 1.0; // Gy y += luma(texture2D(u_image, v_texCoord + vec2(-1,-1)*texel).rgb)*-1.0; y += luma(texture2D(u_image, v_texCoord + vec2( 0,-1)*texel).rgb)*-2.0; y += luma(texture2D(u_image, v_texCoord + vec2( 1,-1)*texel).rgb)*-1.0; y += luma(texture2D(u_image, v_texCoord + vec2(-1, 1)*texel).rgb)* 1.0; y += luma(texture2D(u_image, v_texCoord + vec2( 0, 1)*texel).rgb)* 2.0; y += luma(texture2D(u_image, v_texCoord + vec2( 1, 1)*texel).rgb)* 1.0; float mag = sqrt(x*x + y*y); return (mag > u_param) ? vec3(mag) : vec3(0.0); } vec3 pixelate() { float pixels = 50.0 + (1.0 - u_param) * 300.0; vec2 uv = floor(v_texCoord * pixels) / pixels; return texture2D(u_image, uv).rgb; } vec3 chromatic() { float offset = u_param * 0.05; float r = texture2D(u_image, v_texCoord + vec2(offset, 0.0)).r; float g = texture2D(u_image, v_texCoord).g; float b = texture2D(u_image, v_texCoord - vec2(offset, 0.0)).b; return vec3(r, g, b); } vec3 scanline() { vec3 c = texture2D(u_image, v_texCoord).rgb; float scan = sin(v_texCoord.y * u_resolution.y * 0.5 * 3.14 + u_time * 10.0); c -= scan * u_param * 0.5; return c; } vec3 thermal() { vec3 c = texture2D(u_image, v_texCoord).rgb; float l = luma(c); vec3 c1=vec3(0,0,1); vec3 c2=vec3(1,1,0); vec3 c3=vec3(1,0,0); return (l < 0.5) ? mix(c1, c2, l*2.0) : mix(c2, c3, (l-0.5)*2.0); } void main() { vec3 color = texture2D(u_image, v_texCoord).rgb; if (u_filterType == 1) color = sobel(); else if (u_filterType == 2) color = pixelate(); else if (u_filterType == 3) color = chromatic(); else if (u_filterType == 4) color = scanline(); else if (u_filterType == 5) color = thermal(); gl_FragColor = vec4(color, 1.0); } </script> <script> // --- DATA --- const FILTER_DATA = [ { name: "Normal", desc: "Standard passthrough.", code: `gl_FragColor = texture2D(u_image, v_texCoord);`, hasParam: false }, { name: "Edge Detection", desc: "Sobel operator to find edges.", code: `// Sobel Algorithm...`, hasParam: true, paramName: "Threshold", paramMin: 0, paramMax: 100, paramDef: 10, paramMap: v => v/200.0 }, { name: "Pixelate", desc: "Lowers resolution.", code: `vec2 pixelUV = floor(uv * pixels) / pixels;`, hasParam: true, paramName: "Block Size", paramMin: 0, paramMax: 100, paramDef: 50, paramMap: v => v/100.0 }, { name: "Chromatic Aberration", desc: "RGB channel separation.", code: `r = tex(uv+off).r; b = tex(uv-off).b;`, hasParam: true, paramName: "Separation", paramMin: 0, paramMax: 100, paramDef: 50, paramMap: v => v/100.0 }, { name: "Scanlines", desc: "CRT TV effect.", code: `color -= sin(uv.y * lines) * intensity;`, hasParam: true, paramName: "Darkness", paramMin: 0, paramMax: 100, paramDef: 30, paramMap: v => v/100.0 }, { name: "Thermal Vision", desc: "False color map.", code: `mix(blue, yellow, red, luminance);`, hasParam: false } ]; const BOILERPLATES = { basic: `void main() { // Basic pass-through vec4 color = texture2D(u_image, v_texCoord); gl_FragColor = color; }`, invert: `void main() { vec4 color = texture2D(u_image, v_texCoord); // Invert RGB channels gl_FragColor = vec4(1.0 - color.rgb, 1.0); }`, wobble: `void main() { vec2 uv = v_texCoord; // Add sine wave to X based on Y and Time uv.x += sin(uv.y * 10.0 + u_time * 5.0) * 0.05; gl_FragColor = texture2D(u_image, uv); }`, vignette: `void main() { vec4 color = texture2D(u_image, v_texCoord); vec2 uv = v_texCoord; // Distance from center (0.5, 0.5) float dist = distance(uv, vec2(0.5)); // Darken edges color.rgb *= smoothstep(0.8, 0.2, dist); gl_FragColor = color; }` }; // --- WEBGL SETUP --- const canvas = document.getElementById("gl-canvas"); const gl = canvas.getContext("webgl"); const video = document.getElementById("webcam"); let activeFilterIndex = 0; let isCustomMode = false; let presetProgram = null; let customProgram = null; let currentProgram = null; // --- INIT --- function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); if (!gl) alert("WebGL not supported"); function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(shader); gl.deleteShader(shader); throw new Error(log); } return shader; } function createProgram(vSource, fSource) { const vs = createShader(gl, gl.VERTEX_SHADER, vSource); const fs = createShader(gl, gl.FRAGMENT_SHADER, fSource); const prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { throw new Error(gl.getProgramInfoLog(prog)); } return prog; } // 1. Compile Preset Program const vertexSource = document.getElementById("vertex-shader").text; const presetFragmentSource = document.getElementById("fragment-shader-presets").text; try { presetProgram = createProgram(vertexSource, presetFragmentSource); currentProgram = presetProgram; // Default } catch (e) { console.error("Critical: Failed to compile preset shader", e); } // Geometry (Two triangles) const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW); const texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 1,0, 0,1, 0,1, 1,0, 1,1]), gl.STATIC_DRAW); const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // --- CAMERA --- async function setupCamera() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }); video.srcObject = stream; video.play(); requestAnimationFrame(render); } catch (err) { document.getElementById('error-modal').classList.remove('hidden'); } } // --- RENDER LOOP --- let startTime = performance.now(); let frameCount = 0; let lastFpsTime = 0; function render(time) { frameCount++; if (time - lastFpsTime >= 1000) { document.getElementById('fps-counter').innerText = `FPS: ${frameCount} | GPU: Active`; frameCount = 0; lastFpsTime = time; } resizeCanvas(); // Handle dynamic resizes // 1. Aspect Ratio Logic ("Cover" fit) // Default 1.0 means no cropping let scaleX = 1.0; let scaleY = 1.0; if (video.readyState === video.HAVE_ENOUGH_DATA) { const vw = video.videoWidth; const vh = video.videoHeight; const cw = canvas.width; const ch = canvas.height; const videoAspect = vw / vh; const screenAspect = cw / ch; if (screenAspect > videoAspect) { // Screen is wider than video. We need to crop TOP/BOTTOM of video to fill width. // We keep Width (1.0), and scale Height down (inverse relationship) scaleY = videoAspect / screenAspect; } else { // Screen is taller than video. We need to crop SIDES of video to fill height. scaleX = screenAspect / videoAspect; } // Update texture gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video); } if (!currentProgram) { requestAnimationFrame(render); return; } gl.useProgram(currentProgram); // Bind Attributes const posLoc = gl.getAttribLocation(currentProgram, "a_position"); gl.enableVertexAttribArray(posLoc); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0); const texLoc = gl.getAttribLocation(currentProgram, "a_texCoord"); gl.enableVertexAttribArray(texLoc); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0); // Set Uniforms const resLoc = gl.getUniformLocation(currentProgram, "u_resolution"); const timeLoc = gl.getUniformLocation(currentProgram, "u_time"); const typeLoc = gl.getUniformLocation(currentProgram, "u_filterType"); const paramLoc = gl.getUniformLocation(currentProgram, "u_param"); const scaleLoc = gl.getUniformLocation(currentProgram, "u_textureScale"); if(resLoc) gl.uniform2f(resLoc, canvas.width, canvas.height); if(timeLoc) gl.uniform1f(timeLoc, (time - startTime) * 0.001); if(typeLoc) gl.uniform1i(typeLoc, activeFilterIndex); if(scaleLoc) gl.uniform2f(scaleLoc, scaleX, scaleY); // Apply aspect fix // Slider mapping const slider = document.getElementById('param-slider'); if (isCustomMode) { if(paramLoc) gl.uniform1f(paramLoc, slider.value / 100.0); } else { const config = FILTER_DATA[activeFilterIndex]; let val = 0.5; if(config.hasParam && config.paramMap) val = config.paramMap(slider.value); if(paramLoc) gl.uniform1f(paramLoc, val); } gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame(render); } // --- UI & CUSTOM SHADER LOGIC --- const btnCustom = document.getElementById('btn-custom'); const editorPanel = document.getElementById('editor-panel'); const closeEditorBtn = document.getElementById('close-editor'); const customCodeArea = document.getElementById('custom-code'); const applyCustomBtn = document.getElementById('apply-custom'); const compileStatus = document.getElementById('compile-status'); const boilerplateSelect = document.getElementById('boilerplate-select'); const sliderContainer = document.getElementById('slider-container'); const slider = document.getElementById('param-slider'); const sliderLabel = document.getElementById('slider-label'); const sliderValDisplay = document.getElementById('slider-val'); // Initial Boilerplate const CUSTOM_HEADER = `precision mediump float; uniform sampler2D u_image; uniform vec2 u_resolution; uniform float u_time; uniform float u_param; varying vec2 v_texCoord; `; customCodeArea.value = BOILERPLATES.basic; function enableCustomMode() { isCustomMode = true; editorPanel.classList.remove('hidden'); setTimeout(() => { editorPanel.classList.remove('scale-95', 'opacity-0'); editorPanel.classList.add('scale-100', 'opacity-100'); }, 10); // UI Tweaks sliderContainer.classList.remove('hidden'); sliderLabel.textContent = "Custom Param (u_param)"; slider.value = 50; // Highlight Button document.querySelectorAll('.filter-btn').forEach(b => { b.classList.remove('ring-2', 'ring-cyan-400'); b.classList.add('opacity-50'); }); btnCustom.classList.remove('opacity-50'); btnCustom.classList.add('ring-2', 'ring-purple-400'); // If we have a valid custom program, use it, else try compiling what's in box if (customProgram) { currentProgram = customProgram; } else { compileCustomShader(); } } function disableCustomMode() { editorPanel.classList.add('scale-95', 'opacity-0'); setTimeout(() => editorPanel.classList.add('hidden'), 300); isCustomMode = false; currentProgram = presetProgram; document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('opacity-50')); btnCustom.classList.remove('ring-2', 'ring-purple-400'); // Restore UI for active preset updateUIForFilter(activeFilterIndex); } function compileCustomShader() { const userCode = customCodeArea.value; const fullFragmentSource = CUSTOM_HEADER + userCode; try { const newProg = createProgram(vertexSource, fullFragmentSource); // Cleanup old custom program if exists if (customProgram) gl.deleteProgram(customProgram); customProgram = newProg; currentProgram = customProgram; compileStatus.innerText = "Compiled Successfully"; compileStatus.className = "text-[10px] font-mono text-green-400 truncate max-w-[60%]"; } catch (err) { compileStatus.innerText = "Error: " + err.message.substring(0, 30) + "..."; compileStatus.className = "text-[10px] font-mono text-red-400 truncate max-w-[60%]"; console.error(err); } } // Event Listeners btnCustom.addEventListener('click', enableCustomMode); closeEditorBtn.addEventListener('click', disableCustomMode); applyCustomBtn.addEventListener('click', compileCustomShader); boilerplateSelect.addEventListener('change', (e) => { if(BOILERPLATES[e.target.value]) { customCodeArea.value = BOILERPLATES[e.target.value]; compileCustomShader(); } }); // Preset Filters Logic function updateUIForFilter(index) { if (isCustomMode) return; // Ignore if custom is open activeFilterIndex = index; const data = FILTER_DATA[index]; // Buttons document.querySelectorAll('.filter-btn:not(#btn-custom)').forEach((btn, i) => { if (i === index) { btn.classList.remove('bg-slate-800', 'text-slate-300', 'hover:bg-slate-700'); btn.classList.add('bg-cyan-600', 'text-white', 'ring-2', 'ring-cyan-400'); } else { btn.classList.add('bg-slate-800', 'text-slate-300', 'hover:bg-slate-700'); btn.classList.remove('bg-cyan-600', 'text-white', 'ring-2', 'ring-cyan-400'); } }); // Info Panel document.getElementById('filter-title').textContent = data.name; document.getElementById('filter-desc').textContent = data.desc; document.getElementById('filter-code').textContent = data.code; // Slider if (data.hasParam) { sliderContainer.classList.remove('hidden'); sliderLabel.textContent = data.paramName; slider.min = data.paramMin; slider.max = data.paramMax; slider.value = data.paramDef; sliderValDisplay.textContent = slider.value; } else { sliderContainer.classList.add('hidden'); } } document.querySelectorAll('.filter-btn:not(#btn-custom)').forEach(btn => { btn.addEventListener('click', (e) => { if(isCustomMode) disableCustomMode(); updateUIForFilter(parseInt(e.target.dataset.filter)); }); }); slider.addEventListener('input', (e) => sliderValDisplay.textContent = e.target.value); // Info Panel Toggle const infoPanel = document.getElementById('info-panel'); const toggleInfoBtn = document.getElementById('toggle-info'); const closeInfoBtn = document.getElementById('close-info'); let isInfoOpen = false; toggleInfoBtn.addEventListener('click', () => { isInfoOpen = !isInfoOpen; if(isInfoOpen) { infoPanel.classList.remove('pointer-events-none', 'scale-y-0', 'opacity-0'); infoPanel.classList.add('pointer-events-auto', 'scale-y-100', 'opacity-100'); } else { infoPanel.classList.add('pointer-events-none', 'scale-y-0', 'opacity-0'); infoPanel.classList.remove('pointer-events-auto', 'scale-y-100', 'opacity-100'); } }); closeInfoBtn.addEventListener('click', () => toggleInfoBtn.click()); // Start updateUIForFilter(0); setupCamera(); </script> </body> </html>
No revisions found. Save the file to create a backup.
Delete
Update App