Coding soon

Coding soon

<canvas id="seascape"></canvas>
<div id="infos">
	Seascape<br/>
	Optimized<br/>
	<a href="https://www.shadertoy.com/view/Ms2SD1">Credits</a> 🌊
</div>
#seascape {
	display: block;
	width: 100vw;
	height: 100vh;
	user-select: none;
}
#infos {
	position: fixed;
	bottom: 0;
	left: 0;
	font-family: 'Courier New', Courier, monospace;
	padding: .3rem .4rem;
	color: #FFF;
	background: rgba(0, 0, 0, 0.3);
}
#infos a {
	color: inherit;
	border-bottom: solid 0.5px var(--link-color);
}
/**
 * @class SeascapeShader
 */
class SeascapeShader {

	// original shader : https://www.shadertoy.com/view/Ms2SD1

	constructor(canvas) {

		this.canvas = canvas;
		this.gl = canvas.getContext(
			"webgl",
			{
				antialias: false, 
				depth: false, 
				alpha: false,
				powerPreference: "high-performance"
			}
		);
		
		if(!this.gl) {

			console.error("WebGL not supported");

			return;
		
		}
		
		// Disable unnecessary features
		this.gl.disable(this.gl.DEPTH_TEST);
		this.gl.disable(this.gl.BLEND);
		this.gl.clearColor(
			0.0,
			0.0,
			0.0,
			1.0
		);
		
		// Camera state
		this.offset = 0;
		this.timeOffset = 0;
		this.pointing = false;
		
		this.initShaders();
		this.initBuffers();
		this.initEvents();
		this.resize();
		
		// Cache bound render method
		this.boundRender = this.render.bind(this);

		requestAnimationFrame(this.boundRender);
	
	}
	
	initShaders() {

		// Vertex shader
		const vsSource = `
			attribute vec2 aVertexPosition;
			void main() {
				gl_Position = vec4(aVertexPosition, 0.0, 1.0);
			}
		`;
		
		// Fragment shader
		const fsSource = `
			precision highp float;
			
			uniform vec2 iResolution;
			uniform float iTime;
			uniform float iOffset;
			
			const int NUM_STEPS = 8;
			const float PI = 3.141592;
			const float EPSILON = 1e-3;
			#define EPSILON_NRM (0.1 / iResolution.x)
			
			// Sea constants
			const int ITER_GEOMETRY = 3;
			const int ITER_FRAGMENT = 4;
			const float SEA_HEIGHT = 0.8;
			const float SEA_CHOPPY = 4.0;
			const float SEA_SPEED = 0.9;
			const float SEA_FREQ = 0.16;
			const vec3 SEA_BASE = vec3(0.0, 0.09, 0.18);
			const vec3 SEA_WATER_COLOR = vec3(0.8, 0.9, 0.6) * 0.6;
			#define SEA_TIME (iTime * SEA_SPEED)
			const mat2 octave_m = mat2(1.6, 1.2, -1.2, 1.6);
			
			// Hash function
			float hash(vec2 p) {
				return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
			}
			
			// Noise function
			float noise(vec2 p) {
				vec2 i = floor(p);
				vec2 f = fract(p);
				vec2 u = f * f * (3.0 - 2.0 * f);
				
				float a = hash(i);
				float b = hash(i + vec2(1.0, 0.0));
				float c = hash(i + vec2(0.0, 1.0));
				float d = hash(i + vec2(1.0, 1.0));
				
				return -1.0 + 2.0 * (a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y);
			}
			
			// Rotation matrix calculation
			mat3 fromEuler(vec3 ang) {
				vec2 a1 = vec2(sin(ang.x), cos(ang.x));
				vec2 a2 = vec2(sin(ang.y), cos(ang.y));
				vec2 a3 = vec2(sin(ang.z), cos(ang.z));
				
				mat3 m;
				m[0] = vec3(a1.y * a3.y + a1.x * a2.x * a3.x, a1.y * a2.x * a3.x + a3.y * a1.x, -a2.y * a3.x);
				m[1] = vec3(-a2.y * a1.x, a1.y * a2.y, a2.x);
				m[2] = vec3(a3.y * a1.x * a2.x + a1.y * a3.x, a1.x * a3.x - a1.y * a3.y * a2.x, a2.y * a3.y);
				return m;
			}
			
			// Lighting functions
			float diffuse(vec3 n, vec3 l, float p) {
				return pow(dot(n, l) * 0.4 + 0.61, p);
			}
			
			float specular(vec3 n, vec3 l, vec3 e, float s) {
				float nrm = (s + 8.0) / (PI * 8.0);
				return pow(max(dot(reflect(e, n), l), 0.0), s) * nrm;
			}
			
			// Sky color
			vec3 getSkyColor(vec3 e) {
				e.y = (max(e.y, 0.0) * 0.8 + 0.2) * 0.8;
				return vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4) * 1.1;
			}
			
			// Sea octave
			float sea_octave(vec2 uv, float choppy) {
				uv += noise(uv);
				vec2 wv = 1.0 - abs(sin(uv));
				vec2 swv = abs(cos(uv));
				wv = mix(wv, swv, wv);
				return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
			}
			
			// Map functions
			float map(vec3 p) {
				float freq = SEA_FREQ;
				float amp = SEA_HEIGHT;
				float choppy = SEA_CHOPPY;
				vec2 uv = p.xz;
				uv.x *= 0.75;
				
				float h = 0.0;
				for(int i = 0; i < ITER_GEOMETRY; i++) {
					float d = sea_octave((uv + SEA_TIME) * freq, choppy);
					d += sea_octave((uv - SEA_TIME) * freq, choppy);
					h += d * amp;
					
					uv *= octave_m;
					freq *= 1.9;
					amp *= 0.22;
					choppy = mix(choppy, 1.0, 0.2);
				}
				return p.y - h;
			}
			
			float map_detailed(vec3 p) {
				float freq = SEA_FREQ;
				float amp = SEA_HEIGHT;
				float choppy = SEA_CHOPPY;
				vec2 uv = p.xz;
				uv.x *= 0.75;
				
				float h = 0.0;
				for(int i = 0; i < ITER_FRAGMENT; i++) {
					float d = sea_octave((uv + SEA_TIME) * freq, choppy);
					d += sea_octave((uv - SEA_TIME) * freq, choppy);
					h += d * amp;
					
					uv *= octave_m;
					freq *= 1.9;
					amp *= 0.22;
					choppy = mix(choppy, 1.0, 0.2);
				}
				return p.y - h;
			}
			
			// Sea color calculation
			vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {
				float fresnel = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
				fresnel = pow(fresnel, 3.0) * 0.5;
				
				vec3 reflected = getSkyColor(reflect(eye, n));
				vec3 refracted = SEA_BASE + diffuse(n, l, 80.0) * SEA_WATER_COLOR * 0.15;
				
				vec3 color = mix(refracted, reflected, fresnel);
				float atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
				color += SEA_WATER_COLOR * (p.y - SEA_HEIGHT) * 0.18 * atten;
				
				color += specular(n, l, eye, 60.0);
				
				return color;
			}
			
			// Normal calculation
			vec3 getNormal(vec3 p, float eps) {
				vec3 n;
				n.y = map_detailed(p);
				n.x = map_detailed(vec3(p.x + eps, p.y, p.z)) - n.y;
				n.z = map_detailed(vec3(p.x, p.y, p.z + eps)) - n.y;
				n.y = eps;
				return normalize(n);
			}
			
			// Ray tracing
			float heightMapTracing(vec3 ori, vec3 dir, out vec3 p) {
				float tm = 0.0;
				float tx = 1000.0;
				float hx = map(ori + dir * tx);
				
				if(hx > 0.0) {
					p = ori + dir * tx;
					return tx;
				}
				
				float hm = map(ori);
				float tmid = 0.0;
				
				for(int i = 0; i < NUM_STEPS; i++) {
					tmid = mix(tm, tx, hm / (hm - hx));
					p = ori + dir * tmid;
					float hmid = map(p);
					
					if (hmid < 0.0) {
						tx = tmid;
						hx = hmid;
					}
					else {
						tm = tmid;
						hm = hmid;
					}
					
					if (abs(hmid) < EPSILON) 
						break;
				}
				
				return tmid;
			}
			
			// Main pixel calculation
			vec3 getPixel(vec2 coord, float time) {
				vec2 uv = coord / iResolution.xy;
				uv = uv * 2.0 - 1.0;
				uv.x *= iResolution.x / iResolution.y;
				
				// Camera
				vec3 ang = vec3(sin(time * 1.0) * 0.05, sin(time) * 0.1, time * 0.07);
				vec3 ori = vec3(0.0, 3.5, time * 5.0);
				vec3 dir = normalize(vec3(uv.xy, -2.0));
				dir.z += length(uv) * 0.19;
				dir = normalize(dir) * fromEuler(ang);
				
				// Tracing
				vec3 p;
				heightMapTracing(ori, dir, p);
				vec3 dist = p - ori;
				vec3 n = getNormal(p, dot(dist, dist) * EPSILON_NRM);
				vec3 light = normalize(vec3(0.0, 1.0, 0.8));
				
				// Calculate final color
				vec3 sea = getSeaColor(p, n, light, dir, dist);
				vec3 sky = getSkyColor(dir);
				float skyFactor = smoothstep(0.0, -0.05, dir.y);
				
				return mix(sky, sea, pow(skyFactor, 0.2));
			}
			
			void main() {
				// Combined time with offset
				float time = iTime + iOffset;
				
				// Get color and apply gamma correction
				vec3 color = getPixel(gl_FragCoord.xy, time);
				gl_FragColor = vec4(pow(color, vec3(0.65)), 1.0);
			}
		`;
		
		// Create shader program
		this.shaderProgram = this.createShaderProgram(
			vsSource,
			fsSource
		);
		
		// Store locations
		this.programInfo = {
			attribLocations: {
				vertexPosition: this.gl.getAttribLocation(
					this.shaderProgram,
					"aVertexPosition"
				)
			},
			uniformLocations: {
				resolution: this.gl.getUniformLocation(
					this.shaderProgram,
					"iResolution"
				),
				time: this.gl.getUniformLocation(
					this.shaderProgram,
					"iTime"
				),
				offset: this.gl.getUniformLocation(
					this.shaderProgram,
					"iOffset"
				)
			}
		};
	
	}
	
	createShaderProgram(vsSource, fsSource) {

		const vertexShader = this.loadShader(
			this.gl.VERTEX_SHADER,
			vsSource
		);

		const fragmentShader = this.loadShader(
			this.gl.FRAGMENT_SHADER,
			fsSource
		);
		
		const program = this.gl.createProgram();

		this.gl.attachShader(
			program,
			vertexShader
		);

		this.gl.attachShader(
			program,
			fragmentShader
		);

		this.gl.linkProgram(program);
		
		if(!this.gl.getProgramParameter(
			program,
			this.gl.LINK_STATUS
		)) {

			console.error("Shader program link fail");

			return null;
		
		}
		
		// Clean up shaders after linking
		this.gl.deleteShader(vertexShader);
		this.gl.deleteShader(fragmentShader);
		
		return program;
	
	}
	
	loadShader(type, source) {

		const shader = this.gl.createShader(type);

		this.gl.shaderSource(
			shader,
			source
		);

		this.gl.compileShader(shader);
		
		if(!this.gl.getShaderParameter(
			shader,
			this.gl.COMPILE_STATUS
		)) {

			console.error(
				"Shader compilation error",
				this.gl.getShaderInfoLog(shader)
			);

			this.gl.deleteShader(shader);

			return null;
		
		}
		
		return shader;
	
	}
	
	initBuffers() {

		// Single quad buffer creation
		const positionBuffer = this.gl.createBuffer();

		this.gl.bindBuffer(
			this.gl.ARRAY_BUFFER,
			positionBuffer
		);
		
		// Quad coordinates 
		const positions = new Float32Array([
			-1.0, -1.0,
			 1.0, -1.0,
			-1.0, 1.0,
			 1.0, 1.0
		]);
		
		this.gl.bufferData(
			this.gl.ARRAY_BUFFER,
			positions,
			this.gl.STATIC_DRAW
		);
		
		this.buffers = {
			position: positionBuffer
		};
	
	}
	
	initEvents() {

		Object.entries({
			"pointerdown": this.onPointerDown, 
			"pointermove": this.onPointerMove, 
			"pointerup": this.onPointerUp, 
			"pointerleave": this.onPointerUp
		})
		.forEach(([evtName, evtHandler]) => 
			this.canvas.addEventListener(
				evtName,
				evtHandler.bind(this)
			));

		this.canvas.addEventListener(
			"touchstart", 
			this.doNotTouch.bind(this)
		);
			
		window.addEventListener(
			"resize",
			this.resize.bind(this)
		);
	
	}
	
	onPointerDown(evt) {

		this.pointing = true;
		this.startX = evt.clientX;
	
	}
	
	onPointerMove(evt) {

		if(this.pointing) 
			this.offset = (this.startX - evt.clientX) * 0.001;
	
	}
	
	onPointerUp() {

		if(this.pointing) {

			this.timeOffset += this.offset;
			this.offset = 0;
			this.pointing = false;
		
		}
	
	}

	doNotTouch(evt) {
		evt.preventDefault();
	}
	
	resize() {

		// Set canvas to display size
		const displayWidth = window.innerWidth;
		const displayHeight = window.innerHeight;
		
		this.canvas.width = displayWidth;
		this.canvas.height = displayHeight;
		this.gl.viewport(
			0,
			0,
			displayWidth,
			displayHeight
		);
	
	}
	
	render(timestamp) {

		// Convert to seconds and slow down
		const time = timestamp * 0.001;
		
		this.gl.clear(this.gl.COLOR_BUFFER_BIT);
		this.gl.useProgram(this.shaderProgram);
		
		// Set uniforms
		this.gl.uniform2f(
			this.programInfo.uniformLocations.resolution,
			this.canvas.width,
			this.canvas.height
		);

		this.gl.uniform1f(
			this.programInfo.uniformLocations.time,
			time
		);

		this.gl.uniform1f(
			this.programInfo.uniformLocations.offset,
			this.offset + this.timeOffset
		);
		
		// Draw setup
		this.gl.bindBuffer(
			this.gl.ARRAY_BUFFER,
			this.buffers.position
		);

		this.gl.vertexAttribPointer(
			this.programInfo.attribLocations.vertexPosition,
			2,
			this.gl.FLOAT,
			false,
			0,
			0
		);

		this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
		
		// Draw the quad
		this.gl.drawArrays(
			this.gl.TRIANGLE_STRIP,
			0,
			4
		);
		
		// Frame loop
		requestAnimationFrame(this.boundRender);
	
	}

}

new SeascapeShader(document.getElementById("seascape"))