Plasma

logo

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 sinsin 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 (x,y)(x, y) 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 sinsin 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 2π/2562\pi/256 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 - 2π2\pi.

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