201 lines
7.2 KiB
JavaScript
201 lines
7.2 KiB
JavaScript
/*
|
|
To do:
|
|
Press z for zen mode (hides all control and other display on top of the canvas)
|
|
Ability to add this shader effect on top of an image?
|
|
Presets / seed choice??
|
|
Allow user to upload a song, and then it becomes audio reactive?
|
|
Generate perfect loops in x seconds
|
|
*/
|
|
|
|
// Initialize WebGL context
|
|
const canvas = document.getElementById('canvas');
|
|
let startingWidth = window.innerWidth;
|
|
let startingHeight = window.innerHeight;
|
|
canvas.width = startingWidth;
|
|
canvas.height = startingHeight;
|
|
console.log("canvas width/height: "+canvas.width+" / "+canvas.height);
|
|
|
|
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
let isPlaying = false;
|
|
let animationID = null;
|
|
let randomSeed;
|
|
let time;
|
|
let timeOffset = 0;
|
|
|
|
// FPS tracking variables
|
|
let frameCount = 0;
|
|
let lastTime = 0;
|
|
let fps = 0;
|
|
|
|
if (!gl) {
|
|
alert('WebGL not supported');
|
|
}
|
|
|
|
// Compile shaders
|
|
function compileShader(source, type) {
|
|
const shader = gl.createShader(type);
|
|
gl.shaderSource(shader, source);
|
|
gl.compileShader(shader);
|
|
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
console.error('Shader compilation error:', gl.getShaderInfoLog(shader));
|
|
gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
|
|
return shader;
|
|
}
|
|
|
|
// Create program
|
|
const vertexShader = compileShader(document.getElementById('vertexShader').textContent, gl.VERTEX_SHADER);
|
|
const fragmentShader = compileShader(document.getElementById('fragmentShader').textContent, gl.FRAGMENT_SHADER);
|
|
|
|
const program = gl.createProgram();
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.error('Program linking error:', gl.getProgramInfoLog(program));
|
|
}
|
|
|
|
gl.useProgram(program);
|
|
|
|
// Create rectangle covering the entire canvas
|
|
const positionBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
-1.0, -1.0,
|
|
1.0, -1.0,
|
|
-1.0, 1.0,
|
|
1.0, 1.0
|
|
]), gl.STATIC_DRAW);
|
|
|
|
// Set up attributes and uniforms
|
|
const positionLocation = gl.getAttribLocation(program, 'position');
|
|
gl.enableVertexAttribArray(positionLocation);
|
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
|
|
const timeLocation = gl.getUniformLocation(program, 'time');
|
|
const resolutionLocation = gl.getUniformLocation(program, 'resolution');
|
|
const seedLocation = gl.getUniformLocation(program, 'seed');
|
|
|
|
// GUI-controlled uniform locations
|
|
const timeScaleLocation = gl.getUniformLocation(program, 'timeScale');
|
|
const bloomStrengthLocation = gl.getUniformLocation(program, 'bloomStrength');
|
|
const saturationLocation = gl.getUniformLocation(program, 'saturation');
|
|
const grainAmountLocation = gl.getUniformLocation(program, 'grainAmount');
|
|
const colorTintLocation = gl.getUniformLocation(program, 'colorTint');
|
|
const minCircleSizeLocation = gl.getUniformLocation(program, 'minCircleSize');
|
|
const circleStrengthLocation = gl.getUniformLocation(program, 'circleStrength');
|
|
const distortXLocation = gl.getUniformLocation(program, 'distortX');
|
|
const distortYLocation = gl.getUniformLocation(program, 'distortY');
|
|
|
|
const patternAmpLocation = gl.getUniformLocation(program, 'patternAmp');
|
|
const patternFreqLocation = gl.getUniformLocation(program, 'patternFreq');
|
|
|
|
// Initialize parameters object for dat.gui
|
|
const params = {
|
|
canvasWidth: startingWidth,
|
|
canvasHeight: startingHeight,
|
|
timeScale: .666,
|
|
patternAmp: 2,
|
|
patternFreq: 0.4,
|
|
bloomStrength: 0.777,
|
|
saturation: 1.74,
|
|
grainAmount: 0.161,
|
|
colorTintR: 1.5,
|
|
colorTintG: 1.0,
|
|
colorTintB: 1.0,
|
|
minCircleSize: 2.8,
|
|
circleStrength: 0,
|
|
distortX: 1,
|
|
distortY: 1,
|
|
};
|
|
|
|
// Also refresh on page load
|
|
window.addEventListener('load', refreshPattern);
|
|
window.addEventListener('resize', updateCanvasSize);
|
|
|
|
// Initialize dat.gui
|
|
const gui = new dat.GUI({ autoplace: false });
|
|
gui.close();
|
|
|
|
// Add GUI controls with folders for organization
|
|
const canvasFolder = gui.addFolder('Canvas Size');
|
|
canvasFolder.add(params, 'canvasWidth', 100, 4000).step(10).name('Width').onChange(updateCanvasSize);
|
|
canvasFolder.add(params, 'canvasHeight', 100, 4000).step(10).name('Height').onChange(updateCanvasSize);
|
|
canvasFolder.open();
|
|
|
|
const timeFolder = gui.addFolder('Animation');
|
|
timeFolder.add(params, 'timeScale', 0.1, 3.0).name('Speed').onChange(updateUniforms);
|
|
timeFolder.open();
|
|
|
|
const patternFolder = gui.addFolder('Pattern');
|
|
patternFolder.add(params, 'patternAmp', 1.0, 50.0).step(0.1).name('Pattern Amp').onChange(updateUniforms);
|
|
patternFolder.add(params, 'patternFreq', 0.2, 10.0).step(0.1).name('Pattern Freq').onChange(updateUniforms);
|
|
patternFolder.open();
|
|
|
|
const visualFolder = gui.addFolder('Visual Effects');
|
|
visualFolder.add(params, 'bloomStrength', 0.0, 5.0).name('Bloom').onChange(updateUniforms);
|
|
visualFolder.add(params, 'saturation', 0.0, 2.0).name('Saturation').onChange(updateUniforms);
|
|
visualFolder.add(params, 'grainAmount', 0.0, 0.5).name('Grain').onChange(updateUniforms);
|
|
visualFolder.add(params, 'minCircleSize', 0.0, 10.0).name('Circle Size').onChange(updateUniforms);
|
|
visualFolder.add(params, 'circleStrength', 0.0, 3.0).name('Circle Strength').onChange(updateUniforms);
|
|
visualFolder.add(params, 'distortX', 0.0, 50.0).name('Distort-X').onChange(updateUniforms);
|
|
visualFolder.add(params, 'distortY', 0.0, 50.0).name('Distort-Y').onChange(updateUniforms);
|
|
|
|
visualFolder.open();
|
|
|
|
const colorFolder = gui.addFolder('Color Tint');
|
|
colorFolder.add(params, 'colorTintR', 0.0, 1.5).name('Red').onChange(updateUniforms);
|
|
colorFolder.add(params, 'colorTintG', 0.0, 1.5).name('Green').onChange(updateUniforms);
|
|
colorFolder.add(params, 'colorTintB', 0.0, 1.5).name('Blue').onChange(updateUniforms);
|
|
colorFolder.open();
|
|
|
|
// Function to update shader uniforms from GUI values
|
|
function updateUniforms() {
|
|
gl.uniform1f(timeScaleLocation, params.timeScale);
|
|
gl.uniform1f(patternAmpLocation, params.patternAmp);
|
|
gl.uniform1f(patternFreqLocation, params.patternFreq);
|
|
gl.uniform1f(bloomStrengthLocation, params.bloomStrength);
|
|
gl.uniform1f(saturationLocation, params.saturation);
|
|
gl.uniform1f(grainAmountLocation, params.grainAmount);
|
|
gl.uniform3f(colorTintLocation, params.colorTintR, params.colorTintG, params.colorTintB);
|
|
gl.uniform1f(minCircleSizeLocation, params.minCircleSize);
|
|
gl.uniform1f(circleStrengthLocation, params.circleStrength);
|
|
gl.uniform1f(distortXLocation, params.distortX);
|
|
gl.uniform1f(distortYLocation, params.distortY);
|
|
}
|
|
|
|
function drawScene(){
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
// Animation loop
|
|
function render(timestamp) {
|
|
if (isPlaying) {
|
|
// Calculate adjusted time by subtracting the offset
|
|
const adjustedTime = timestamp - timeOffset;
|
|
time = timestamp;
|
|
|
|
const timeInSeconds = adjustedTime * 0.0035;
|
|
gl.uniform1f(timeLocation, timeInSeconds);
|
|
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
|
|
|
|
|
|
// If video recording is ongoing, drawScene is called already
|
|
if (!recordVideoState || useMobileRecord) {
|
|
drawScene();
|
|
}
|
|
|
|
animationID = requestAnimationFrame(render);
|
|
}
|
|
}
|
|
|
|
// Start the animation loop
|
|
isPlaying = true;
|
|
refreshPattern();
|
|
updateUniforms();
|
|
animationID = requestAnimationFrame(render);
|