GLSL-Shader-Viewer / index.html
Nymbo's picture
Update index.html
2369c60 verified
raw
history blame
53.3 kB
<!DOCTYPE html>
<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>