|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Animated Fractal Shader Playground</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
font-family: 'Monaco', monospace; |
|
} |
|
|
|
body { |
|
background: #1a1a1a; |
|
color: #fff; |
|
display: flex; |
|
min-height: 100vh; |
|
} |
|
|
|
.controls-panel { |
|
width: 350px; |
|
background: #252525; |
|
padding: 20px; |
|
overflow-y: auto; |
|
} |
|
|
|
.main-panel { |
|
flex: 1; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
canvas { |
|
width: 100%; |
|
flex: 1; |
|
background: #000; |
|
} |
|
|
|
.control-group { |
|
margin-bottom: 20px; |
|
border: 1px solid #333; |
|
padding: 10px; |
|
border-radius: 4px; |
|
background: #1f1f1f; |
|
} |
|
|
|
.control-group h3 { |
|
margin-bottom: 10px; |
|
color: #0f0; |
|
font-size: 14px; |
|
text-transform: uppercase; |
|
} |
|
|
|
.control-item { |
|
margin-bottom: 12px; |
|
} |
|
|
|
label { |
|
display: block; |
|
font-size: 12px; |
|
color: #aaa; |
|
margin-bottom: 4px; |
|
} |
|
|
|
input[type="range"] { |
|
width: 100%; |
|
background: #333; |
|
height: 6px; |
|
-webkit-appearance: none; |
|
border-radius: 3px; |
|
} |
|
|
|
input[type="range"]::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 15px; |
|
height: 15px; |
|
background: #0f0; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
input[type="color"] { |
|
width: 100%; |
|
height: 30px; |
|
border: none; |
|
background: #333; |
|
cursor: pointer; |
|
} |
|
|
|
select, textarea { |
|
width: 100%; |
|
background: #333; |
|
color: #fff; |
|
padding: 5px; |
|
border: 1px solid #444; |
|
border-radius: 3px; |
|
} |
|
|
|
textarea { |
|
height: 100px; |
|
font-size: 12px; |
|
resize: vertical; |
|
font-family: monospace; |
|
} |
|
|
|
.preset-btn { |
|
padding: 8px; |
|
background: #2a2a2a; |
|
border: 1px solid #444; |
|
color: #0f0; |
|
width: 100%; |
|
margin: 2px 0; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
border-radius: 3px; |
|
} |
|
|
|
.preset-btn:hover { |
|
background: #333; |
|
border-color: #0f0; |
|
} |
|
|
|
.coordinates { |
|
position: fixed; |
|
bottom: 10px; |
|
right: 10px; |
|
background: rgba(0,0,0,0.8); |
|
padding: 8px 12px; |
|
border-radius: 4px; |
|
font-size: 12px; |
|
color: #0f0; |
|
border: 1px solid #333; |
|
} |
|
|
|
.checkbox-group { |
|
display: flex; |
|
gap: 10px; |
|
align-items: center; |
|
} |
|
|
|
.checkbox-group input[type="checkbox"] { |
|
width: 16px; |
|
height: 16px; |
|
cursor: pointer; |
|
} |
|
|
|
.animation-controls { |
|
display: flex; |
|
gap: 10px; |
|
margin-top: 10px; |
|
} |
|
|
|
.animation-btn { |
|
flex: 1; |
|
padding: 8px; |
|
background: #333; |
|
border: 1px solid #444; |
|
color: #0f0; |
|
cursor: pointer; |
|
border-radius: 3px; |
|
transition: all 0.3s; |
|
} |
|
|
|
.animation-btn:hover { |
|
background: #444; |
|
border-color: #0f0; |
|
} |
|
|
|
.stats { |
|
position: fixed; |
|
top: 10px; |
|
right: 10px; |
|
background: rgba(0,0,0,0.8); |
|
padding: 8px; |
|
border-radius: 4px; |
|
font-size: 12px; |
|
color: #0f0; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="controls-panel" id="controls"> |
|
<div class="control-group"> |
|
<h3>Fractal Type</h3> |
|
<select id="fractalType" onchange="updateShader()"> |
|
<option value="mandelbrot">Mandelbrot Set</option> |
|
<option value="julia">Julia Set</option> |
|
<option value="burningShip">Burning Ship</option> |
|
<option value="tricorn">Tricorn</option> |
|
<option value="custom">Custom Function</option> |
|
</select> |
|
</div> |
|
|
|
<div class="control-group" id="customFunctionGroup" style="display:none"> |
|
<h3>Custom Function</h3> |
|
<textarea id="customFunction">z = vec2( |
|
z.x * z.x - z.y * z.y, |
|
2.0 * z.x * z.y |
|
) + c;</textarea> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3>Animation</h3> |
|
<div class="control-item"> |
|
<label>Animation Speed</label> |
|
<input type="range" id="animSpeed" min="0" max="2" step="0.01" value="0.5"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Motion Pattern</label> |
|
<select id="motionPattern"> |
|
<option value="orbit">Orbital</option> |
|
<option value="wave">Wave</option> |
|
<option value="spiral">Spiral</option> |
|
<option value="bounce">Bounce</option> |
|
</select> |
|
</div> |
|
<div class="checkbox-group"> |
|
<input type="checkbox" id="animateColors" checked> |
|
<label>Animate Colors</label> |
|
</div> |
|
<div class="animation-controls"> |
|
<button class="animation-btn" id="playPause">Pause</button> |
|
<button class="animation-btn" id="reset">Reset</button> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3>Navigation</h3> |
|
<div class="control-item"> |
|
<label>Zoom</label> |
|
<input type="range" id="zoom" min="-2" max="2" step="0.01" value="0"> |
|
</div> |
|
<div class="control-item"> |
|
<label>X Position</label> |
|
<input type="range" id="xPos" min="-2" max="2" step="0.01" value="0"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Y Position</label> |
|
<input type="range" id="yPos" min="-2" max="2" step="0.01" value="0"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Rotation</label> |
|
<input type="range" id="rotation" min="0" max="360" step="1" value="0"> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3>Iteration Settings</h3> |
|
<div class="control-item"> |
|
<label>Max Iterations</label> |
|
<input type="range" id="maxIterations" min="10" max="1000" step="10" value="100"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Escape Radius</label> |
|
<input type="range" id="escapeRadius" min="2" max="20" step="0.5" value="4"> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3>Color Settings</h3> |
|
<div class="control-item"> |
|
<label>Color Scheme</label> |
|
<select id="colorScheme"> |
|
<option value="smooth">Smooth</option> |
|
<option value="bands">Color Bands</option> |
|
<option value="hsv">HSV Cycle</option> |
|
<option value="rainbow">Rainbow</option> |
|
<option value="fire">Fire</option> |
|
</select> |
|
</div> |
|
<div class="control-item"> |
|
<label>Primary Color</label> |
|
<input type="color" id="color1" value="#00ff00"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Secondary Color</label> |
|
<input type="color" id="color2" value="#0000ff"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Color Bands</label> |
|
<input type="range" id="colorBands" min="1" max="20" step="1" value="5"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Color Intensity</label> |
|
<input type="range" id="colorIntensity" min="0.1" max="2" step="0.1" value="1"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Color Speed</label> |
|
<input type="range" id="colorSpeed" min="0" max="2" step="0.1" value="1"> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3>Effects</h3> |
|
<div class="control-item"> |
|
<label>Distortion</label> |
|
<input type="range" id="distortion" min="0" max="1" step="0.01" value="0"> |
|
</div> |
|
<div class="control-item"> |
|
<label>Glow Intensity</label> |
|
<input type="range" id="glow" min="0" max="1" step="0.01" value="0.2"> |
|
</div> |
|
<div class="checkbox-group"> |
|
<input type="checkbox" id="symmetry"> |
|
<label>Enable Symmetry</label> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3>Presets</h3> |
|
<button class="preset-btn" onclick="loadPreset('classic')">Classic</button> |
|
<button class="preset-btn" onclick="loadPreset('psychedelic')">Psychedelic</button> |
|
<button class="preset-btn" onclick="loadPreset('cosmic')">Cosmic</button> |
|
<button class="preset-btn" onclick="loadPreset('fire')">Fire Dance</button> |
|
<button class="preset-btn" onclick="loadPreset('vortex')">Vortex</button> |
|
</div> |
|
</div> |
|
|
|
<div class="main-panel"> |
|
<canvas id="canvas"></canvas> |
|
<div class="coordinates" id="coords"></div> |
|
<div class="stats" id="stats"></div> |
|
</div> |
|
|
|
<script> |
|
|
|
const fragmentShaderSource = ` |
|
precision highp float; |
|
|
|
uniform vec2 resolution; |
|
uniform vec2 position; |
|
uniform float zoom; |
|
uniform float rotation; |
|
uniform float time; |
|
uniform int maxIterations; |
|
uniform float escapeRadius; |
|
uniform vec3 color1; |
|
uniform vec3 color2; |
|
uniform float colorIntensity; |
|
uniform int colorScheme; |
|
uniform float colorBands; |
|
uniform float distortion; |
|
uniform float glow; |
|
uniform bool symmetry; |
|
uniform int fractalType; |
|
uniform int motionPattern; |
|
uniform float animSpeed; |
|
uniform bool animateColors; |
|
uniform float colorSpeed; |
|
|
|
vec2 rotate(vec2 p, float angle) { |
|
float c = cos(angle); |
|
float s = sin(angle); |
|
return vec2( |
|
p.x * c - p.y * s, |
|
p.x * s + p.y * c |
|
); |
|
} |
|
|
|
vec2 complexMul(vec2 a, vec2 b) { |
|
return vec2( |
|
a.x * b.x - a.y * b.y, |
|
a.x * b.y + a.y * b.x |
|
); |
|
} |
|
|
|
vec2 complexDiv(vec2 a, vec2 b) { |
|
float denom = b.x * b.x + b.y * b.y; |
|
return vec2( |
|
(a.x * b.x + a.y * b.y) / denom, |
|
(a.y * b.x - a.x * b.y) / denom |
|
); |
|
} |
|
|
|
vec3 hsv2rgb(vec3 c) { |
|
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); |
|
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); |
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); |
|
} |
|
|
|
vec2 getMotionOffset() { |
|
if(motionPattern == 0) { // Orbit |
|
return vec2( |
|
cos(time * animSpeed), |
|
sin(time * animSpeed) |
|
) * 0.1; |
|
} else if(motionPattern == 1) { // Wave |
|
return vec2( |
|
sin(time * animSpeed) * 0.1, |
|
cos(time * animSpeed * 2.0) * 0.1 |
|
); |
|
} else if(motionPattern == 2) { // Spiral |
|
float t = time * animSpeed; |
|
return vec2( |
|
cos(t) * t * 0.01, |
|
sin(t) * t * 0.01 |
|
); |
|
} else { // Bounce |
|
return vec2( |
|
abs(sin(time * animSpeed)), |
|
abs(cos(time * animSpeed)) |
|
) * 0.1; |
|
} |
|
} |
|
|
|
vec3 getColor(float iter, vec2 z) { |
|
float smoothed = iter + 1.0 - log(log(length(z))) / log(2.0); |
|
float normalized = smoothed / float(maxIterations); |
|
|
|
if(animateColors) { |
|
normalized = mod(normalized + time * colorSpeed, 1.0); |
|
} |
|
|
|
if(colorScheme == 0) { // Smooth |
|
return mix(color1, color2, normalized) * colorIntensity; |
|
} else if(colorScheme == 1) { // Bands |
|
return mix(color1, color2, mod(normalized * colorBands, 1.0)) * colorIntensity; |
|
} else if(colorScheme == 2) { // HSV |
|
return hsv2rgb(vec3(normalized, 1.0, 1.0)) * colorIntensity; |
|
} else if(colorScheme == 3) { // Rainbow |
|
return vec3( |
|
0.5 + 0.5 * sin(normalized * 6.28318), |
|
0.5 + 0.5 * sin(normalized * 6.28318 + 2.09439), |
|
0.5 + 0.5 * sin(normalized * 6.28318 + 4.18879) |
|
) * colorIntensity; |
|
} else { // Fire |
|
return vec3( |
|
normalized * 1.5, |
|
pow(normalized, 2.0), |
|
pow(normalized, 4.0) |
|
) * colorIntensity; |
|
} |
|
} |
|
|
|
void main() { |
|
vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / min(resolution.x, resolution.y); |
|
uv = rotate(uv, rotation * 0.0174533); |
|
|
|
vec2 offset = getMotionOffset(); |
|
vec2 c = uv * pow(2.0, zoom) + position + offset; |
|
|
|
if(symmetry) { |
|
c = abs(c); |
|
} |
|
|
|
vec2 z = fractalType == 1 ? uv : c; |
|
vec2 juliaC = fractalType == 1 ? vec2(0.285, 0.01) + offset : c; |
|
|
|
float iter = 0.0; |
|
|
|
for(int i = 0; i < 1000; i++) { |
|
if(i >= maxIterations) break; |
|
|
|
if(fractalType == 0 || fractalType == 1) { |
|
z = complexMul(z, z) + juliaC; |
|
} else if(fractalType == 2) { |
|
z = complexMul(abs(z), abs(z)) + c; |
|
} else if(fractalType == 3) { |
|
z = vec2(z.x, -z.y); |
|
z = complexMul(z, z) + c; |
|
} |
|
|
|
// Apply distortion |
|
z += vec2(sin(z.y * distortion), cos(z.x * distortion)) * distortion; |
|
|
|
if(length(z) > escapeRadius) { |
|
vec3 color = getColor(iter, z); |
|
|
|
// Apply glow effect |
|
float glowFactor = 1.0 + glow * (1.0 - iter / float(maxIterations)); |
|
color *= glowFactor; |
|
|
|
gl_FragColor = vec4(color, 1.0); |
|
return; |
|
} |
|
iter += 1.0; |
|
} |
|
|
|
gl_FragColor = vec4(vec3(0.0), 1.0); |
|
} |
|
`; |
|
|
|
let gl, program; |
|
let isAnimating = true; |
|
let startTime = Date.now(); |
|
let frameCount = 0; |
|
let lastFpsUpdate = startTime; |
|
const uniforms = {}; |
|
|
|
function initGL() { |
|
const canvas = document.querySelector('canvas'); |
|
canvas.width = canvas.clientWidth; |
|
canvas.height = canvas.clientHeight; |
|
|
|
gl = canvas.getContext('webgl'); |
|
if (!gl) { |
|
alert('WebGL not supported'); |
|
return; |
|
} |
|
|
|
|
|
const vertexShader = gl.createShader(gl.VERTEX_SHADER); |
|
gl.shaderSource(vertexShader, ` |
|
attribute vec2 position; |
|
void main() { |
|
gl_Position = vec4(position, 0.0, 1.0); |
|
} |
|
`); |
|
gl.compileShader(vertexShader); |
|
|
|
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); |
|
gl.shaderSource(fragmentShader, fragmentShaderSource); |
|
gl.compileShader(fragmentShader); |
|
|
|
|
|
program = gl.createProgram(); |
|
gl.attachShader(program, vertexShader); |
|
gl.attachShader(program, fragmentShader); |
|
gl.linkProgram(program); |
|
|
|
|
|
const vertices = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]); |
|
const buffer = gl.createBuffer(); |
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); |
|
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); |
|
|
|
const positionLocation = gl.getAttribLocation(program, 'position'); |
|
gl.enableVertexAttribArray(positionLocation); |
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); |
|
|
|
|
|
const uniformNames = [ |
|
'resolution', 'position', 'zoom', 'rotation', 'time', |
|
'maxIterations', 'escapeRadius', 'color1', 'color2', |
|
'colorIntensity', 'colorScheme', 'colorBands', 'distortion', |
|
'glow', 'symmetry', 'fractalType', 'motionPattern', |
|
'animSpeed', 'animateColors', 'colorSpeed' |
|
]; |
|
|
|
uniformNames.forEach(name => { |
|
uniforms[name] = gl.getUniformLocation(program, name); |
|
}); |
|
} |
|
|
|
function hexToRGB(hex) { |
|
const r = parseInt(hex.substr(1,2), 16) / 255; |
|
const g = parseInt(hex.substr(3,2), 16) / 255; |
|
const b = parseInt(hex.substr(5,2), 16) / 255; |
|
return [r, g, b]; |
|
} |
|
|
|
function updateShader() { |
|
if (!isAnimating) return; |
|
|
|
gl.useProgram(program); |
|
|
|
|
|
const time = (Date.now() - startTime) / 1000; |
|
gl.uniform2f(uniforms.resolution, canvas.width, canvas.height); |
|
gl.uniform2f(uniforms.position, |
|
parseFloat(document.getElementById('xPos').value), |
|
parseFloat(document.getElementById('yPos').value) |
|
); |
|
gl.uniform1f(uniforms.zoom, parseFloat(document.getElementById('zoom').value)); |
|
gl.uniform1f(uniforms.rotation, parseFloat(document.getElementById('rotation').value)); |
|
gl.uniform1f(uniforms.time, time); |
|
gl.uniform1i(uniforms.maxIterations, parseInt(document.getElementById('maxIterations').value)); |
|
gl.uniform1f(uniforms.escapeRadius, parseFloat(document.getElementById('escapeRadius').value)); |
|
|
|
const color1 = hexToRGB(document.getElementById('color1').value); |
|
const color2 = hexToRGB(document.getElementById('color2').value); |
|
gl.uniform3f(uniforms.color1, color1[0], color1[1], color1[2]); |
|
gl.uniform3f(uniforms.color2, color2[0], color2[1], color2[2]); |
|
|
|
gl.uniform1f(uniforms.colorIntensity, parseFloat(document.getElementById('colorIntensity').value)); |
|
gl.uniform1i(uniforms.colorScheme, document.getElementById('colorScheme').selectedIndex); |
|
gl.uniform1f(uniforms.colorBands, parseFloat(document.getElementById('colorBands').value)); |
|
gl.uniform1f(uniforms.distortion, parseFloat(document.getElementById('distortion').value)); |
|
gl.uniform1f(uniforms.glow, parseFloat(document.getElementById('glow').value)); |
|
gl.uniform1i(uniforms.fractalType, document.getElementById('fractalType').selectedIndex); |
|
gl.uniform1i(uniforms.motionPattern, document.getElementById('motionPattern').selectedIndex); |
|
gl.uniform1f(uniforms.animSpeed, parseFloat(document.getElementById('animSpeed').value)); |
|
gl.uniform1f(uniforms.colorSpeed, parseFloat(document.getElementById('colorSpeed').value)); |
|
|
|
gl.uniform1i(uniforms.symmetry, document.getElementById('symmetry').checked); |
|
gl.uniform1i(uniforms.animateColors, document.getElementById('animateColors').checked); |
|
|
|
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); |
|
|
|
|
|
frameCount++; |
|
const currentTime = Date.now(); |
|
if (currentTime - lastFpsUpdate >= 1000) { |
|
const fps = Math.round((frameCount * 1000) / (currentTime - lastFpsUpdate)); |
|
document.getElementById('stats').textContent = `FPS: ${fps}`; |
|
frameCount = 0; |
|
lastFpsUpdate = currentTime; |
|
} |
|
|
|
|
|
if (isAnimating) { |
|
requestAnimationFrame(updateShader); |
|
} |
|
} |
|
|
|
const presets = { |
|
classic: { |
|
fractalType: 'mandelbrot', |
|
zoom: 0, |
|
colorScheme: 'smooth', |
|
animSpeed: 0.5, |
|
motionPattern: 'orbit', |
|
distortion: 0, |
|
glow: 0.2, |
|
colorIntensity: 1 |
|
}, |
|
psychedelic: { |
|
fractalType: 'julia', |
|
zoom: 0.5, |
|
colorScheme: 'rainbow', |
|
animSpeed: 1, |
|
motionPattern: 'spiral', |
|
distortion: 0.3, |
|
glow: 0.5, |
|
colorIntensity: 1.5 |
|
}, |
|
cosmic: { |
|
fractalType: 'tricorn', |
|
zoom: 0.2, |
|
colorScheme: 'hsv', |
|
animSpeed: 0.8, |
|
motionPattern: 'wave', |
|
distortion: 0.1, |
|
glow: 0.4, |
|
colorIntensity: 1.2 |
|
}, |
|
fire: { |
|
fractalType: 'burningShip', |
|
zoom: 0.3, |
|
colorScheme: 'fire', |
|
animSpeed: 1.2, |
|
motionPattern: 'bounce', |
|
distortion: 0.2, |
|
glow: 0.6, |
|
colorIntensity: 1.8 |
|
}, |
|
vortex: { |
|
fractalType: 'julia', |
|
zoom: 0.1, |
|
colorScheme: 'bands', |
|
animSpeed: 1.5, |
|
motionPattern: 'spiral', |
|
distortion: 0.4, |
|
glow: 0.3, |
|
colorIntensity: 1.4 |
|
} |
|
}; |
|
|
|
function loadPreset(name) { |
|
const preset = presets[name]; |
|
Object.keys(preset).forEach(key => { |
|
const element = document.getElementById(key); |
|
if (element) { |
|
element.value = preset[key]; |
|
} |
|
}); |
|
} |
|
|
|
|
|
document.getElementById('playPause').addEventListener('click', function() { |
|
isAnimating = !isAnimating; |
|
this.textContent = isAnimating ? 'Pause' : 'Play'; |
|
if (isAnimating) { |
|
startTime = Date.now() - (parseFloat(document.getElementById('time').value) || 0) * 1000; |
|
updateShader(); |
|
} |
|
}); |
|
|
|
document.getElementById('reset').addEventListener('click', function() { |
|
startTime = Date.now(); |
|
if (!isAnimating) { |
|
isAnimating = true; |
|
document.getElementById('playPause').textContent = 'Pause'; |
|
updateShader(); |
|
} |
|
}); |
|
|
|
document.getElementById('fractalType').addEventListener('change', function() { |
|
document.getElementById('customFunctionGroup').style.display = |
|
this.value === 'custom' ? 'block' : 'none'; |
|
}); |
|
|
|
|
|
let isDragging = false; |
|
let lastMouseX, lastMouseY; |
|
|
|
canvas.addEventListener('mousedown', (e) => { |
|
isDragging = true; |
|
lastMouseX = e.clientX; |
|
lastMouseY = e.clientY; |
|
}); |
|
|
|
canvas.addEventListener('mousemove', (e) => { |
|
if (!isDragging) { |
|
const rect = canvas.getBoundingClientRect(); |
|
const x = (e.clientX - rect.left) / canvas.width; |
|
const y = (e.clientY - rect.top) / canvas.height; |
|
const zoom = Math.pow(2.0, parseFloat(document.getElementById('zoom').value)); |
|
const xPos = parseFloat(document.getElementById('xPos').value); |
|
const yPos = parseFloat(document.getElementById('yPos').value); |
|
|
|
const coordX = (x - 0.5) * 4 / zoom + xPos; |
|
const coordY = (0.5 - y) * 4 / zoom + yPos; |
|
|
|
document.getElementById('coords').textContent = |
|
`x: ${coordX.toFixed(6)}, y: ${coordY.toFixed(6)}`; |
|
return; |
|
} |
|
|
|
const deltaX = (e.clientX - lastMouseX) / canvas.width; |
|
const deltaY = (e.clientY - lastMouseY) / canvas.height; |
|
const zoom = Math.pow(2.0, parseFloat(document.getElementById('zoom').value)); |
|
|
|
document.getElementById('xPos').value = |
|
parseFloat(document.getElementById('xPos').value) - deltaX * 4 / zoom; |
|
document.getElementById('yPos').value = |
|
parseFloat(document.getElementById('yPos').value) + deltaY * 4 / zoom; |
|
|
|
lastMouseX = e.clientX; |
|
lastMouseY = e.clientY; |
|
}); |
|
|
|
canvas.addEventListener('mouseup', () => isDragging = false); |
|
canvas.addEventListener('mouseleave', () => isDragging = false); |
|
|
|
canvas.addEventListener('wheel', (e) => { |
|
e.preventDefault(); |
|
const delta = e.deltaY > 0 ? -0.1 : 0.1; |
|
document.getElementById('zoom').value = |
|
parseFloat(document.getElementById('zoom').value) + delta; |
|
}); |
|
|
|
|
|
initGL(); |
|
updateShader(); |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
canvas.width = canvas.clientWidth; |
|
canvas.height = canvas.clientHeight; |
|
gl.viewport(0, 0, canvas.width, canvas.height); |
|
}); |
|
</script> |
|
</body> |
|
</html> |