Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GLSL Shader Viewer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: Arial, sans-serif; | |
| } | |
| body { | |
| background-color: #121212; | |
| color: #e0e0e0; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 1200px; | |
| background-color: #1e1e1e; | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); | |
| } | |
| h1 { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| color: #3498db; | |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); | |
| } | |
| .shader-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| @media (min-width: 992px) { | |
| .shader-container { | |
| flex-direction: row; | |
| } | |
| .shader-controls { | |
| width: 30%; | |
| } | |
| .shader-preview { | |
| width: 70%; | |
| } | |
| } | |
| .shader-controls { | |
| background-color: #242424; | |
| padding: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); | |
| } | |
| .shader-preview { | |
| position: relative; | |
| overflow: hidden; | |
| border: 2px solid #3498db; | |
| border-radius: 8px; | |
| box-shadow: 0 0 20px rgba(52, 152, 219, 0.2); | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| background-color: #000; | |
| } | |
| .form-group { | |
| margin-bottom: 15px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-weight: bold; | |
| color: #3498db; | |
| } | |
| input[type="file"], select { | |
| width: 100%; | |
| padding: 8px; | |
| border-radius: 5px; | |
| border: 1px solid #333; | |
| background-color: #2a2a2a; | |
| color: #e0e0e0; | |
| margin-bottom: 10px; | |
| } | |
| button { | |
| background-color: #3498db; | |
| color: #121212; | |
| border: none; | |
| padding: 10px 15px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| transition: all 0.3s; | |
| margin-right: 10px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| button:hover { | |
| background-color: #2980b9; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | |
| } | |
| .shader-controls-section { | |
| background-color: #1a1a1a; | |
| border-radius: 5px; | |
| padding: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .shader-controls-section h3 { | |
| margin-bottom: 10px; | |
| color: #3498db; | |
| font-size: 16px; | |
| border-bottom: 1px solid #333; | |
| padding-bottom: 5px; | |
| } | |
| .control-row { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .control-row label { | |
| flex: 1; | |
| margin-bottom: 0; | |
| } | |
| .control-row input[type="range"] { | |
| flex: 2; | |
| } | |
| .control-row .value-display { | |
| flex: 0 0 60px; | |
| text-align: right; | |
| font-family: monospace; | |
| color: #3498db; | |
| } | |
| .shader-code { | |
| background-color: #1a1a1a; | |
| border-radius: 5px; | |
| padding: 10px; | |
| margin-top: 15px; | |
| font-family: monospace; | |
| font-size: 12px; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| white-space: pre; | |
| color: #ddd; | |
| border-left: 3px solid #3498db; | |
| } | |
| .shader-code.error { | |
| border-left-color: #e74c3c; | |
| color: #e74c3c; | |
| } | |
| .code-container { | |
| position: relative; | |
| } | |
| .code-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| } | |
| .code-header h3 { | |
| margin: 0; | |
| } | |
| .code-toggle { | |
| background: none; | |
| border: none; | |
| color: #3498db; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| .code-toggle:hover { | |
| text-decoration: underline; | |
| } | |
| .status-indicator { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| margin-right: 5px; | |
| } | |
| .status-indicator.success { | |
| background-color: #2ecc71; | |
| } | |
| .status-indicator.error { | |
| background-color: #e74c3c; | |
| } | |
| .status-message { | |
| margin-top: 10px; | |
| padding: 8px; | |
| border-radius: 4px; | |
| background-color: #242424; | |
| font-size: 14px; | |
| } | |
| .status-message.success { | |
| color: #2ecc71; | |
| border-left: 3px solid #2ecc71; | |
| } | |
| .status-message.error { | |
| color: #e74c3c; | |
| border-left: 3px solid #e74c3c; | |
| } | |
| .preset-controls { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .preset-btn { | |
| background-color: #2c3e50; | |
| color: #ecf0f1; | |
| border: none; | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| transition: all 0.2s; | |
| } | |
| .preset-btn:hover { | |
| background-color: #34495e; | |
| } | |
| .preset-btn.active { | |
| background-color: #3498db; | |
| box-shadow: 0 0 8px rgba(52, 152, 219, 0.5); | |
| } | |
| .resolution-control { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .resolution-control input { | |
| width: 80px; | |
| text-align: center; | |
| } | |
| .resolution-control span { | |
| color: #3498db; | |
| font-weight: bold; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>GLSL Shader Viewer</h1> | |
| <div class="shader-container"> | |
| <div class="shader-controls"> | |
| <div class="form-group"> | |
| <label for="shaderFile">Upload Shader File:</label> | |
| <input type="file" id="shaderFile" accept=".glsl,.frag,.vert"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="shaderPresets">Shader Presets:</label> | |
| <div class="preset-controls"> | |
| <button id="cloudPreset" class="preset-btn active">Clouds</button> | |
| <button id="crtPreset" class="preset-btn">CRT Effect</button> | |
| <button id="noisePreset" class="preset-btn">Noise</button> | |
| </div> | |
| </div> | |
| <div class="shader-controls-section"> | |
| <h3>Display Settings</h3> | |
| <div class="form-group"> | |
| <label for="resolutionSelect">Resolution:</label> | |
| <select id="resolutionSelect"> | |
| <option value="auto">Auto (Canvas Size)</option> | |
| <option value="custom">Custom</option> | |
| <option value="720p">720p (1280×720)</option> | |
| <option value="1080p">1080p (1920×1080)</option> | |
| </select> | |
| </div> | |
| <div id="customResolution" style="display: none;"> | |
| <div class="resolution-control"> | |
| <input type="number" id="resolutionWidth" value="800" min="100" max="2560"> | |
| <span>×</span> | |
| <input type="number" id="resolutionHeight" value="600" min="100" max="1440"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="shader-controls-section"> | |
| <h3>Uniform Values</h3> | |
| <!-- Time Control --> | |
| <div class="control-row"> | |
| <label for="timeSpeed">Time Speed:</label> | |
| <input type="range" id="timeSpeed" min="0" max="2" step="0.05" value="1"> | |
| <span class="value-display" id="timeSpeedValue">1.00</span> | |
| </div> | |
| <!-- Mouse Position --> | |
| <div class="control-row"> | |
| <label>Mouse Position:</label> | |
| <span class="value-display" id="mousePosition">0, 0</span> | |
| </div> | |
| <!-- Cloud Parameters --> | |
| <div id="cloudParams"> | |
| <h3>Cloud Parameters</h3> | |
| <div class="control-row"> | |
| <label for="cloudCoverage">Coverage:</label> | |
| <input type="range" id="cloudCoverage" min="0" max="1" step="0.05" value="0.6"> | |
| <span class="value-display" id="cloudCoverageValue">0.60</span> | |
| </div> | |
| <div class="control-row"> | |
| <label for="cloudSharpness">Sharpness:</label> | |
| <input type="range" id="cloudSharpness" min="0.01" max="0.5" step="0.01" value="0.3"> | |
| <span class="value-display" id="cloudSharpnessValue">0.30</span> | |
| </div> | |
| <div class="control-row"> | |
| <label for="cloudSpeed">Movement:</label> | |
| <input type="range" id="cloudSpeed" min="0" max="2" step="0.05" value="0.5"> | |
| <span class="value-display" id="cloudSpeedValue">0.50</span> | |
| </div> | |
| </div> | |
| <!-- CRT Parameters --> | |
| <div id="crtParams" style="display: none;"> | |
| <h3>CRT Parameters</h3> | |
| <div class="control-row"> | |
| <label for="crtCurvature">Curvature:</label> | |
| <input type="range" id="crtCurvature" min="0" max="2" step="0.05" value="1.1"> | |
| <span class="value-display" id="crtCurvatureValue">1.10</span> | |
| </div> | |
| <div class="control-row"> | |
| <label for="crtScanlines">Scanlines:</label> | |
| <input type="range" id="crtScanlines" min="0" max="2" step="0.05" value="1.0"> | |
| <span class="value-display" id="crtScanlinesValue">1.00</span> | |
| </div> | |
| <div class="control-row"> | |
| <label for="crtChromatic">Chromatic:</label> | |
| <input type="range" id="crtChromatic" min="0" max="2" step="0.05" value="1.0"> | |
| <span class="value-display" id="crtChromaticValue">1.00</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <button id="playPauseBtn">Pause</button> | |
| <button id="resetBtn">Reset</button> | |
| </div> | |
| <div id="statusMessage" class="status-message success" style="display: none;"></div> | |
| </div> | |
| <div class="shader-preview"> | |
| <canvas id="shaderCanvas"></canvas> | |
| </div> | |
| </div> | |
| <div class="code-container"> | |
| <div class="code-header"> | |
| <h3> | |
| <span class="status-indicator success" id="shaderStatus"></span> | |
| Shader Code | |
| </h3> | |
| <button class="code-toggle" id="toggleCode">Show/Hide Code</button> | |
| </div> | |
| <div class="shader-code" id="shaderCode" style="display: none;"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const canvas = document.getElementById('shaderCanvas'); | |
| const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); | |
| const shaderFileInput = document.getElementById('shaderFile'); | |
| const cloudPresetBtn = document.getElementById('cloudPreset'); | |
| const crtPresetBtn = document.getElementById('crtPreset'); | |
| const noisePresetBtn = document.getElementById('noisePreset'); | |
| const playPauseBtn = document.getElementById('playPauseBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const timeSpeedSlider = document.getElementById('timeSpeed'); | |
| const timeSpeedValue = document.getElementById('timeSpeedValue'); | |
| const cloudCoverageSlider = document.getElementById('cloudCoverage'); | |
| const cloudCoverageValue = document.getElementById('cloudCoverageValue'); | |
| const cloudSharpnessSlider = document.getElementById('cloudSharpness'); | |
| const cloudSharpnessValue = document.getElementById('cloudSharpnessValue'); | |
| const cloudSpeedSlider = document.getElementById('cloudSpeed'); | |
| const cloudSpeedValue = document.getElementById('cloudSpeedValue'); | |
| const crtCurvatureSlider = document.getElementById('crtCurvature'); | |
| const crtCurvatureValue = document.getElementById('crtCurvatureValue'); | |
| const crtScanlinesSlider = document.getElementById('crtScanlines'); | |
| const crtScanlinesValue = document.getElementById('crtScanlinesValue'); | |
| const crtChromaticSlider = document.getElementById('crtChromatic'); | |
| const crtChromaticValue = document.getElementById('crtChromaticValue'); | |
| const shaderCode = document.getElementById('shaderCode'); | |
| const toggleCodeBtn = document.getElementById('toggleCode'); | |
| const shaderStatus = document.getElementById('shaderStatus'); | |
| const statusMessage = document.getElementById('statusMessage'); | |
| const mousePosition = document.getElementById('mousePosition'); | |
| const resolutionSelect = document.getElementById('resolutionSelect'); | |
| const customResolution = document.getElementById('customResolution'); | |
| const resolutionWidth = document.getElementById('resolutionWidth'); | |
| const resolutionHeight = document.getElementById('resolutionHeight'); | |
| const cloudParams = document.getElementById('cloudParams'); | |
| const crtParams = document.getElementById('crtParams'); | |
| // Check if WebGL is available | |
| if (!gl) { | |
| alert('Unable to initialize WebGL. Your browser may not support it.'); | |
| } | |
| // Shader state | |
| let isPlaying = true; | |
| let startTime = Date.now(); | |
| let timeSpeed = 1.0; | |
| let currentShaderType = 'cloud'; | |
| let mouseX = 0; | |
| let mouseY = 0; | |
| let cameraOffsetX = 0; | |
| let cameraOffsetY = 0; | |
| // Initialize cloud parameters | |
| let cloudCoverage = 0.6; | |
| let cloudSharpness = 0.3; | |
| let cloudSpeed = 0.5; | |
| // Initialize CRT parameters | |
| let crtCurvature = 1.1; | |
| let crtScanlines = 1.0; | |
| let crtChromatic = 1.0; | |
| // Keep track of shader program | |
| let shaderProgram = null; | |
| let uniformLocations = {}; | |
| // Vertex shader for a quad that fills the canvas | |
| const vertexShaderSource = ` | |
| attribute vec2 a_position; | |
| attribute vec2 a_texCoord; | |
| varying vec2 texture_coords; | |
| varying vec2 screen_coords; | |
| void main() { | |
| gl_Position = vec4(a_position, 0, 1); | |
| texture_coords = a_texCoord; | |
| screen_coords = (a_position + 1.0) * 0.5 * vec2(${canvas.width}.0, ${canvas.height}.0); | |
| } | |
| `; | |
| // Default fragment shaders | |
| const cloudShaderSource = ` | |
| // Cloud shader converted for WebGL | |
| precision mediump float; | |
| // Uniform variables | |
| uniform float millis; | |
| uniform vec2 resolution; | |
| uniform vec2 cameraOffset; | |
| // Hash function for noise generation | |
| vec2 hash(vec2 p) | |
| { | |
| p = vec2(dot(p, vec2(127.1, 311.7)), | |
| dot(p, vec2(269.5, 183.3))); | |
| return -1.0 + 2.0 * fract(sin(p) * 43758.5453123); | |
| } | |
| // Improved gradient noise function | |
| float noise(in vec2 p) | |
| { | |
| vec2 i = floor(p); | |
| vec2 f = fract(p); | |
| // Cubic Hermite curve for smoother interpolation | |
| vec2 u = f * f * (3.0 - 2.0 * f); | |
| // Improved gradient interpolation | |
| return mix( | |
| mix(dot(hash(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)), | |
| dot(hash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x), | |
| mix(dot(hash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)), | |
| dot(hash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), | |
| u.y | |
| ); | |
| } | |
| // Enhanced fBm with more octaves | |
| float fbm(vec2 p) | |
| { | |
| float f = 0.0; | |
| float amplitude = 0.5; | |
| float frequency = 1.0; | |
| float total_amplitude = 0.0; | |
| // Rotation matrix for domain warping | |
| mat2 m = mat2(1.6, 1.2, -1.2, 1.6); | |
| // More octaves for richer detail | |
| for (int i = 0; i < 6; i++) { | |
| f += amplitude * noise(frequency * p); | |
| total_amplitude += amplitude; | |
| amplitude *= 0.5; | |
| frequency *= 2.0; | |
| p = m * p; | |
| } | |
| return f / total_amplitude; | |
| } | |
| // Domain warping for more natural cloud shapes | |
| vec2 warp_domain(vec2 p, float time) { | |
| // Apply slow warping to the domain | |
| vec2 offset = vec2( | |
| fbm(p + vec2(0.0, 0.1*time)), | |
| fbm(p + vec2(0.1*time, 0.0)) | |
| ); | |
| // Second level of warping | |
| return p + 0.4 * offset; | |
| } | |
| // Cloud density function | |
| float cloud_density(float noise_val, float coverage, float sharpness) { | |
| // Map noise from [-1, 1] to [0, 1] | |
| float mapped = (noise_val + 1.0) * 0.5; | |
| // Apply coverage control and sharpening | |
| return smoothstep(1.0 - coverage, 1.0 - coverage + sharpness, mapped); | |
| } | |
| // Additional uniform for cloud parameters | |
| uniform float cloudCoverage; | |
| uniform float cloudSharpness; | |
| uniform float cloudSpeed; | |
| void main() { | |
| // Normalize coordinates | |
| vec2 uv = gl_FragCoord.xy / resolution.xy; | |
| // Time calculation with speed control | |
| float time = millis * cloudSpeed; | |
| // Calculate a base UV that includes camera offset | |
| float cloud_world_scale = 0.001; | |
| vec2 uv_world = uv + (cameraOffset * cloud_world_scale); | |
| // Movement vectors | |
| vec2 time_movement1 = vec2(time * 0.01, time * 0.005); | |
| vec2 time_movement2 = vec2(time * 0.015, -time * 0.007); | |
| vec2 time_movement3 = vec2(time * 0.02, time * 0.01); | |
| // BASE LAYER - large clouds | |
| vec2 warped_uv1 = warp_domain(uv_world, time * 0.2); | |
| float base_clouds = fbm(warped_uv1 * 2.0 + time_movement1); | |
| // DETAIL LAYER - medium features | |
| vec2 warped_uv2 = warp_domain(uv_world * 1.5, time * 0.3); | |
| float detail_clouds = fbm(warped_uv2 * 4.0 + time_movement2); | |
| // WISP LAYER - high-frequency details | |
| float wisp_clouds = fbm(uv_world * 8.0 + time_movement3); | |
| // Combine layers with different weights | |
| float combined = base_clouds * 0.65 + detail_clouds * 0.25 + wisp_clouds * 0.1; | |
| // Shape the noise into defined cloud formations using provided parameters | |
| float cloud_shape = cloud_density(combined, cloudCoverage, cloudSharpness); | |
| // Add height variation for 3D effect | |
| vec3 cloud_color = mix( | |
| vec3(0.8, 0.8, 0.85), // Bottom color (slightly grayish) | |
| vec3(1.0, 1.0, 1.0), // Top color (bright white) | |
| cloud_shape * 0.7 + base_clouds * 0.3 | |
| ); | |
| // Final cloud color with alpha | |
| float opacity = cloud_shape * 0.85; | |
| gl_FragColor = vec4(cloud_color, opacity); | |
| } | |
| `; | |
| const crtShaderSource = ` | |
| // CRT shader converted for WebGL | |
| precision mediump float; | |
| // Uniform variables | |
| uniform float millis; | |
| uniform vec2 resolution; | |
| uniform sampler2D u_texture; | |
| // Additional parameters for CRT effect | |
| uniform float crtCurvature; | |
| uniform float crtScanlines; | |
| uniform float crtChromatic; | |
| // Helper function for screen curvature effect | |
| vec2 curve(vec2 uv) | |
| { | |
| uv = (uv - 0.5) * 2.0; // Map uv from [0,1] to [-1,1] | |
| uv *= crtCurvature; // Apply curvature parameter | |
| // Apply barrel distortion based on distance from center | |
| uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); | |
| uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); | |
| uv = (uv / 2.0) + 0.5; // Map back to [0,1] | |
| uv = uv * 0.92 + 0.04; // Scale down and add border margin | |
| return uv; | |
| } | |
| // Helper to sample texture with bounds checking | |
| vec4 texSample(sampler2D tex, vec2 uv) { | |
| if(uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { | |
| return vec4(0.0, 0.0, 0.0, 1.0); | |
| } | |
| return texture2D(tex, uv); | |
| } | |
| void main() { | |
| // Normalize coordinates | |
| vec2 uv = gl_FragCoord.xy / resolution.xy; | |
| // Apply screen curvature | |
| vec2 curved_uv = curve(uv); | |
| // Base color sample (replace with a generated pattern for this example) | |
| // Generate a sample pattern as we don't have an actual texture | |
| vec2 patternUV = curved_uv * 10.0; // Scale for visibility | |
| vec3 baseColor = vec3( | |
| mod(floor(patternUV.x) + floor(patternUV.y), 2.0) * 0.5 + 0.25, | |
| mod(floor(patternUV.x * 0.7) + floor(patternUV.y * 0.7), 2.0) * 0.4 + 0.2, | |
| mod(floor(patternUV.x * 0.5) + floor(patternUV.y * 0.9), 2.0) * 0.5 + 0.15 | |
| ); | |
| vec3 col = vec3(0.0); | |
| // Chromatic aberration effect based on time | |
| float x = sin(0.3 * millis + curved_uv.y * 21.0) * | |
| sin(0.7 * millis + curved_uv.y * 29.0) * | |
| sin(0.3 + 0.33 * millis + curved_uv.y * 31.0) * | |
| 0.0017 * crtChromatic; | |
| // Sample R, G, B channels with chromatic aberration | |
| vec2 rUV = vec2(x + curved_uv.x + 0.001 * crtChromatic, curved_uv.y + 0.001 * crtChromatic); | |
| vec2 gUV = vec2(x + curved_uv.x, curved_uv.y - 0.002 * crtChromatic); | |
| vec2 bUV = vec2(x + curved_uv.x - 0.002 * crtChromatic, curved_uv.y); | |
| // Get RGB values with a patterned background to simulate content | |
| col.r = baseColor.r; | |
| if(rUV.x >= 0.0 && rUV.x <= 1.0 && rUV.y >= 0.0 && rUV.y <= 1.0) { | |
| vec2 patternR = rUV * 10.0; | |
| col.r = mod(floor(patternR.x) + floor(patternR.y), 2.0) * 0.5 + 0.25 + 0.05; | |
| } | |
| col.g = baseColor.g; | |
| if(gUV.x >= 0.0 && gUV.x <= 1.0 && gUV.y >= 0.0 && gUV.y <= 1.0) { | |
| vec2 patternG = gUV * 10.0; | |
| col.g = mod(floor(patternG.x * 0.7) + floor(patternG.y * 0.7), 2.0) * 0.4 + 0.2 + 0.05; | |
| } | |
| col.b = baseColor.b; | |
| if(bUV.x >= 0.0 && bUV.x <= 1.0 && bUV.y >= 0.0 && bUV.y <= 1.0) { | |
| vec2 patternB = bUV * 10.0; | |
| col.b = mod(floor(patternB.x * 0.5) + floor(patternB.y * 0.9), 2.0) * 0.5 + 0.15 + 0.05; | |
| } | |
| // Apply contrast curve and clamp | |
| col = clamp(col * 0.6 + 0.4 * col * col * 1.0, 0.0, 1.0); | |
| // Apply vignetting effect (darken corners) | |
| float vig = (0.0 + 1.0 * 16.0 * curved_uv.x * curved_uv.y * (1.0 - curved_uv.x) * (1.0 - curved_uv.y)); | |
| col *= vec3(pow(vig, 0.3)); | |
| // Adjust color balance and intensity | |
| col *= vec3(0.95, 1.05, 0.95); // Slightly tint green | |
| col *= 2.8; // Increase brightness | |
| // Simulate scanlines based on time and position | |
| float scans = clamp(0.35 + 0.35 * sin(3.5 * millis + curved_uv.y * resolution.y * 1.5 * crtScanlines), 0.0, 1.0); | |
| float s = pow(scans, 5.7); // Sharpen the scanline effect | |
| col = col * vec3(0.4 + 0.7 * s); // Darken based on scanlines | |
| // Add slight flicker effect | |
| col *= 1.0 + 0.01 * sin(10.0 * millis); | |
| // Black out pixels outside the curved screen area | |
| if (curved_uv.x < 0.0 || curved_uv.x > 1.0 || curved_uv.y < 0.0 || curved_uv.y > 1.0) { | |
| col = vec3(0.0); | |
| } | |
| // Simulate RGB pixel grid (simple version) | |
| col *= 1.0 - 0.65 * vec3(clamp((mod(gl_FragCoord.x, 2.0) - 1.0) * 2.0, 0.0, 1.0)); | |
| gl_FragColor = vec4(col, 1.0); | |
| } | |
| `; | |
| const noiseShaderSource = ` | |
| // Simple perlin noise shader | |
| precision mediump float; | |
| uniform vec2 resolution; | |
| uniform float millis; | |
| uniform vec2 cameraOffset; | |
| // Simplex noise helper functions | |
| vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); } | |
| float snoise(vec2 v) { | |
| const vec4 C = vec4(0.211324865405187, 0.366025403784439, | |
| -0.577350269189626, 0.024390243902439); | |
| vec2 i = floor(v + dot(v, C.yy)); | |
| vec2 x0 = v - i + dot(i, C.xx); | |
| vec2 i1; | |
| i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); | |
| vec4 x12 = x0.xyxy + C.xxzz; | |
| x12.xy -= i1; | |
| i = mod(i, 289.0); | |
| vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); | |
| vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); | |
| m = m*m; | |
| m = m*m; | |
| vec3 x = 2.0 * fract(p * C.www) - 1.0; | |
| vec3 h = abs(x) - 0.5; | |
| vec3 ox = floor(x + 0.5); | |
| vec3 a0 = x - ox; | |
| m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h); | |
| vec3 g; | |
| g.x = a0.x * x0.x + h.x * x0.y; | |
| g.yz = a0.yz * x12.xz + h.yz * x12.yw; | |
| return 130.0 * dot(m, g); | |
| } | |
| void main() { | |
| vec2 uv = gl_FragCoord.xy / resolution.xy; | |
| // Apply camera offset for panning | |
| vec2 p = uv + cameraOffset * 0.001; | |
| // Create animated noise pattern | |
| float n1 = snoise(p * 3.0 + millis * 0.1); | |
| float n2 = snoise(p * 6.0 - millis * 0.15); | |
| float n3 = snoise(p * 12.0 + millis * 0.2); | |
| // Combine noise at different frequencies | |
| float combinedNoise = | |
| 0.5 * n1 + | |
| 0.3 * n2 + | |
| 0.2 * n3; | |
| // Map from [-1,1] to [0,1] range | |
| combinedNoise = (combinedNoise + 1.0) * 0.5; | |
| // Create color gradient based on noise | |
| vec3 color = mix( | |
| vec3(0.2, 0.1, 0.4), // Dark purple for low values | |
| vec3(1.0, 0.8, 0.2), // Yellow for high values | |
| combinedNoise | |
| ); | |
| // Add some glow effects | |
| color += 0.05 * vec3(1.0, 0.6, 0.3) * pow(combinedNoise, 3.0); | |
| gl_FragColor = vec4(color, 1.0); | |
| } | |
| `; | |
| // Initialize WebGL | |
| function initWebGL() { | |
| // Set canvas size | |
| resizeCanvas(); | |
| // Create a vertex buffer for a quad that fills the viewport | |
| const vertices = new Float32Array([ | |
| -1.0, -1.0, // bottom left | |
| 1.0, -1.0, // bottom right | |
| -1.0, 1.0, // top left | |
| 1.0, 1.0 // top right | |
| ]); | |
| const vertexBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); | |
| // Set up texcoords for the quad | |
| const texCoords = new Float32Array([ | |
| 0.0, 0.0, // bottom left | |
| 1.0, 0.0, // bottom right | |
| 0.0, 1.0, // top left | |
| 1.0, 1.0 // top right | |
| ]); | |
| const texCoordBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW); | |
| // Use the cloud shader by default | |
| loadShader(cloudShaderSource); | |
| // Start the rendering loop | |
| requestAnimationFrame(render); | |
| } | |
| // Resize canvas based on resolution settings | |
| function resizeCanvas() { | |
| const parent = canvas.parentElement; | |
| const resolution = resolutionSelect.value; | |
| if (resolution === 'auto') { | |
| // Use the container size | |
| canvas.width = parent.clientWidth; | |
| canvas.height = Math.floor(parent.clientWidth * 0.5625); // 16:9 aspect ratio | |
| } else if (resolution === 'custom') { | |
| // Use custom resolution | |
| canvas.width = parseInt(resolutionWidth.value); | |
| canvas.height = parseInt(resolutionHeight.value); | |
| } else if (resolution === '720p') { | |
| canvas.width = 1280; | |
| canvas.height = 720; | |
| } else if (resolution === '1080p') { | |
| canvas.width = 1920; | |
| canvas.height = 1080; | |
| } | |
| // Update the vertex shader with new dimensions | |
| if (shaderProgram) { | |
| loadShader(currentShaderType === 'cloud' ? cloudShaderSource : | |
| (currentShaderType === 'crt' ? crtShaderSource : noiseShaderSource)); | |
| } | |
| // Update WebGL viewport | |
| gl.viewport(0, 0, canvas.width, canvas.height); | |
| } | |
| // Compile and link shaders | |
| function loadShader(fragmentShaderSource, isUpload = false) { | |
| try { | |
| // Create and compile vertex shader | |
| const vertexShader = gl.createShader(gl.VERTEX_SHADER); | |
| gl.shaderSource(vertexShader, vertexShaderSource); | |
| gl.compileShader(vertexShader); | |
| if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { | |
| throw new Error("Vertex shader compilation failed: " + gl.getShaderInfoLog(vertexShader)); | |
| } | |
| // Create and compile fragment shader | |
| const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); | |
| gl.shaderSource(fragmentShader, fragmentShaderSource); | |
| gl.compileShader(fragmentShader); | |
| if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { | |
| throw new Error("Fragment shader compilation failed: " + gl.getShaderInfoLog(fragmentShader)); | |
| } | |
| // Create shader program | |
| const program = gl.createProgram(); | |
| gl.attachShader(program, vertexShader); | |
| gl.attachShader(program, fragmentShader); | |
| gl.linkProgram(program); | |
| if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
| throw new Error("Shader program linking failed: " + gl.getProgramInfoLog(program)); | |
| } | |
| // Clean up old program if exists | |
| if (shaderProgram) { | |
| gl.deleteProgram(shaderProgram); | |
| } | |
| // Use the new program | |
| shaderProgram = program; | |
| gl.useProgram(shaderProgram); | |
| // Set up attribute locations | |
| const positionAttribLocation = gl.getAttribLocation(shaderProgram, "a_position"); | |
| gl.enableVertexAttribArray(positionAttribLocation); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ | |
| -1.0, -1.0, // bottom left | |
| 1.0, -1.0, // bottom right | |
| -1.0, 1.0, // top left | |
| 1.0, 1.0 // top right | |
| ]), gl.STATIC_DRAW); | |
| gl.vertexAttribPointer(positionAttribLocation, 2, gl.FLOAT, false, 0, 0); | |
| const texCoordAttribLocation = gl.getAttribLocation(shaderProgram, "a_texCoord"); | |
| if (texCoordAttribLocation !== -1) { | |
| gl.enableVertexAttribArray(texCoordAttribLocation); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ | |
| 0.0, 0.0, // bottom left | |
| 1.0, 0.0, // bottom right | |
| 0.0, 1.0, // top left | |
| 1.0, 1.0 // top right | |
| ]), gl.STATIC_DRAW); | |
| gl.vertexAttribPointer(texCoordAttribLocation, 2, gl.FLOAT, false, 0, 0); | |
| } | |
| // Get uniform locations | |
| uniformLocations = { | |
| millis: gl.getUniformLocation(shaderProgram, "millis"), | |
| resolution: gl.getUniformLocation(shaderProgram, "resolution"), | |
| cameraOffset: gl.getUniformLocation(shaderProgram, "cameraOffset"), | |
| cloudCoverage: gl.getUniformLocation(shaderProgram, "cloudCoverage"), | |
| cloudSharpness: gl.getUniformLocation(shaderProgram, "cloudSharpness"), | |
| cloudSpeed: gl.getUniformLocation(shaderProgram, "cloudSpeed"), | |
| crtCurvature: gl.getUniformLocation(shaderProgram, "crtCurvature"), | |
| crtScanlines: gl.getUniformLocation(shaderProgram, "crtScanlines"), | |
| crtChromatic: gl.getUniformLocation(shaderProgram, "crtChromatic") | |
| }; | |
| // Update shader code display | |
| shaderCode.textContent = fragmentShaderSource; | |
| // Update status | |
| shaderStatus.className = 'status-indicator success'; | |
| if (isUpload) { | |
| statusMessage.textContent = 'Shader compiled successfully!'; | |
| statusMessage.className = 'status-message success'; | |
| statusMessage.style.display = 'block'; | |
| setTimeout(() => { statusMessage.style.display = 'none'; }, 3000); | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('Shader compilation error:', error); | |
| // Update status | |
| shaderStatus.className = 'status-indicator error'; | |
| statusMessage.textContent = 'Shader Error: ' + error.message; | |
| statusMessage.className = 'status-message error'; | |
| statusMessage.style.display = 'block'; | |
| // Show error in shader code display | |
| shaderCode.className = 'shader-code error'; | |
| shaderCode.textContent = error.message + '\n\nShader Source:\n' + fragmentShaderSource; | |
| // Make sure code is visible | |
| shaderCode.style.display = 'block'; | |
| return false; | |
| } | |
| } | |
| // Main render loop | |
| function render() { | |
| if (!gl || !shaderProgram) { | |
| requestAnimationFrame(render); | |
| return; | |
| } | |
| // Clear canvas | |
| gl.clearColor(0.0, 0.0, 0.0, 1.0); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| // Set uniforms | |
| const currentTime = isPlaying ? (Date.now() - startTime) / 1000.0 * timeSpeed : 0; | |
| if (uniformLocations.millis !== -1) { | |
| gl.uniform1f(uniformLocations.millis, currentTime); | |
| } | |
| if (uniformLocations.resolution !== -1) { | |
| gl.uniform2f(uniformLocations.resolution, canvas.width, canvas.height); | |
| } | |
| if (uniformLocations.cameraOffset !== -1) { | |
| gl.uniform2f(uniformLocations.cameraOffset, cameraOffsetX, cameraOffsetY); | |
| } | |
| // Set shader-specific uniforms | |
| if (currentShaderType === 'cloud') { | |
| if (uniformLocations.cloudCoverage !== -1) { | |
| gl.uniform1f(uniformLocations.cloudCoverage, cloudCoverage); | |
| } | |
| if (uniformLocations.cloudSharpness !== -1) { | |
| gl.uniform1f(uniformLocations.cloudSharpness, cloudSharpness); | |
| } | |
| if (uniformLocations.cloudSpeed !== -1) { | |
| gl.uniform1f(uniformLocations.cloudSpeed, cloudSpeed); | |
| } | |
| } else if (currentShaderType === 'crt') { | |
| if (uniformLocations.crtCurvature !== -1) { | |
| gl.uniform1f(uniformLocations.crtCurvature, crtCurvature); | |
| } | |
| if (uniformLocations.crtScanlines !== -1) { | |
| gl.uniform1f(uniformLocations.crtScanlines, crtScanlines); | |
| } | |
| if (uniformLocations.crtChromatic !== -1) { | |
| gl.uniform1f(uniformLocations.crtChromatic, crtChromatic); | |
| } | |
| } | |
| // Draw the quad | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| // Continue animation | |
| requestAnimationFrame(render); | |
| } | |
| // Handle shader file upload | |
| shaderFileInput.addEventListener('change', (event) => { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const shaderSource = e.target.result; | |
| // Try to determine shader type from content | |
| if (shaderSource.includes('cloud_density') || shaderSource.includes('fbm')) { | |
| currentShaderType = 'cloud'; | |
| cloudParams.style.display = 'block'; | |
| crtParams.style.display = 'none'; | |
| // Update preset buttons | |
| cloudPresetBtn.classList.add('active'); | |
| crtPresetBtn.classList.remove('active'); | |
| noisePresetBtn.classList.remove('active'); | |
| } else if (shaderSource.includes('curve') || shaderSource.includes('scanline')) { | |
| currentShaderType = 'crt'; | |
| cloudParams.style.display = 'none'; | |
| crtParams.style.display = 'block'; | |
| // Update preset buttons | |
| cloudPresetBtn.classList.remove('active'); | |
| crtPresetBtn.classList.add('active'); | |
| noisePresetBtn.classList.remove('active'); | |
| } else { | |
| // Default to cloud shader if can't determine | |
| currentShaderType = 'cloud'; | |
| cloudParams.style.display = 'block'; | |
| crtParams.style.display = 'none'; | |
| // Update preset buttons | |
| cloudPresetBtn.classList.add('active'); | |
| crtPresetBtn.classList.remove('active'); | |
| noisePresetBtn.classList.remove('active'); | |
| } | |
| // Adapt LÖVE shader to WebGL | |
| let adaptedSource = adaptLoveShaderToWebGL(shaderSource); | |
| // Load the shader | |
| loadShader(adaptedSource, true); | |
| // Show the shader code | |
| shaderCode.style.display = 'block'; | |
| }; | |
| reader.readAsText(file); | |
| } | |
| }); | |
| // Adapt LÖVE shader to WebGL | |
| function adaptLoveShaderToWebGL(source) { | |
| // Add precision specifier if missing | |
| if (!source.includes('precision ')) { | |
| source = 'precision mediump float;\n\n' + source; | |
| } | |
| // Replace LÖVE's effect function with main | |
| source = source.replace(/vec4\s+effect\s*\(\s*vec4\s+color\s*,\s*Image\s+tex\s*,\s*vec2\s+texture_coords\s*,\s*vec2\s+screen_coords\s*\)/g, | |
| 'void main()'); | |
| // Replace Texel with texture2D | |
| source = source.replace(/Texel\s*\(\s*tex\s*,/g, 'texture2D(u_texture,'); | |
| // Add necessary uniforms if they don't exist | |
| if (!source.includes('uniform vec2 resolution')) { | |
| source = source.replace('void main()', | |
| 'uniform vec2 resolution;\n\nvoid main()'); | |
| } | |
| // Add cloud-specific uniforms if needed | |
| if (source.includes('cloud_density')) { | |
| if (!source.includes('uniform float cloudCoverage')) { | |
| source = source.replace('void main()', | |
| 'uniform float cloudCoverage;\nuniform float cloudSharpness;\nuniform float cloudSpeed;\n\nvoid main()'); | |
| } | |
| } | |
| // Add CRT-specific uniforms if needed | |
| if (source.includes('curve(') || source.includes('scanline')) { | |
| if (!source.includes('uniform float crtCurvature')) { | |
| source = source.replace('void main()', | |
| 'uniform float crtCurvature;\nuniform float crtScanlines;\nuniform float crtChromatic;\n\nvoid main()'); | |
| } | |
| } | |
| // Replace texture_coords and screen_coords with gl_FragCoord | |
| source = source.replace(/texture_coords/g, 'gl_FragCoord.xy / resolution.xy'); | |
| source = source.replace(/screen_coords/g, 'gl_FragCoord.xy'); | |
| // Replace return statements with gl_FragColor assignment | |
| source = source.replace(/return\s+(.*?);/g, 'gl_FragColor = $1;'); | |
| return source; | |
| } | |
| // Initialize event listeners for controls | |
| function initControlListeners() { | |
| // Play/Pause button | |
| playPauseBtn.addEventListener('click', () => { | |
| isPlaying = !isPlaying; | |
| playPauseBtn.textContent = isPlaying ? 'Pause' : 'Play'; | |
| if (isPlaying) { | |
| startTime = Date.now() - (startTime - Date.now()); // Adjust for pause time | |
| } | |
| }); | |
| // Reset button | |
| resetBtn.addEventListener('click', () => { | |
| startTime = Date.now(); | |
| cameraOffsetX = 0; | |
| cameraOffsetY = 0; | |
| }); | |
| // Time speed control | |
| timeSpeedSlider.addEventListener('input', (e) => { | |
| timeSpeed = parseFloat(e.target.value); | |
| timeSpeedValue.textContent = timeSpeed.toFixed(2); | |
| }); | |
| // Cloud parameters | |
| cloudCoverageSlider.addEventListener('input', (e) => { | |
| cloudCoverage = parseFloat(e.target.value); | |
| cloudCoverageValue.textContent = cloudCoverage.toFixed(2); | |
| }); | |
| cloudSharpnessSlider.addEventListener('input', (e) => { | |
| cloudSharpness = parseFloat(e.target.value); | |
| cloudSharpnessValue.textContent = cloudSharpness.toFixed(2); | |
| }); | |
| cloudSpeedSlider.addEventListener('input', (e) => { | |
| cloudSpeed = parseFloat(e.target.value); | |
| cloudSpeedValue.textContent = cloudSpeed.toFixed(2); | |
| }); | |
| // CRT parameters | |
| crtCurvatureSlider.addEventListener('input', (e) => { | |
| crtCurvature = parseFloat(e.target.value); | |
| crtCurvatureValue.textContent = crtCurvature.toFixed(2); | |
| }); | |
| crtScanlinesSlider.addEventListener('input', (e) => { | |
| crtScanlines = parseFloat(e.target.value); | |
| crtScanlinesValue.textContent = crtScanlines.toFixed(2); | |
| }); | |
| crtChromaticSlider.addEventListener('input', (e) => { | |
| crtChromatic = parseFloat(e.target.value); | |
| crtChromaticValue.textContent = crtChromatic.toFixed(2); | |
| }); | |
| // Show/Hide shader code | |
| toggleCodeBtn.addEventListener('click', () => { | |
| if (shaderCode.style.display === 'none') { | |
| shaderCode.style.display = 'block'; | |
| } else { | |
| shaderCode.style.display = 'none'; | |
| } | |
| }); | |
| // Preset buttons | |
| cloudPresetBtn.addEventListener('click', () => { | |
| currentShaderType = 'cloud'; | |
| loadShader(cloudShaderSource); | |
| // Update UI | |
| cloudParams.style.display = 'block'; | |
| crtParams.style.display = 'none'; | |
| // Update buttons | |
| cloudPresetBtn.classList.add('active'); | |
| crtPresetBtn.classList.remove('active'); | |
| noisePresetBtn.classList.remove('active'); | |
| }); | |
| crtPresetBtn.addEventListener('click', () => { | |
| currentShaderType = 'crt'; | |
| loadShader(crtShaderSource); | |
| // Update UI | |
| cloudParams.style.display = 'none'; | |
| crtParams.style.display = 'block'; | |
| // Update buttons | |
| cloudPresetBtn.classList.remove('active'); | |
| crtPresetBtn.classList.add('active'); | |
| noisePresetBtn.classList.remove('active'); | |
| }); | |
| noisePresetBtn.addEventListener('click', () => { | |
| currentShaderType = 'noise'; | |
| loadShader(noiseShaderSource); | |
| // Update UI | |
| cloudParams.style.display = 'none'; | |
| crtParams.style.display = 'none'; | |
| // Update buttons | |
| cloudPresetBtn.classList.remove('active'); | |
| crtPresetBtn.classList.remove('active'); | |
| noisePresetBtn.classList.add('active'); | |
| }); | |
| // Mouse interaction for camera offset | |
| canvas.addEventListener('mousemove', (e) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| // Normalize coordinates | |
| mouseX = (x / canvas.width) * 2 - 1; | |
| mouseY = (y / canvas.height) * 2 - 1; | |
| // Update display | |
| mousePosition.textContent = `${mouseX.toFixed(2)}, ${mouseY.toFixed(2)}`; | |
| }); | |
| // Mouse drag for camera panning | |
| let isDragging = false; | |
| let lastMouseX, lastMouseY; | |
| canvas.addEventListener('mousedown', (e) => { | |
| isDragging = true; | |
| lastMouseX = e.clientX; | |
| lastMouseY = e.clientY; | |
| }); | |
| window.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| window.addEventListener('mousemove', (e) => { | |
| if (isDragging) { | |
| const deltaX = e.clientX - lastMouseX; | |
| const deltaY = e.clientY - lastMouseY; | |
| cameraOffsetX += deltaX; | |
| cameraOffsetY -= deltaY; // Invert Y for intuitive panning | |
| lastMouseX = e.clientX; | |
| lastMouseY = e.clientY; | |
| } | |
| }); | |
| // Resolution controls | |
| resolutionSelect.addEventListener('change', () => { | |
| if (resolutionSelect.value === 'custom') { | |
| customResolution.style.display = 'block'; | |
| } else { | |
| customResolution.style.display = 'none'; | |
| } | |
| resizeCanvas(); | |
| }); | |
| // Custom resolution inputs | |
| resolutionWidth.addEventListener('change', resizeCanvas); | |
| resolutionHeight.addEventListener('change', resizeCanvas); | |
| // Window resize handler | |
| window.addEventListener('resize', () => { | |
| if (resolutionSelect.value === 'auto') { | |
| resizeCanvas(); | |
| } | |
| }); | |
| } | |
| // Initialize the application | |
| initWebGL(); | |
| initControlListeners(); | |
| </script> | |
| </body> | |
| </html> |