(function() {

function createShader(gl, type, source) {
	const shader = gl.createShader(type);
	gl.shaderSource(shader, source);
	gl.compileShader(shader);
	const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);

	if (!success) {
		console.log(gl.getShaderInfoLog(shader));
		gl.deleteShader(shader);
		return undefined;
	}
	return shader;
}

function createProgram(gl, vertShader, fragShader) {
	const program = gl.createProgram();
	gl.attachShader(program, vertShader);
	gl.attachShader(program, fragShader);
	gl.linkProgram(program);

	const success = gl.getProgramParameter(program, gl.LINK_STATUS);
	if (!success) {
		console.log(gl.getProgramInfoLog(program));
		gl.deleteProgram(program);
		return undefined;
	}
	return program;
}

function createProgramFromString(gl, vertexShaderString, fragmentShaderString) {

	const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderString);
	const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderString);

	return createProgram(gl, vertexShader, fragmentShader);
}

function resizeCanvasToDisplaySize(canvas, pixelRatio) {
	pixelRatio = pixelRatio || 1;
	const width  = canvas.clientWidth * pixelRatio | 0;
	const height = canvas.clientHeight * pixelRatio | 0;
	if (canvas.width !== width ||  canvas.height !== height) {
		canvas.width = width;
		canvas.height = height;
		return true;
	}
	return false;
}

function mod(x, y) {
	return x - y * Math.floor(x / y);
}

function fract(x) {
	return x - Math.floor(x);
}

function clamp(x, xmin, xmax) {
	return Math.min(Math.max(x, xmin), xmax);
}

function hashi(x)
{
	x ^= x >> 16;
	x *= 0x7feb352d;
	x ^= x >> 15;
	x *= 0x846ca68b;
	x ^= x >> 16;
	return x;
}

function hashf(p)
{
	p = fract(p * .1031);
	p *= p + 323.333;
	p *= p + p;
	return fract(p);
}

const title = document.getElementById("title");
const githubIcon = document.getElementById("github-icon");

const urlParams = new URLSearchParams(window.location.search);

if (!urlParams.has("header")) {
	title.style.display = "none";
} else {
	githubIcon.style.display = "none";
}

const container = document.getElementById("canvas-container");
const canvas = document.getElementById("main-canvas");
const gl = canvas.getContext("webgl2");

if (!gl) {
	console.error("WebGL2 context not available!");
}

if (!gl.getExtension('EXT_color_buffer_float')) {
	console.error('need EXT_color_buffer_float');
}

if (!gl.getExtension('OES_texture_float_linear')) {
	console.error('need OES_texture_float_linear');
}

const program = createProgramFromString(gl, vsText, fsText);
const blitProgram = createProgramFromString(gl, vsText, renderShaderText);
const jfaShadowProgram = createProgramFromString(gl, vsText, jfaShaderText);
const jfaInitProgram = createProgramFromString(gl, vsText, jfaInitShaderText);

const quad = [
	-1.0, -1.0, 0.0, 0.0,
	-1.0,  1.0, 0.0, 1.0,
	 1.0,  1.0, 1.0, 1.0,
	 1.0, -1.0, 1.0, 0.0
];

const posBuffer = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(quad), gl.STATIC_DRAW);

const vao = gl.createVertexArray();

gl.bindVertexArray(vao);

const posAttribLoc = gl.getAttribLocation(program, "aPosition");
const uvAttribLoc = gl.getAttribLocation(program, "aUv");

gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 16, 0);
gl.enableVertexAttribArray(posAttribLoc);

gl.vertexAttribPointer(uvAttribLoc, 2, gl.FLOAT, false, 16, 8);
gl.enableVertexAttribArray(uvAttribLoc);

resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

const SCALE = 10;

let bufferWidth = Math.ceil(gl.canvas.width / SCALE);
let bufferHeight = Math.ceil(gl.canvas.height / SCALE);

const fbo = [null, null];
const tex = [null, null];

for (let i = 0; i < 2; i++) {

	tex[i] = gl.createTexture();
	gl.bindTexture(gl.TEXTURE_2D, tex[i]);
	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);

	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

	fbo[i] = gl.createFramebuffer();
	gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[i]);
	gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex[i], 0);
}

let shadowSwap = 0;
const shadowFbo = [null, null];
const shadowTexR = [null, null];
const shadowTexG = [null, null];
const shadowTexB = [null, null];

for (let i = 0; i < 2; i++) {

	shadowTexR[i] = gl.createTexture();
	gl.bindTexture(gl.TEXTURE_2D, shadowTexR[i]);
	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);

	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

	shadowTexG[i] = gl.createTexture();
	gl.bindTexture(gl.TEXTURE_2D, shadowTexG[i]);
	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);

	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

	shadowTexB[i] = gl.createTexture();
	gl.bindTexture(gl.TEXTURE_2D, shadowTexB[i]);
	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);

	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

	shadowFbo[i] = gl.createFramebuffer();
	gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo[i]);
	gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, shadowTexR[i], 0);
	gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, shadowTexG[i], 0);
	gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, shadowTexB[i], 0);

	gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
}

console.log(`Screen size: ${gl.canvas.width}, ${gl.canvas.height}`);
console.log(`Buffer size: ${bufferWidth}, ${bufferHeight}`);

let mouse = {
	x: -1, y: -1,
	px: -1, py: -1,
	pressed: false
};

let materialType = 5;
let brushRadius = 1;
let minBrushRadius = 0.5;
let maxBrushRadius = 5;
let paused = false;

function mouseMoved(event) {
	let x = event.clientX != null ? event.clientX : event.touches[0].clientX;
	let y = event.clientY != null ? event.clientY : event.touches[0].clientY;
	let rect = canvas.getBoundingClientRect();
	mouse.x = x - rect.left;
	mouse.y = y - rect.top;
}

function mousePressed(event) {
	let x = event.clientX != null ? event.clientX : event.touches[0].clientX;
	let y = event.clientY != null ? event.clientY : event.touches[0].clientY;
	let rect = canvas.getBoundingClientRect();
	mouse.x = x - rect.left;
	mouse.y = y - rect.top;
	mouse.px = mouse.x;
	mouse.py = mouse.y;
	mouse.pressed = true;
}

function mouseReleased(event) {
	mouse.pressed = false;
}

container.addEventListener("mousemove", mouseMoved);
container.addEventListener("touchmove", mouseMoved);

container.addEventListener("mousedown", mousePressed);
container.addEventListener("touchstart", mousePressed);

container.addEventListener("mouseup", mouseReleased);
container.addEventListener("touchend", mouseReleased);

container.addEventListener("mouseleave", mouseReleased);
container.addEventListener("touchleave", mouseReleased);

const buttons = document.querySelectorAll("#material-buttons ul li a");

buttons[materialType].classList.add("selected");

function setMaterialType(type) {
	if (buttons[materialType].classList.contains("selected")) {
		buttons[materialType].classList.remove("selected");
	}
	materialType = clamp(type, 0, 7);
	if (!buttons[materialType].classList.contains("selected")) {
		buttons[materialType].classList.add("selected");
	}
}

buttons.forEach((button, idx) => {
	button.addEventListener("click", (e) => {
		e.preventDefault();
		setMaterialType(parseInt(button.getAttribute("data-type")));
	});
});

document.addEventListener("keydown", (e) => {
	if (e.keyCode >= 48 && e.keyCode <= 57) {
		setMaterialType(e.keyCode - 48);
	} else if (e.keyCode >= 96 && e.keyCode <= 105) {
		setMaterialType(e.keyCode - 96);
	}

	if (e.keyCode == 189 || e.keyCode == 109) {
		brushRadius = clamp(brushRadius - 0.5, minBrushRadius, maxBrushRadius);
	} else if (e.keyCode == 187 || e.keyCode == 107) {
		brushRadius = clamp(brushRadius + 0.5, minBrushRadius, maxBrushRadius);
	}

	if (e.keyCode == 32 || e.keyCode == 27) {
		paused = !paused;
	}
});

document.addEventListener("keyup", (e) => {
});

let swap = 0;
let frames = 0;
let demo = true;
let seed = Date.now();

function render(time) {

	let resized = false;
	if (resizeCanvasToDisplaySize(gl.canvas)) {
		resized = true;

		gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

		bufferWidth = Math.ceil(gl.canvas.width / SCALE);
		bufferHeight = Math.ceil(gl.canvas.height / SCALE);

		for (let i = 0; i < 2; i++)
		{
			gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[i]);

			const newTex = gl.createTexture();
			gl.bindTexture(gl.TEXTURE_2D, newTex);
			gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);

			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

			gl.bindTexture(gl.TEXTURE_2D, newTex);
			gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, bufferWidth, bufferHeight);
			
			gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTex, 0);

			gl.deleteTexture(tex[i]);
			tex[i] = newTex;
		}

		for (let i = 0; i < 2; i++)
		{
			gl.bindTexture(gl.TEXTURE_2D, shadowTexR[i]);
			gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
		
			gl.bindTexture(gl.TEXTURE_2D, shadowTexG[i]);
			gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
		
			gl.bindTexture(gl.TEXTURE_2D, shadowTexB[i]);
			gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, bufferWidth, bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
		}
	}

	if (!paused || mouse.pressed) {

		gl.useProgram(program);
		gl.bindVertexArray(vao);

		const timeLoc = gl.getUniformLocation(program, "time");
		const frameLoc = gl.getUniformLocation(program, "frame");
		const resLoc = gl.getUniformLocation(program, "resolution");
		const texLoc = gl.getUniformLocation(program, "tex");
		const mouseLoc = gl.getUniformLocation(program, "mouse");
		const mouseButtonsLoc = gl.getUniformLocation(program, "mouseButtons");
		const materialTypeLoc = gl.getUniformLocation(program, "materialType");
		const brushRadiusLoc = gl.getUniformLocation(program, "brushRadius");

		let mx = (mouse.x / gl.canvas.width) * bufferWidth;
		let my = (1.0 - mouse.y / gl.canvas.height) * bufferHeight;
		let mpx = (mouse.px / gl.canvas.width) * bufferWidth;
		let mpy = (1.0 - mouse.py / gl.canvas.height) * bufferHeight;

		let pressed = false;

		if (mouse.pressed)
			demo = false;

		if (demo && frames > 40) {
			if (frames < 1200) {
				let id = Math.ceil(frames / 40);
				let type = hashf(id + seed);
				let x = hashf(id + seed + 3.3);
				x += Math.sin(frames * 0.1) * 0.03;
				let y = hashf(id + seed + 5.3);
				type = Math.floor(type * 5.0 + 2.0);
				mx = clamp(x, 0.0, 1.0) * bufferWidth;
				my = (0.8 + y * 0.1) * bufferHeight;
				mpx = mx;
				mpy = my;
				pressed = true;
				setMaterialType(type);
			} else {
				demo = false;
			}
		}

		gl.uniform1f(timeLoc, time * 0.001);
		gl.uniform2f(resLoc, bufferWidth, bufferHeight);
		gl.uniform1i(materialTypeLoc, materialType);
		gl.uniform1f(brushRadiusLoc, brushRadius);

		if (mouse.pressed || pressed)
			gl.uniform4f(mouseLoc, mx, my, mpx, mpy);
		else
			gl.uniform4f(mouseLoc, -mx, -my, -mpx, -mpy);

		const PASSES = 3;
		for (let i = 0; i < PASSES; i++) {
			gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[swap]);
			gl.viewport(0, 0, bufferWidth, bufferHeight);

			gl.clearColor(0, 0, 0, 0);
			gl.clear(gl.COLOR_BUFFER_BIT);

			gl.uniform1i(frameLoc, frames * PASSES + i);

			gl.uniform1i(texLoc, 0);
			gl.activeTexture(gl.TEXTURE0);
			gl.bindTexture(gl.TEXTURE_2D, tex[1 - swap]);

			gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

			swap = 1 - swap;
		}

		frames++;
	}

	if (paused && !resized && !mouse.pressed) {
		requestAnimationFrame(render);
		return;
	}

	{
		shadowSwap = 0;

		gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo[shadowSwap]);
		gl.viewport(0, 0, bufferWidth, bufferHeight);

		gl.clearColor(0, 0, 0, 0);
		gl.clear(gl.COLOR_BUFFER_BIT);

		gl.useProgram(jfaInitProgram);
		gl.bindVertexArray(vao);

		const resLoc = gl.getUniformLocation(jfaInitProgram, "resolution");
		const texLoc = gl.getUniformLocation(jfaInitProgram, "dataTex");

		gl.uniform2f(resLoc, bufferWidth, bufferHeight);

		gl.uniform1i(texLoc, 0);
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, tex[swap]);

		gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

		shadowSwap = 1 - shadowSwap;
	}

	{
		const JFA_PASSES = 4;

		gl.useProgram(jfaShadowProgram);

		const resLoc = gl.getUniformLocation(jfaShadowProgram, "resolution");
		const texLoc = gl.getUniformLocation(jfaShadowProgram, "texR");
		const texGLoc = gl.getUniformLocation(jfaShadowProgram, "texG");
		const texBLoc = gl.getUniformLocation(jfaShadowProgram, "texB");
		const stepSizeLoc = gl.getUniformLocation(jfaShadowProgram, "stepSize");
		const passCountLoc = gl.getUniformLocation(jfaShadowProgram, "passCount");
		const passIdxLoc = gl.getUniformLocation(jfaShadowProgram, "passIndex");

		gl.uniform2f(resLoc, bufferWidth, bufferHeight);
		gl.uniform1i(texLoc, 0);
		gl.uniform1i(texGLoc, 1);
		gl.uniform1i(texBLoc, 2);
		gl.uniform1i(passCountLoc, JFA_PASSES);

		for (let i = 0; i < JFA_PASSES; i++) {

			gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo[shadowSwap]);
			gl.viewport(0, 0, bufferWidth, bufferHeight);

			gl.clearColor(0, 0, 0, 0);
			gl.clear(gl.COLOR_BUFFER_BIT);

			const stepSize = Math.pow(2, JFA_PASSES - i - 1);

			gl.uniform1f(stepSizeLoc, stepSize);
			gl.uniform1i(passIdxLoc, i);

			gl.activeTexture(gl.TEXTURE0);
			gl.bindTexture(gl.TEXTURE_2D, shadowTexR[1 - shadowSwap]);
			gl.activeTexture(gl.TEXTURE1);
			gl.bindTexture(gl.TEXTURE_2D, shadowTexG[1 - shadowSwap]);
			gl.activeTexture(gl.TEXTURE2);
			gl.bindTexture(gl.TEXTURE_2D, shadowTexB[1 - shadowSwap]);

			gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

			shadowSwap = 1 - shadowSwap;
		}
	}

	gl.bindFramebuffer(gl.FRAMEBUFFER, null);
	gl.drawBuffers([gl.BACK]);
	gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
	
	gl.clearColor(0, 0, 0, 1);
	gl.clear(gl.COLOR_BUFFER_BIT);

	gl.useProgram(blitProgram);
	gl.bindVertexArray(vao);

	const timeLoc = gl.getUniformLocation(blitProgram, "time");
	const resLoc = gl.getUniformLocation(blitProgram, "resolution");
	const texLoc = gl.getUniformLocation(blitProgram, "tex");
	const shadowTexLoc = gl.getUniformLocation(blitProgram, "shadowTexR");
	const shadowTexGLoc = gl.getUniformLocation(blitProgram, "shadowTexG");
	const shadowTexBLoc = gl.getUniformLocation(blitProgram, "shadowTexB");
	const scaleLoc = gl.getUniformLocation(blitProgram, "scale");
	const texResLoc = gl.getUniformLocation(blitProgram, "texResolution");
	const texScaleLoc = gl.getUniformLocation(blitProgram, "texScale");

	gl.uniform1f(timeLoc, time * 0.001);
	gl.uniform2f(resLoc, gl.canvas.width, gl.canvas.height);
	gl.uniform2f(texResLoc, bufferWidth, bufferHeight);
	gl.uniform1f(texScaleLoc, SCALE);
	gl.uniform1f(scaleLoc, 1.0);

	gl.uniform1i(texLoc, 0);
	gl.activeTexture(gl.TEXTURE0);
	gl.bindTexture(gl.TEXTURE_2D, tex[swap]);

	gl.uniform1i(shadowTexLoc, 1);
	gl.uniform1i(shadowTexGLoc, 2);
	gl.uniform1i(shadowTexBLoc, 3);
	gl.activeTexture(gl.TEXTURE1);
	gl.bindTexture(gl.TEXTURE_2D, shadowTexR[1- shadowSwap]);
	gl.activeTexture(gl.TEXTURE2);
	gl.bindTexture(gl.TEXTURE_2D, shadowTexG[1- shadowSwap]);
	gl.activeTexture(gl.TEXTURE3);
	gl.bindTexture(gl.TEXTURE_2D, shadowTexB[1- shadowSwap]);

	gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

	mouse.px = mouse.x;
	mouse.py = mouse.y;

	requestAnimationFrame(render);
}

requestAnimationFrame(render);

})();

const vsText = `#version 300 es
in vec4 aPosition;
in vec2 aUv;

out vec2 outUv;

void main() {
	gl_Position = aPosition;
	outUv = aUv;
}
`;

const constantsShader = `

#define AIR 0.0
#define SMOKE 1.0
#define WATER 2.0
#define LAVA 3.0
#define SAND 4.0
#define GLITTER 5.0
#define STONE 6.0
#define WALL 7.0

const vec3 bgColor = pow(vec3(31, 34, 36) / 255.0, vec3(2));

`;

const commonShader = `

const float EPSILON = 1e-4;

const float PI = acos(-1.);
const float TAU = PI * 2.0;

float safeacos(float x) { return acos(clamp(x, -1.0, 1.0)); }

float saturate(float x) { return clamp(x, 0., 1.); }
vec2 saturate(vec2 x) { return clamp(x, vec2(0), vec2(1)); }
vec3 saturate(vec3 x) { return clamp(x, vec3(0), vec3(1)); }

float sqr(float x) { return x*x; }
vec2 sqr(vec2 x) { return x*x; }
vec3 sqr(vec3 x) { return x*x; }

float luminance(vec3 col) { return dot(col, vec3(0.2126729, 0.7151522, 0.0721750)); }

mat2 rot2D(float a)
{
	float c = cos(a);
	float s = sin(a);
	return mat2(c, s, -s, c);
}

// https://iquilezles.org/articles/smin/
float smin( float d1, float d2, float k ) {
	float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
	return mix( d2, d1, h ) - k*h*(1.0-h);
}

float smax( float d1, float d2, float k ) {
	float h = clamp( 0.5 - 0.5*(d2-d1)/k, 0.0, 1.0 );
	return mix( d2, d1, h ) + k*h*(1.0-h);
}

// https://iquilezles.org/articles/palettes/
vec3 palette(float t)
{
	return .5 + .5 * cos(TAU * (vec3(1, 1, 1) * t + vec3(0, .33, .67)));
}

// Hash without Sine
// https://www.shadertoy.com/view/4djSRW
float hash12(vec2 p)
{
	vec3 p3  = fract(vec3(p.xyx) * .1031);
	p3 += dot(p3, p3.yzx + 33.33);
	return fract((p3.x + p3.y) * p3.z);
}

float hash13(vec3 p3)
{
	p3  = fract(p3 * .1031);
	p3 += dot(p3, p3.zyx + 31.32);
	return fract((p3.x + p3.y) * p3.z);
}

vec2 hash22(vec2 p)
{
	vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
	p3 += dot(p3, p3.yzx+33.33);
	return fract((p3.xx+p3.yz)*p3.zy);
}

vec2 hash23(vec3 p3)
{
	p3 = fract(p3 * vec3(.1031, .1030, .0973));
	p3 += dot(p3, p3.yzx+33.33);
	return fract((p3.xx+p3.yz)*p3.zy);
}

vec3 hash33(vec3 p3)
{
	p3 = fract(p3 * vec3(.1031, .1030, .0973));
	p3 += dot(p3, p3.yxz+33.33);
	return fract((p3.xxy + p3.yxx)*p3.zyx);
}

vec4 hash43(vec3 p)
{
	vec4 p4 = fract(vec4(p.xyzx)  * vec4(.1031, .1030, .0973, .1099));
	p4 += dot(p4, p4.wzxy+33.33);
	return fract((p4.xxyz+p4.yzzw)*p4.zywx);
}

// https://www.chilliant.com/rgb2hsv.html
vec3 RGBtoHCV(in vec3 RGB)
{
	// Based on work by Sam Hocevar and Emil Persson
	vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0);
	vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx);
	float C = Q.x - min(Q.w, Q.y);
	float H = abs((Q.w - Q.y) / (6.0 * C + EPSILON) + Q.z);
	return vec3(H, C, Q.x);
}

vec3 RGBtoHSV(in vec3 RGB)
{
	vec3 HCV = RGBtoHCV(RGB);
	float S = HCV.y / (HCV.z + EPSILON);
	return vec3(HCV.x, S, HCV.z);
}

vec3 RGBtoHSL(in vec3 RGB)
{
	vec3 HCV = RGBtoHCV(RGB);
	float L = HCV.z - HCV.y * 0.5;
	float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + EPSILON);
	return vec3(HCV.x, S, L);
}

vec3 HUEtoRGB(in float H)
{
	float R = abs(H * 6.0 - 3.0) - 1.0;
	float G = 2.0 - abs(H * 6.0 - 2.0);
	float B = 2.0 - abs(H * 6.0 - 4.0);
	return saturate(vec3(R,G,B));
}

vec3 HSVtoRGB(in vec3 HSV)
{
	vec3 RGB = HUEtoRGB(HSV.x);
	return ((RGB - 1.0) * HSV.y + 1.0) * HSV.z;
}

vec3 HSLtoRGB(in vec3 HSL)
{
	vec3 RGB = HUEtoRGB(HSL.x);
	float C = (1.0 - abs(2.0 * HSL.z - 1.0)) * HSL.y;
	return (RGB - 0.5) * C + HSL.z;
}

vec3 sRGBToLinear(vec3 col)
{
	return mix(pow((col + 0.055) / 1.055, vec3(2.4)), col / 12.92, lessThan(col, vec3(0.04045)));
}

vec3 linearTosRGB(vec3 col)
{
	return mix(1.055 * pow(col, vec3(1.0 / 2.4)) - 0.055, col * 12.92, lessThan(col, vec3(0.0031308)));
}
`;

const fsText = `#version 300 es
precision highp float;

uniform vec2 resolution;
uniform float time;
uniform int frame;
uniform vec4 mouse;
uniform int materialType;
uniform float brushRadius;
uniform sampler2D tex;

in vec2 outUv;

out vec4 fragColor;
` +
constantsShader +
commonShader +
`

// https://iquilezles.org/articles/distfunctions2d/
float sdSegment(vec2 p, vec2 a, vec2 b)
{
	vec2 pa = p-a, ba = b-a;
	float h = clamp( dot(pa,ba) / dot(ba,ba), 0.0, 1.0 );
	return length( pa - ba*h );
}

ivec2 getOffset(int frame)
{
	int i = frame % 4;
	if (i == 0)
		return ivec2(0, 0);
	else if (i == 1)
		return ivec2(1, 1);
	else if (i == 2)
		return ivec2(0, 1);
	return ivec2(1, 0);
}

vec4 getData(ivec2 p)
{
	if (p.x < 0 || p.y < 0 || p.x >= int(resolution.x) || p.y >= int(resolution.y))
		return vec4(vec3(0.02), WALL);
	vec4 data = texelFetch(tex, p, 0);
	if (data.xyz == vec3(0))
		data.xyz = bgColor;
	return data;
}

void swap(inout vec4 a, inout vec4 b)
{
	vec4 tmp = a;
	a = b;
	b = tmp;
}

vec4 createParticle(float id)
{
	if (id == AIR)
	{
		return vec4(bgColor, AIR);
	} else if (id == SMOKE)
	{
		return vec4(mix(bgColor, vec3(0.15), 0.5), SMOKE);
	} else if (id == WATER)
	{
		return vec4(mix(bgColor, vec3(0.15, 0.45, 0.9), 0.7), WATER);
	} else if (id == LAVA)
	{
		vec3 r = hash33(vec3(gl_FragCoord.xy, frame));
		vec3 color = vec3(255, 40, 20) / 255.0;
		vec3 hsl = RGBtoHSL(color);
		hsl.x += (r.z - 0.5) * 12.0 / 255.0;
		hsl.y += (r.x - 0.5) * 16.0 / 255.0;
		hsl.z *= (r.y * 80.0 / 255.0 + (255.0 - 80.0) / 255.0);
		return vec4(HSLtoRGB(hsl), LAVA);
	} else if (id == SAND)
	{
		vec3 r = hash33(vec3(gl_FragCoord.xy, frame));
		vec3 color = vec3(220, 158, 70) / 255.0;
		vec3 hsl = RGBtoHSL(color);
		hsl.x += (r.z - 0.5) * 12.0 / 255.0;
		hsl.y += (r.x - 0.5) * 16.0 / 255.0;
		hsl.z += (r.y - 0.5) * 40.0 / 255.0;
		return vec4(HSLtoRGB(hsl), SAND);
	} else if (id == GLITTER)
	{
		float r = fract(sin(gl_FragCoord.x / 36.0 * PI) + cos(gl_FragCoord.y / 36.0 * PI));
		r += (hash13(vec3(gl_FragCoord.xy, frame)) - 0.5) * 0.05;
		r += time * 0.08;
		return vec4(palette(r), GLITTER);
	} else if (id == STONE)
	{
		float r = hash13(vec3(gl_FragCoord.xy, frame));
		return vec4(vec3(0.08, 0.1, 0.12) * (r * 0.5 + 0.5), STONE);
	} else if (id == WALL)
	{
		float r = hash13(vec3(gl_FragCoord.xy, frame));
		return vec4(bgColor * 0.5 * (r * 0.4 + 0.6), WALL);
	}
		return vec4(bgColor, AIR);
}

void main() {
	vec2 uv = gl_FragCoord.xy / resolution;

	if (frame == 0) {
		float r = hash12(gl_FragCoord.xy);
		float id = AIR;
		if (r < 0.15)
		{
			id = SAND;
		} else if (r < 0.25)
		{
			id = SMOKE;
		}

		fragColor = createParticle(id);
		return;
	}

	if (mouse.x > 0.0)
	{
		float d = sdSegment(gl_FragCoord.xy, mouse.xy, mouse.zw);
		if (d < brushRadius)
		{
			fragColor = createParticle(float(materialType));
			return;
		}
	}

	ivec2 offset = getOffset(frame);
	ivec2 fc = ivec2(gl_FragCoord.xy) + offset;
	ivec2 p = (fc / 2) * 2 - offset;
	ivec2 xy = fc % 2;
	int i = xy.x + xy.y * 2;

	vec4 t00 = getData(p);
	vec4 t10 = getData(p + ivec2(1, 0));
	vec4 t01 = getData(p + ivec2(0, 1));
	vec4 t11 = getData(p + ivec2(1, 1));

	vec4 tn00 = getData(p + ivec2(0, -1));
	vec4 tn10 = getData(p + ivec2(1, -1));

	if (t00.a == t10.a && t01.a == t11.a && t00.a == t01.a)
	{
		fragColor = i == 0 ? t00 :
					i == 1 ? t10 :
					i == 2 ? t01 : t11;
		return;
	}

	vec4 r = hash43(vec3(p, frame));

	if ((t01.a == SMOKE && t11.a < SMOKE ||
		t01.a < SMOKE && t11.a == SMOKE) && r.x < 0.25)
	{
		swap(t01, t11);
	}

	if (t00.a == SMOKE)
	{
		if (t01.a < t00.a && r.y < 0.25)
		{
			swap(t00, t01);
		} else if (r.z < 0.003)
		{
			t00 = vec4(bgColor, AIR);
		}
	}
	if (t10.a == SMOKE)
	{
		if (t11.a < t10.a && r.y < 0.25)
		{
			swap(t10, t11);
		} else if (r.z < 0.003)
		{
			t10 = vec4(bgColor, AIR);
		}
	}

	if (((t01.a == SAND || t01.a == GLITTER) && t11.a < SAND ||
		t01.a < SAND && (t11.a == SAND || t11.a == GLITTER)) &&
		t00.a < SAND && t10.a < SAND && r.x < 0.4)
	{
		swap(t01, t11);
	}

	if (t01.a == SAND || t01.a == GLITTER || t01.a == STONE)
	{
		if (t00.a < SAND)
		{
			if (r.y < 0.9) swap(t01, t00);
		} else if (t11.a < SAND && t10.a < SAND)
		{
			swap(t01, t10);
		}
	}

	if (t11.a == SAND || t11.a == GLITTER || t11.a == STONE)
	{
		if (t10.a < SAND)
		{
			if (r.y < 0.9) swap(t11, t10);
		} else if (t01.a < SAND && t00.a < SAND)
		{
			swap(t11, t00);
		}
	}

	bool drop = false;
	if (t01.a == WATER)
	{
		if (t00.a < t01.a && r.y < 0.95)
		{
			swap(t01, t00);
			drop = true;
		} else if (t11.a < t01.a && t10.a < t01.a && r.z < 0.3)
		{
			swap(t01, t10);
			drop = true;
		}
	}
	if (t11.a == WATER)
	{
		if (t10.a < t11.a && r.y < 0.95)
		{
			swap(t11, t10);
			drop = true;
		} else if (t01.a < t11.a && t00.a < t11.a && r.z < 0.3)
		{
			swap(t11, t00);
			drop = true;
		}
	}
	
	if (!drop)
	{
		if ((t01.a == WATER && t11.a < WATER ||
			t01.a < WATER && t11.a == WATER) &&
			(t00.a >= WATER && t10.a >= WATER || r.w < 0.8))
		{
			swap(t01, t11);
		}
		if ((t00.a == WATER && t10.a < WATER ||
			t00.a < WATER && t10.a == WATER) &&
			(tn00.a >= WATER && tn10.a >= WATER || r.w < 0.8))
		{
			swap(t00, t10);
		}
	}

	if (t01.a == LAVA)
	{
		if (t00.a < t01.a && r.y < 0.8)
		{
			swap(t01, t00);
		} else if (t11.a < t01.a && t10.a < t01.a && r.z < 0.2)
		{
			swap(t01, t10);
		}
	}
	if (t11.a == LAVA)
	{
		if (t10.a < t11.a && r.y < 0.8)
		{
			swap(t11, t10);
		} else if (t01.a < t11.a && t00.a < t11.a && r.z < 0.2)
		{
			swap(t11, t00);
		}
	}

	if (t00.a == LAVA)
	{
		if (t01.a == WATER)
		{
			t00 = createParticle(STONE);
			t01 = createParticle(SMOKE);
		} else if (t10.a == WATER)
		{
			t00 = createParticle(STONE);
			t10 = createParticle(SMOKE);
		}
	}
	if (t10.a == LAVA)
	{
		if (t11.a == WATER)
		{
			t10 = createParticle(STONE);
			t11 = createParticle(SMOKE);
		} else if (t00.a == WATER)
		{
			t10 = createParticle(STONE);
			t00 = createParticle(SMOKE);
		}
	}

	if ((t01.a == LAVA && t11.a < LAVA ||
		t01.a < LAVA && t11.a == LAVA) && r.x < 0.6)
	{
		swap(t01, t11);
	}

	fragColor = i == 0 ? t00 :
				i == 1 ? t10 :
				i == 2 ? t01 : t11;
}
`;

const jfaInitShaderText = `#version 300 es
precision highp float;

uniform vec2 resolution;
uniform sampler2D dataTex;

` +
constantsShader +
`

layout(location = 0) out vec4 fragColorR;
layout(location = 1) out vec4 fragColorG;
layout(location = 2) out vec4 fragColorB;

void main()
{
	vec2 uv = gl_FragCoord.xy / resolution;

	vec4 data = texture(dataTex, uv);

	fragColorR = vec4(-1, -1, 0, 0);
	fragColorG = vec4(-1, -1, 0, 0);
	fragColorB = vec4(-1, -1, 0, 0);

	if (data.a <= LAVA || data.a == GLITTER)
	{
		fragColorR.xy = gl_FragCoord.xy;
		fragColorG.xy = gl_FragCoord.xy;
		fragColorB.xy = gl_FragCoord.xy;
	}
	if (data.a == SMOKE)
	{
		fragColorR.w = 6.0;
		fragColorG.w = 6.0;
		fragColorB.w = 6.0;
	} else if (data.a == WATER)
	{
		fragColorR.w = 9.0;
		fragColorG.w = 6.0;
		fragColorB.w = 4.0;
	} else if (data.a == LAVA)
	{
		fragColorR.w = 0.0;
		fragColorG.w = 11.0;
		fragColorB.w = 14.0;
	} else if (data.a == GLITTER)
	{
		fragColorR.w = 4.0 - data.r * 4.0;
		fragColorG.w = 4.0 - data.g * 4.0;
		fragColorB.w = 4.0 - data.b * 4.0;
	}
}
`

const jfaShaderText = `#version 300 es
precision highp float;

uniform float stepSize;
uniform vec2 resolution;
uniform sampler2D texR;
uniform sampler2D texG;
uniform sampler2D texB;

uniform int passCount;
uniform int passIndex;

layout(location = 0) out vec4 fragColorR;
layout(location = 1) out vec4 fragColorG;
layout(location = 2) out vec4 fragColorB;

float distanceFunc(vec2 a, vec2 b)
{
	return abs(a.x - b.x) + abs(a.y - b.y);
	//return max(abs(a.x - b.x), abs(a.y - b.y));
	//return distance(a, b);
}

void main()
{
	vec2 fc = gl_FragCoord.xy;

	vec4 bestR = vec4(0,0,1e3,0);
	vec4 bestG = vec4(0,0,1e3,0);
	vec4 bestB = vec4(0,0,1e3,0);

	for (int x = -1; x <= 1; x++)
	{
		for (int y = -1; y <= 1; y++)
		{
			vec2 p = fc + vec2(x, y) * stepSize;

			vec4 dataR = texture(texR, p / resolution);
			vec4 dataG = texture(texG, p / resolution);
			vec4 dataB = texture(texB, p / resolution);

			if (dataR.xy != vec2(-1) && dataR.xy == clamp(dataR.xy, vec2(0.5), resolution-0.5))
			{
				float dist = distanceFunc(fc, dataR.xy) + dataR.w;
				if (dist < bestR.z)
				{
					bestR = dataR;
					bestR.z = dist;
				}
			}
			if (dataG.xy != vec2(-1) && dataG.xy == clamp(dataG.xy, vec2(0.5), resolution-0.5))
			{
				float dist = distanceFunc(fc, dataG.xy) + dataG.w;
				if (dist < bestG.z)
				{
					bestG = dataG;
					bestG.z = dist;
				}
			}
			if (dataB.xy != vec2(-1) && dataB.xy == clamp(dataB.xy, vec2(0.5), resolution-0.5))
			{
				float dist = distanceFunc(fc, dataB.xy) + dataB.w;
				if (dist < bestB.z)
				{
					bestB = dataB;
					bestB.z = dist;
				}
			}
		}
	}

	fragColorR = vec4(bestR.xy, bestR.z != 1e3 ? bestR.z : 1e3, bestR.w);
	fragColorG = vec4(bestG.xy, bestG.z != 1e3 ? bestG.z : 1e3, bestG.w);
	fragColorB = vec4(bestB.xy, bestB.z != 1e3 ? bestB.z : 1e3, bestB.w);

	if (passIndex == passCount - 1)
	{
		if (bestR.xy == vec2(-1))
			fragColorR.z = 1e3;
		if (bestG.xy == vec2(-1))
			fragColorG.z = 1e3;
		if (bestB.xy == vec2(-1))
			fragColorB.z = 1e3;
	}
}
`;

const renderShaderText = `#version 300 es
precision highp float;

uniform vec2 texResolution;
uniform float texScale;
uniform vec2 resolution;
uniform sampler2D tex;
uniform sampler2D shadowTexR;
uniform sampler2D shadowTexG;
uniform sampler2D shadowTexB;
uniform float scale;

` +
constantsShader +
`

out vec4 fragColor;

vec2 getCoordsAA(vec2 uv)
{
	float w = 1.0; // 1.5
	vec2 fl = floor(uv + 0.5);
	vec2 fr = fract(uv + 0.5);
	vec2 aa = fwidth(uv) * w * 0.5;
	fr = smoothstep(0.5 - aa, 0.5 + aa, fr);

	return fl + fr - 0.5;
}

vec3 linearTosRGB(vec3 col)
{
	return mix(1.055 * pow(col, vec3(1.0 / 2.4)) - 0.055, col * 12.92, lessThan(col, vec3(0.0031308)));
}

void main() {
	vec2 uv = gl_FragCoord.xy / (texResolution * texScale);

	uv -= 0.5;
	uv *= scale;
	uv += 0.5;

	vec2 fc = uv * texResolution;

	vec4 data = texture(tex, getCoordsAA(fc) / texResolution);
	vec4 dataUp = texture(tex, getCoordsAA(fc + vec2(0, 1)) / texResolution);
	vec4 dataDown = texture(tex, getCoordsAA(fc - vec2(0, 1)) / texResolution);

	float hig = float(data.a > dataUp.a);
	float dropSha = 1.0 - float(data.a > dataDown.a);

	vec3 color = data.rgb == vec3(0) ? bgColor : data.rgb;

	vec4 shaDataR = texture(shadowTexR, uv);
	vec4 shaDataG = texture(shadowTexG, uv);
	vec4 shaDataB = texture(shadowTexB, uv);
	
	float shaR = shaDataR.xy != vec2(-1) ? shaDataR.z : 16.0;
	float shaG = shaDataG.xy != vec2(-1) ? shaDataG.z : 16.0;
	float shaB = shaDataB.xy != vec2(-1) ? shaDataB.z : 16.0;

	vec3 sha = clamp(1.0 - vec3(shaR, shaG, shaB) / 16.0, vec3(0.0), vec3(1.0));
	sha *= sha;

	color *= 0.5 * max(hig, dropSha) + 0.5;
	color *= sha * 1.0 + 0.2;
	color += color * 0.4 * hig;

	fragColor = vec4(linearTosRGB(color), 1);
}
`;

const fsBlitText = `#version 300 es
precision highp float;

uniform vec2 resolution;
uniform sampler2D tex;

out vec4 fragColor;

void main() {
	vec2 uv = gl_FragCoord.xy / resolution;

	fragColor = vec4(1.0 - texture(tex, uv).rgb, 1);
}
`;

Can you make a temperature system? add a sensor on the top left where it displays the element the cursor is looking and the temperature of that object. lava makes heat