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> |