Move your mouse(or your finger) over the applet to see a shift in the pattern. Refresh the page for a new pattern.
As in the previous post, almost everything here happens in the fragment shader. The animation you see is a cycling of a fixed number of colours, something that used to be called palette animation. In the fragment shader this is achieved by passing a different value of the uniform
u_shift
every time the pattern is rendered.
#version 300 es
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
out vec4 out_color;
uniform float u_rfreq;
uniform float u_gfreq;
uniform float u_bfreq;
uniform float u_rphase;
uniform float u_gphase;
uniform float u_bphase;
uniform float u_shift;
uniform float u_mouse_x;
uniform float u_mouse_y;
void main() {
float x = gl_FragCoord.x;
float y = gl_FragCoord.y;
vec2 xy = gl_FragCoord.xy;
vec2 xy_minus_mouse = xy - vec2(u_mouse_x, u_mouse_y);
float param = floor(512.0 + 128.0*sin(xy_minus_mouse.x/12.0) +
128.0*sin(xy_minus_mouse.y/35.0) + 128.0*sin(length(xy_minus_mouse)/8.0)
+ 128.0*sin(length(xy)/10.0) )/4.0;
param = mod(param + u_shift, 256.0);
float r = 0.5 + 0.5*sin(u_rfreq*param + u_rphase );
float g = 0.5 + 0.5*sin(u_gfreq*param + u_gphase );
float b = 0.5 + 0.5*sin(u_bfreq*param + u_bphase );
out_color = vec4(r, g, b, 1.0);
}
Essentially, what you are seeing is an interference pattern, which is what you get when you superpose a bunch of functions. See the line above that calculates a value for the variable param
. The net result of the superposition is arranged to be in the range 0 - 256 for every coordinate on the screen. Why? Because the maximum of the range is a multiple of the number of colours that we are cycling over, here, a multiple of 1 since that's the number of colours that we are cycling over. You can choose other multiples too and suitably change the formula that calculates param
. This formula, by the way, is not original except for a minor modification to make it depend on the mouse position. Here is the resource that this post is mostly based on.
To get a set of colours that repeat cyclically over param
we calculate their r
, g
, and b
components using a periodic function, here, again a function. The red, green, and blue frequencies, u_rfreq
, u_gfreq
, and u_bfreq
are random(in a range of 1-5) multiples of the fundamental frequency which tells us that we have 256 distinct colours at most. In addition to making the component frequencies random, their constant phases u_rphase
, u_gphase
, and u_bphase
are also chosen randomly in a range of 0 - .
function initGL() {
...
const two_pi = Math.PI * 2;
_gl.uniform1f(u_rfreq, (two_pi / 256) * rnd(1, 5));
_gl.uniform1f(u_gfreq, (two_pi / 256) * rnd(1, 5));
_gl.uniform1f(u_bfreq, (two_pi / 256) * rnd(1, 5));
_gl.uniform1f(u_rphase, two_pi * Math.random());
_gl.uniform1f(u_gphase, two_pi * Math.random());
_gl.uniform1f(u_bphase, two_pi * Math.random());
...
}
Here is the entire code:
const _v_shader_src = `#version 300 es
in vec2 a_coords;
void main() {
gl_Position = vec4(a_coords, 0.0, 1.0);
}`;
const _f_shader_src = `#version 300 es
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
out vec4 out_color;
uniform float u_rfreq;
uniform float u_gfreq;
uniform float u_bfreq;
uniform float u_rphase;
uniform float u_gphase;
uniform float u_bphase;
uniform float u_shift;
uniform float u_mouse_x;
uniform float u_mouse_y;
void main() {
float x = gl_FragCoord.x;
float y = gl_FragCoord.y;
vec2 xy = gl_FragCoord.xy;
vec2 xy_minus_mouse = xy - vec2(u_mouse_x, u_mouse_y);
float param = floor(512.0 + 128.0*sin(xy_minus_mouse.x/12.0) +
128.0*sin(xy_minus_mouse.y/35.0) + 128.0*sin(length(xy_minus_mouse)/8.0)
+ 128.0*sin(length(xy)/10.0) )/4.0;
param = mod(param + u_shift, 256.0);
float r = 0.5 + 0.5*sin(u_rfreq*param + u_rphase );
float g = 0.5 + 0.5*sin(u_gfreq*param + u_gphase );
float b = 0.5 + 0.5*sin(u_bfreq*param + u_bphase );
out_color = vec4(r, g, b, 1.0);
}`;
let _canvas_div;
let _canvas;
let _gl;
let _shift, _mouse_x, _mouse_y;
let _u_shift;
let _u_mouse_x, _u_mouse_y;
let _anim_request;
const rnd = (min, max) => min + Math.floor(Math.random() * (max + 1 - min));
function createProgram() {
const v_shader = _gl.createShader(_gl.VERTEX_SHADER);
_gl.shaderSource(v_shader, _v_shader_src);
_gl.compileShader(v_shader);
if (!_gl.getShaderParameter(v_shader, _gl.COMPILE_STATUS)) {
throw new Error(
`Error in vertex shader: ${_gl.getShaderInfoLog(v_shader)}`
);
}
const f_shader = _gl.createShader(_gl.FRAGMENT_SHADER);
_gl.shaderSource(f_shader, _f_shader_src);
_gl.compileShader(f_shader);
if (!_gl.getShaderParameter(f_shader, _gl.COMPILE_STATUS)) {
throw new Error(
`Error in fragment shader: ${_gl.getShaderInfoLog(f_shader)}`
);
}
const prog = _gl.createProgram();
_gl.attachShader(prog, v_shader);
_gl.attachShader(prog, f_shader);
_gl.linkProgram(prog);
if (!_gl.getProgramParameter(prog, _gl.LINK_STATUS)) {
throw new Error(`Link error in program:${_gl.getProgramInfoLog(prog)}`);
}
return prog;
}
function initGL() {
const prog = createProgram();
const a_coords = _gl.getAttribLocation(prog, 'a_coords');
const coords_buf = _gl.createBuffer();
const coords = new Float32Array([-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0]);
_gl.bindBuffer(_gl.ARRAY_BUFFER, coords_buf);
_gl.bufferData(_gl.ARRAY_BUFFER, coords, _gl.STREAM_DRAW);
_gl.enableVertexAttribArray(a_coords);
_gl.vertexAttribPointer(a_coords, 2, _gl.FLOAT, false, 0, 0);
const u_rfreq = _gl.getUniformLocation(prog, 'u_rfreq');
const u_gfreq = _gl.getUniformLocation(prog, 'u_gfreq');
const u_bfreq = _gl.getUniformLocation(prog, 'u_bfreq');
const u_rphase = _gl.getUniformLocation(prog, 'u_rphase');
const u_gphase = _gl.getUniformLocation(prog, 'u_gphase');
const u_bphase = _gl.getUniformLocation(prog, 'u_bphase');
_u_shift = _gl.getUniformLocation(prog, 'u_shift');
_u_mouse_x = _gl.getUniformLocation(prog, 'u_mouse_x');
_u_mouse_y = _gl.getUniformLocation(prog, 'u_mouse_y');
_gl.viewport(0, 0, _canvas.width, _canvas.height);
_gl.useProgram(prog);
const two_pi = Math.PI * 2;
_gl.uniform1f(u_rfreq, (two_pi / 256) * rnd(1, 5));
_gl.uniform1f(u_gfreq, (two_pi / 256) * rnd(1, 5));
_gl.uniform1f(u_bfreq, (two_pi / 256) * rnd(1, 5));
_gl.uniform1f(u_rphase, two_pi * Math.random());
_gl.uniform1f(u_gphase, two_pi * Math.random());
_gl.uniform1f(u_bphase, two_pi * Math.random());
_shift = 0.0;
_mouse_x = 0.0;
_mouse_y = 0.0;
}
function draw() {
_gl.uniform1f(_u_shift, _shift);
_gl.uniform1f(_u_mouse_x, _mouse_x);
_gl.uniform1f(_u_mouse_y, _canvas.height - _mouse_y);
_gl.drawArrays(_gl.TRIANGLE_STRIP, 0, 4);
}
function animate() {
function tick() {
draw();
_shift = (_shift + 2) % 1073741824;
_anim_request = requestAnimationFrame(tick);
}
_anim_request = requestAnimationFrame(tick);
}
function onPointerMove(e) {
e.preventDefault();
if (!e.isPrimary) {
return;
}
_mouse_x = e.offsetX;
_mouse_y = e.offsetY;
}
function resize() {
if (_anim_request) {
cancelAnimationFrame(_anim_request);
}
_canvas.width = _canvas_div.clientWidth;
_canvas.height = _canvas_div.clientHeight;
try {
_gl = _canvas.getContext('webgl2', {
alpha: false,
depth: false,
preserveDrawingBuffer: true,
});
if (!_gl) {
throw new Error('Browser does not support WebGL2');
}
} catch (e) {
_canvas_div.innerHTML =
'<p>Sorry, could not get a WebGL graphics context. Are you using a modern browser?</p>';
return;
}
try {
initGL();
} catch (e) {
_canvas_div.innerHTML = `<p>Sorry, could not initialize the WebGL graphics context: ${e.message}. Are you using a modern browser?</p>`;
return;
}
animate();
}
function init() {
_canvas_div = document.getElementById('canvas_div');
_canvas = document.getElementById('canvas');
_canvas.addEventListener('pointermove', onPointerMove);
resize();
}
window.addEventListener('load', init);
window.addEventListener('resize', resize, false);
window.addEventListener('orientationchange', resize, false);