// Main.js
(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);

})();

// Shaders.js
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? The color changes as the temperature gets hotter. lava is hot, stone can turn into lava, sand transforms into stone if hot enough, water cools things down.. smoke is hot, wall cannot melt, water if too cold, it freezes, output both of the scripts as a full fullest full script