Coding soon

Coding soon

<canvas id="seascape"></canvas>
<div id="infos">
	Seascape<br/>
	Based on<br/>
	<a href="https://www.shadertoy.com/view/MdXyzX">Shader</a>🌊
</div>
body {
	/* touch-action: none; */
	overflow: hidden;
}
#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 #FFF;
}
/**
 * Enhanced WebGL2 Seascape Shader
 * Based on original shader with improved sun rendering
 */
class SeascapeShader {

	constructor(canvas) {

		// Canvas setup
		this.canvas = canvas;
		
		// Initialize WebGL2 context with performance options
		this.gl = this.canvas.getContext(
			"webgl2",
			{ 
				antialias: false, 
				depth: false,
				powerPreference: "high-performance"
			}
		);
		
		if(!this.gl) {

			console.error("WebGL2 not supported by your browser");

			return;
		
		}
		
		// Time tracking
		this.startTime = Date.now();
		
		// Camera state
		this.cameraState = {
			yaw: 0,
			pitch: 0.1,
			lastX: 0,
			lastY: 0
		};
		this.pointing = false;
		
		// Setup
		this.setupEventListeners();
		this.initShaders();
		this.initGeometry();
		this.resize();
		
		// Start rendering
		this.render = this.render.bind(this);
		requestAnimationFrame(this.render);
	
	}
	
	setupEventListeners() {

		// Resize event
		this.boundResize = this.resize.bind(this);
		window.addEventListener(
			"resize",
			this.boundResize
		);
		
		// Create bound event handlers for pointer events
		this.boundPointerDown = this.onPointerDown.bind(this);
		this.boundPointerMove = this.onPointerMove.bind(this);
		this.boundPointerUp = this.onPointerUp.bind(this);
		this.boundPreventDefault = this.preventDefault.bind(this);
		
		// Add pointer event listeners
		this.canvas.addEventListener(
			"pointerdown",
			this.boundPointerDown
		);
		this.canvas.addEventListener(
			"pointermove",
			this.boundPointerMove
		);
		this.canvas.addEventListener(
			"pointerup",
			this.boundPointerUp
		);
		this.canvas.addEventListener(
			"pointerleave",
			this.boundPointerUp
		);
		
		// Prevent default touch behavior
		this.canvas.addEventListener(
			"touchstart",
			this.boundPreventDefault,
			{
				passive: false 
			}
		);
		
	}
	
	onPointerDown(evt) {

		this.pointing = true;
		this.cameraState.lastX = evt.clientX;
		this.cameraState.lastY = evt.clientY;
	
	}
	
	onPointerMove(evt) {

		if(this.pointing) {

			// Calculate deltas and update camera angles
			const dx = evt.clientX - this.cameraState.lastX;
			const dy = evt.clientY - this.cameraState.lastY;
			const sensitivity = 0.003;
			
			this.cameraState.yaw -= dx * sensitivity;
			this.cameraState.pitch += dy * sensitivity;
			
			// Limit pitch angle to prevent flipping
			const pitchLimit = Math.PI / 2.5;

			this.cameraState.pitch = Math.max(
				Math.min(
					this.cameraState.pitch,
					pitchLimit
				),
				-pitchLimit
			);
			
			this.cameraState.lastX = evt.clientX;
			this.cameraState.lastY = evt.clientY;
		
		}
	
	}
	
	onPointerUp() {

		this.pointing = false;
	
	}
	
	preventDefault(evt) {

		evt.preventDefault();
	
	}
	
	resize() {

		// Get device pixel ratio for high DPI displays
		const dpr = window.devicePixelRatio || 1;
		
		// Calculate size in pixels
		const displayWidth = Math.floor(this.canvas.clientWidth * dpr);
		const displayHeight = Math.floor(this.canvas.clientHeight * dpr);
		
		// Update canvas size if needed
		if(this.canvas.width !== displayWidth || this.canvas.height !== displayHeight) {

			this.canvas.width = displayWidth;
			this.canvas.height = displayHeight;
			this.gl.viewport(
				0,
				0,
				displayWidth,
				displayHeight
			);
		
		}
	
	}
	
	initShaders() {

		// Vertex shader - simple fullscreen quad
		const vertexShaderSource = `#version 300 es
		precision highp float;
		
		in vec2 aPosition;
		out vec2 fragCoord;
		
		uniform vec2 iResolution;
		
		void main() {
			gl_Position = vec4(aPosition, 0.0, 1.0);
			fragCoord = (aPosition * 0.5 + 0.5) * iResolution;
		}`;
		
		// Fragment shader - using the original logic with improved sun
		const fragmentShaderSource = `#version 300 es
		precision highp float;
		
		in vec2 fragCoord;
		out vec4 fragColor;
		
		uniform float iTime;
		uniform vec2 iResolution;
		uniform vec2 iCameraAngle;
		
		// Constants from original shader
		const float DRAG_MULT = 0.38;
		const float WATER_DEPTH = 1.9;
		const float CAMERA_HEIGHT = 1.5;
		const int ITERATIONS_RAYMARCH = 8;
		const int ITERATIONS_NORMAL = 32;
		
		// Calculates wave value and its derivative, 
		// for the wave direction, position in space, wave frequency and time
		vec2 wavedx(vec2 position, vec2 direction, float frequency, float timeshift) {
			float x = dot(direction, position) * frequency + timeshift;
			float wave = exp(sin(x) - 1.0);
			float dx = wave * cos(x);
			return vec2(wave, -dx);
		}
		
		// Calculates waves by summing octaves of various waves with various parameters
		float getwaves(vec2 position, int iterations) {
			float wavePhaseShift = length(position) * 0.1; // this is to avoid every octave having exactly the same phase everywhere
			float iter = 0.0; // this will help generating well distributed wave directions
			float frequency = 1.0; // frequency of the wave, this will change every iteration
			float timeMultiplier = 2.0; // time multiplier for the wave, this will change every iteration
			float weight = 1.0;// weight in final sum for the wave, this will change every iteration
			float sumOfValues = 0.0; // will store final sum of values
			float sumOfWeights = 0.0; // will store final sum of weights
			
			for(int i=0; i < iterations; i++) {
				// generate some wave direction that looks kind of random
				vec2 p = vec2(sin(iter), cos(iter));
				
				// calculate wave data
				vec2 res = wavedx(position, p, frequency, iTime * timeMultiplier + wavePhaseShift);
				
				// shift position around according to wave drag and derivative of the wave
				position += p * res.y * weight * DRAG_MULT;
				
				// add the results to sums
				sumOfValues += res.x * weight;
				sumOfWeights += weight;
				
				// modify next octave
				weight = mix(weight, 0.0, 0.2);
				frequency *= 1.18;
				timeMultiplier *= 1.07;
				
				// add some kind of random value to make next wave look random too
				iter += 1232.399963;
			}
			// calculate and return
			return sumOfValues / sumOfWeights;
		}
		
		// Raymarches the ray from top water layer boundary to low water layer boundary
		float raymarchwater(vec3 camera, vec3 start, vec3 end, float depth) {
			vec3 pos = start;
			vec3 dir = normalize(end - start);
			for(int i=0; i < 64; i++) {
				// the height is from 0 to -depth
				float height = getwaves(pos.xz, ITERATIONS_RAYMARCH) * depth - depth;
				// if the waves height almost nearly matches the ray height, assume its a hit and return the hit distance
				if(height + 0.01 > pos.y) {
					return distance(pos, camera);
				}
				// iterate forwards according to the height mismatch
				float delta = pos.y - height;
				pos += dir * delta;
				
				// Safety check to avoid infinite loops
				if(pos.y < -depth || pos.y > 0.0) break;
			}
			// if hit was not registered, just assume hit the top layer, 
			// this makes the raymarching faster and looks better at higher distances
			return distance(start, camera);
		}
		
		// Calculate normal at point by calculating the height at the pos and 2 additional points very close to pos
		vec3 normal(vec2 pos, float e, float depth) {
			vec2 ex = vec2(e, 0);
			float H = getwaves(pos.xy, ITERATIONS_NORMAL) * depth;
			vec3 a = vec3(pos.x, H, pos.y);
			return normalize(
				cross(
					a - vec3(pos.x - e, getwaves(pos.xy - ex.xy, ITERATIONS_NORMAL) * depth, pos.y), 
					a - vec3(pos.x, getwaves(pos.xy + ex.yx, ITERATIONS_NORMAL) * depth, pos.y + e)
				)
			);
		}
		
		// Generate ray based on camera angles
		vec3 getRay(vec2 fragCoord) {
			vec2 uv = ((fragCoord.xy / iResolution.xy) * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);
			vec3 ray = normalize(vec3(uv.x, uv.y, -2.0));
			
			// Apply camera rotation
			float cp = cos(iCameraAngle.x); // pitch
			float sp = sin(iCameraAngle.x);
			float cy = cos(iCameraAngle.y); // yaw
			float sy = sin(iCameraAngle.y);
			
			// Camera rotation matrix
			mat3 rotMatrix = mat3(
				cy, 0.0, sy,
				sp * sy, cp, -sp * cy,
				-cp * sy, sp, cp * cy
			);
			
			// Apply rotation
			return normalize(rotMatrix * ray);
		}
		
		// Ray-Plane intersection
		float intersectPlane(vec3 origin, vec3 direction, vec3 point, vec3 normal) { 
			return clamp(dot(point - origin, normal) / dot(direction, normal), -1.0, 9991999.0); 
		}
		
		// Calculate where the sun should be - modified to make the sun move in a zenith to horizon pattern
		vec3 getSunDirection() {
			// Create a oscillating pattern from zenith to horizon
			float sunTime = iTime * 0.1; // Slowed down for smoother movement
			float sunCycle = mod(sunTime, 6.28318) - 3.14159; // -π to π oscillation
			float sunElevation = cos(sunCycle) * 0.5 + 0.5; // 0 to 1 range (0 = horizon, 1 = zenith)
			
			// Convert elevation to y component (zenith = 1, horizon = -0.05 to go slightly below)
			float y = mix(-0.05, 1.0, sunElevation);
			
			// Create a sun direction with appropriate x,z components
			return normalize(vec3(0.5, y, 0.5));
		}
		
		// Enhanced atmosphere rendering with smoother transitions
		vec3 getAtmosphere(vec3 ray) {
			vec3 sunDir = getSunDirection();
			float sunDot = dot(ray, sunDir);
			float sunElevation = sunDir.y; // 0 = horizon, 1 = zenith
			
			// Sky base color - blue during day, orange-purple during sunset
			// Use smoothstep with wider range for smoother transition
			float dayFactor = smoothstep(-0.1, 0.6, sunElevation);
			
			// Height factor for horizon gradient (0 at zenith, 1 at horizon)
			float horizon = pow(1.0 - abs(ray.y), 5.0);
			
			// Base colors
			vec3 zenithColorDay = vec3(0.0, 0.3, 0.8); 
			vec3 zenithColorSunset = vec3(0.05, 0.05, 0.1);
			vec3 horizonColorDaySunside = vec3(0.3, 0.6, 0.8);
			vec3 horizonColorSunsetSunside = vec3(0.9, 0.3, 0.1);
			vec3 horizonColorDayOpp = vec3(0.2, 0.4, 0.7);
			vec3 horizonColorSunsetOpp = vec3(0.03, 0.03, 0.1); // Slightly brighter for smoother transition
			
			// Blend colors based on sun elevation with smooth transition
			vec3 zenithColor = mix(zenithColorSunset, zenithColorDay, dayFactor);
			vec3 horizonColorSunside = mix(horizonColorSunsetSunside, horizonColorDaySunside, dayFactor);
			vec3 horizonColorOpp = mix(horizonColorSunsetOpp, horizonColorDayOpp, dayFactor);
			
			// Enhanced sun side vs opposite side contrast 
			// Use a wider smoothstep range for more gradual transition
			float oppositeSideDarkness = mix(
				smoothstep(-0.9, 0.7, sunDot), // Sunset mode
				smoothstep(-0.5, 0.3, sunDot), // Day mode
				dayFactor
			);
			
			// Blend horizon colors with smoother contrast transition
			vec3 horizonColor = mix(horizonColorOpp, horizonColorSunside, oppositeSideDarkness);
			
			// Final sky color
			vec3 skyColor = mix(zenithColor, horizonColor, horizon);
			
			// Return sky color
			return skyColor;
		}
		
		// Improved sun rendering with larger size and more orange color near horizon
		float getSun(vec3 ray) {
			vec3 sunDir = getSunDirection();
			float sunDot = dot(ray, sunDir);
			float sunElevation = sunDir.y;
			
			// Sun size increases more dramatically when close to horizon
			// Start size increase earlier (at 40% of descent)
			float horizonFactor = max(0.0, 1.0 - sunElevation * 2.5); 
			
			// Adjustable sun edge sharpness based on elevation
			// Softer edge near horizon for more realistic atmospheric diffusion
			float sunSharpness = mix(800.0, 1500.0, sunElevation);
			
			// Size increases up to 80% when at horizon (much larger than before)
			float sunSize = 1.0 + horizonFactor * 0.8;
			
			// Increase brightness slightly near horizon to compensate for diffusion
			float sunBrightness = 210.0 * (1.0 + horizonFactor * 0.3);
			
			return pow(max(0.0, sunDot), sunSharpness) * sunBrightness * sunSize;
		}
		
		// Sun color varies more intensely with elevation
		vec3 getSunColor(vec3 ray) {
			vec3 sunDir = getSunDirection();
			float sunElevation = sunDir.y;
			
			// Enhanced sunset colors - deeper orange-red at sunset
			vec3 sunsetColor = vec3(1.0, 0.3, 0.0); // More orange-red at sunset
			vec3 dayColor = vec3(1.0, 0.98, 0.9); // Slightly yellow-white during day
			
			// Use wider transition range for smooth color change
			float dayFactor = smoothstep(0.0, 0.4, sunElevation);
			
			// More pronounced color shift with stronger power curve
			vec3 sunColor = mix(sunsetColor, dayColor, pow(dayFactor, 2.0));
			
			// Get sun intensity
			float sun = getSun(ray);
			
			return sun * sunColor;
		}
		
		// ACES tonemapping from original shader
		vec3 acesTonemap(vec3 color) {  
			mat3 m1 = mat3(
				0.59719, 0.07600, 0.02840,
				0.35458, 0.90834, 0.13383,
				0.04823, 0.01566, 0.83777
			);
			mat3 m2 = mat3(
				1.60475, -0.10208, -0.00327,
				-0.53108,  1.10813, -0.07276,
				-0.07367, -0.00605,  1.07602
			);
			vec3 v = m1 * color;  
			vec3 a = v * (v + 0.0245786) - 0.000090537;
			vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
			return pow(clamp(m2 * (a / b), 0.0, 1.0), vec3(1.0 / 2.2));  
		}
		
		// Main rendering function
		void main() {
			// Get camera ray
			vec3 ray = getRay(fragCoord);
			
			// Sky rendering (ray pointing up)
			if(ray.y >= 0.0) {
				vec3 skyColor = getAtmosphere(ray);
				vec3 sunColor = getSunColor(ray);
				vec3 C = skyColor + sunColor;
				fragColor = vec4(acesTonemap(C * 2.0), 1.0);
				return;
			}
		
			// Water rendering (ray pointing down)
			vec3 origin = vec3(iTime * 0.2, CAMERA_HEIGHT, 0.0);
			
			// Define water planes
			vec3 waterPlaneHigh = vec3(0.0, 0.0, 0.0);
			vec3 waterPlaneLow = vec3(0.0, -WATER_DEPTH, 0.0);
			
			// Calculate intersections
			float highPlaneHit = intersectPlane(origin, ray, waterPlaneHigh, vec3(0.0, 1.0, 0.0));
			float lowPlaneHit = intersectPlane(origin, ray, waterPlaneLow, vec3(0.0, 1.0, 0.0));
			vec3 highHitPos = origin + ray * highPlaneHit;
			vec3 lowHitPos = origin + ray * lowPlaneHit;
		
			// Raymarch water surface
			float dist = raymarchwater(origin, highHitPos, lowHitPos, WATER_DEPTH);
			vec3 waterHitPos = origin + ray * dist;
		
			// Calculate normal and smooth with distance
			vec3 N = normal(waterHitPos.xz, 0.01, WATER_DEPTH);
			N = mix(N, vec3(0.0, 1.0, 0.0), 0.8 * min(1.0, sqrt(dist*0.01) * 1.1));
		
			// Calculate fresnel term for water reflectivity
			float fresnel = (0.04 + (1.0-0.04)*(pow(1.0 - max(0.0, dot(-N, ray)), 5.0)));
		
			// Reflect ray and ensure upward reflection
			vec3 R = normalize(reflect(ray, N));
			R.y = abs(R.y);
			
			// Calculate reflections and scattering
			vec3 skyReflection = getAtmosphere(R);
			vec3 sunReflection = getSunColor(R);
			vec3 reflection = skyReflection + sunReflection;
			vec3 scattering = vec3(0.0293, 0.0698, 0.1717) * 0.1 * (0.2 + (waterHitPos.y + WATER_DEPTH) / WATER_DEPTH);
		
			// Final color with tonemapping
			vec3 C = fresnel * reflection + scattering;
			fragColor = vec4(acesTonemap(C * 2.0), 1.0);
		}`;
		
		// Compile shaders
		const vertexShader = this.compileShader(
			vertexShaderSource,
			this.gl.VERTEX_SHADER
		);
		const fragmentShader = this.compileShader(
			fragmentShaderSource,
			this.gl.FRAGMENT_SHADER
		);
		
		// Create program and get locations
		this.program = this.createProgram(
			vertexShader,
			fragmentShader
		);
		this.positionAttributeLocation = this.gl.getAttribLocation(
			this.program,
			"aPosition"
		);
		this.timeUniformLocation = this.gl.getUniformLocation(
			this.program,
			"iTime"
		);
		this.resolutionUniformLocation = this.gl.getUniformLocation(
			this.program,
			"iResolution"
		);
		this.cameraAngleUniformLocation = this.gl.getUniformLocation(
			this.program,
			"iCameraAngle"
		);
	
	}
	
	compileShader(source, type) {

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

		this.gl.shaderSource(
			shader,
			source
		);
		this.gl.compileShader(shader);
		
		// Check compilation status
		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;
	
	}
	
	createProgram(vertexShader, fragmentShader) {

		const program = this.gl.createProgram();

		this.gl.attachShader(
			program,
			vertexShader
		);
		this.gl.attachShader(
			program,
			fragmentShader
		);
		this.gl.linkProgram(program);
		
		// Check linking status
		if(!this.gl.getProgramParameter(
			program,
			this.gl.LINK_STATUS
		)) {

			console.error(
				"Program linking error:",
				this.gl.getProgramInfoLog(program)
			);
			this.gl.deleteProgram(program);

			return null;
		
		}
		
		return program;
	
	}
	
	initGeometry() {

		// Create fullscreen quad
		this.positionBuffer = this.gl.createBuffer();
		this.gl.bindBuffer(
			this.gl.ARRAY_BUFFER,
			this.positionBuffer
		);
		
		// Quad covering NDC space
		const positions = new Float32Array([
			-1, -1, // bottom left
			1, -1, // bottom right
			-1, 1, // top left
			-1, 1, // top left
			1, -1, // bottom right
			1, 1 // top right
		]);
		
		this.gl.bufferData(
			this.gl.ARRAY_BUFFER,
			positions,
			this.gl.STATIC_DRAW
		);
		
		// Set up VAO for WebGL2
		this.vao = this.gl.createVertexArray();
		this.gl.bindVertexArray(this.vao);
		
		// Configure vertex attribute
		this.gl.enableVertexAttribArray(this.positionAttributeLocation);
		this.gl.vertexAttribPointer(
			this.positionAttributeLocation,
			2, // 2 components per vertex (x,y)
			this.gl.FLOAT, // 32bit floats
			false, // don't normalize
			0, // stride (0 = auto)
			0 // offset
		);
		
		// Unbind VAO
		this.gl.bindVertexArray(null);
	
	}
	
	render() {

		// Clear and set viewport
		this.gl.viewport(
			0,
			0,
			this.canvas.width,
			this.canvas.height
		);
		
		// Use program and bind VAO
		this.gl.useProgram(this.program);
		this.gl.bindVertexArray(this.vao);
		
		// Set uniforms
		this.gl.uniform1f(
			this.timeUniformLocation,
			(Date.now() - this.startTime) * 0.001
		);
		this.gl.uniform2f(
			this.resolutionUniformLocation,
			this.canvas.width,
			this.canvas.height
		);
		this.gl.uniform2f(
			this.cameraAngleUniformLocation,
			this.cameraState.pitch,
			this.cameraState.yaw
		);
		
		// Draw fullscreen quad
		this.gl.drawArrays(
			this.gl.TRIANGLES,
			0,
			6
		);
		
		// Request next frame
		requestAnimationFrame(this.render);
	
	}
	
	destroy() {

		// Remove event listeners
		window.removeEventListener(
			"resize",
			this.boundResize
		);
		this.canvas.removeEventListener(
			"pointerdown",
			this.boundPointerDown
		);
		this.canvas.removeEventListener(
			"pointermove",
			this.boundPointerMove
		);
		this.canvas.removeEventListener(
			"pointerup",
			this.boundPointerUp
		);
		this.canvas.removeEventListener(
			"pointerleave",
			this.boundPointerUp
		);
		this.canvas.removeEventListener(
			"touchstart",
			this.boundPreventDefault
		);
		
		// Clean up WebGL resources
		this.gl.deleteBuffer(this.positionBuffer);
		this.gl.deleteVertexArray(this.vao);
		this.gl.deleteProgram(this.program);
	
	}

}

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