Shader Park and 2D


The swing looks sad because everyone went to play in the Shader Park :(

Midnight doubled its weekly posh ramen revenue! I also had a chance to chat with the creators of my favourite SDF plaything who gave me a bunch of tips and code samples to learn from.

I thought I'd celebrate that with a gentle yak shave before I wrap up my experiments with Shader Park for now.

Psst... You might want to read my previous notes on Shader Park before you continue: Shader Park is Kinda Neat and Midnight Shader.

Result (live version):

Implementation

let noiseScale = 2;
let timeScale = 0.15;

let s = enable2D(); // 🐐!

let n = noise(noiseScale * s + time * timeScale);
let n2 = noise(noiseScale * s + time * timeScale * 0.1 + n * 0.8);

let c = pow(1 - n2, 12);

color(vec3(c, c, c));

And here's the generated noise (interactive example here):

Using 2D mode

ShaderPark uses a 3D mode by default. We're only interested in projecting a flat noise texture onto the screen so we can save ourselves from the unnecessary Ray marching computation by enabling the 2D mode (kudos to Torin for mentioning this!).

let s = enable2D(); // 🐐!

Enabling 2D mode also requires tweaking some Three.js settings, mainly passing the canvas size to the shader.

uniforms.resolution = {
	name: 'resolution',
	value: new Vector2(canvasEl.clientWidth, canvasEl.clientHeight),
	type: 'vec2'
};

Jargon: uniforms are the parameters passed from the JS world into shaders. An example uniform value would be time which can be used for animation:

let render = () => {
	requestAnimationFrame(render);
	material.uniforms.time.value += getDeltaTime();
	renderer.render(scene, camera);
};

render();

Performance tweaks

Three.js is ca. 70-100kb in increased bundle size. That's noticeable on lower-end devices with poor internet connection. But, I still want my silk-smooth sheets animation to play on your fancy iPhone. We can solve this dilemma with progressive enhancement and dynamic imports. Here's an example in SvelteKit:

<script>
	import { onMount } from 'svelte';
	
	let canvasEl;
	let isWebGLReady = false;

	//  called client-side as soon as the component is mounted in DOM
	onMount(async () => {
		const {  /* ... */ WebGLRenderer } = await import('three');
		const { default: generatedShader } = await import('./overlay.sp');
		// ... (yes, this could be Promise.all, have a cookie 🥠)
		render();
		isWebGLReady = true;
	});
</script>

<canvas class:is-ready={isWebGLReady} bind:this={canvasEl}></canvas>

<style>
	canvas {
		/* ... */
		opacity: 0;
	}

	canvas.is-ready {
		animation: fadeIn 3s 0s ease-in-out both;
	}
</style>

Important bits:

Display the animation only when the modules have been imported and the Three.js scene set up:

<canvas class:is-ready={isWebGLReady}>

And:

canvas {
	/* ... */
	opacity: 0; /* 👈 */
}

Note that we're not using display: none to hide the <canvas> element as this would break the animation. That's because we need to pass the canvas dimensions to the shader as the resolution uniform, but .clientWidth and .clientHeight of elements with display: none is 0!

Alternative — drop Three.js completely:

I'm already transpiling SP code into GLSL during build time and I don't rely on many Three.js features so we could probably replace it with a single script. I'll leave that as an excuse to write another note (the yak is bald already).

Summary

With this approach the page will still load fast on slower internet connection and gently fade in the animation when it's ready.

Loading Three.js via dynamic imports increased its bundle size to 170 kb, but from the user perspective the change is not noticeable.

This is just a reminder that:

That's all for today. See you tomorrow!

Want to receive my work as I publish it? Subscribe here.

a giant foot-shaped snail with a house on its back. the house is still in construction, with a big crane towering above it The image is a stylized black-and-white illustration. In the lower left corner, there is a small, cozy-looking house with smoke rising from its chimney. The smoke, however, does not dissipate into the air but instead forms a dark, looming cloud. Within the cloud, the silhouette of a large, menacing face is visible, with its eyes and nose peeking through the darkness. The creature, perhaps a cat, appears to be watching over the house ominously, creating a sense of foreboding or unease.