<div class="container">
	<main>
		<div class="content">
			<h1>Pixel Sorter</h1>
		</div>

		<div class="form-control">
			<input type="file" accept="image/*" class="js-input-image">
			<small>Tip: try uploading a <a href="https://unsplash.com/photos/sf9zGg5DnFw" target="_blank">picture of a landscape</a>.</small>
		</div>

		<div class="js-output-container" role="presentation"></div>
	</main>
	
	<footer class="footer content">
		<p>Pixels are sorted by:</p>

		<ol>
			<li>Light intensity (Y in <a href="https://en.wikipedia.org/wiki/YIQ">YIQ</a> colour space)</li>
			<li>Colour intensity (Chroma)</li>
		</ol>
	</footer>
</div>
$color-brand--primary: #08f;

body {
	background-color: #f8f8f8;
	color: #222;
	line-height: 1.6;
}

:focus {
	outline: .125rem solid $color-brand--primary;
}

label {
	display: block;
	font-weight: bold;
}

input {
	display: block;
}

canvas {
	display: block;
	height: auto !important;
	max-width: 100%;
	
	image-rendering: pixelated;
}

a {
	color: darken($color-brand--primary, 10%);
	
	&:hover,
	&:focus {
		color: darken($color-brand--primary, 20%);
	}
	
	&:active {
		color: darken($color-brand--primary, 30%);
	}
}

.container {
	max-width: 36em;
	margin-left: auto;
	margin-right: auto;
}

.form-control {
	margin-bottom: 1em;
}

.content {
	margin-top: 1em;
	margin-bottom: 1em;
}
.content > * {
	margin-bottom: 0;
}
.content > :first-child {
	margin-top: 0;
}
.content > * + * {
	margin-top: 1em;
}

.footer {
	font-size: smaller;
}
View Compiled
const shaderSortFragment = `
	const vec4  kRGBToYPrime = vec4(0.299, 0.587, 0.114, 0.0);
	const vec4  kRGBToI = vec4(0.596, -0.275, -0.321, 0.0);
	const vec4  kRGBToQ = vec4(0.212, -0.523, 0.311, 0.0);
	
	// Based on https://github.com/genekogan/Processing-Shader-Examples/blob/master/TextureShaders/data/hue.glsl
	vec4 getYIQC(vec4 color) {
		float YPrime = dot(color, kRGBToYPrime);
    float I = dot(color, kRGBToI);
    float Q = dot(color, kRGBToQ);

    float chroma = sqrt(I * I + Q * Q);

		return vec4(YPrime, I, Q, chroma);
	}

	// Compare colors by light intensity and color intensity
	bool compareColor(vec4 a, vec4 b) {
		vec4 aYIQC = getYIQC(a);
		vec4 bYIQC = getYIQC(b);

		if (aYIQC.x > bYIQC.x) {
			return true;
		}

		if (aYIQC.x == bYIQC.x && aYIQC.w > bYIQC.w) {
			return true;
		}

		return false;
	}

	uniform float uIteration;

	void main() {
		vec2 coord = gl_FragCoord.xy;
		bool checkPrevious = mod(coord.x + uIteration, 2.0) < 1.0;
		vec2 pixel = vec2(-1.0, 0.0) / resolution.xy;

		vec2 uv = coord / resolution.xy;
		vec4 current = texture2D(uTexture, uv);
		vec4 reference = texture2D(uTexture, checkPrevious ? uv - pixel : uv + pixel);

		if (checkPrevious) {
			if (compareColor(reference, current)) {
				gl_FragColor = reference;
				return;
			}
		} else {
			if (compareColor(current, reference)) {
				gl_FragColor = reference;
				return;
			}
		}

		gl_FragColor = current;
	}`;

const readFile = (file) => {
	const reader = new FileReader();
	
	return new Promise((resolve, reject) => {
		reader.addEventListener('load', () => resolve(reader.result));
		reader.readAsDataURL(file);
	});
};

const createPlane = () => {
	const fragmentShader = `uniform sampler2D uMap;
		varying vec2 vUv;

		void main() {
			gl_FragColor = texture2D(uMap, vUv);
		}`;
	
	const vertexShader = `varying vec2 vUv;
		void main() {
			vUv = uv;
			${THREE.ShaderChunk.begin_vertex}
			${THREE.ShaderChunk.project_vertex}
		}`;
	
	const geometry = new THREE.PlaneGeometry(2, 2, 1, 1);
	const material = new THREE.ShaderMaterial({
		uniforms: {
			uMap: { value: null },
		},
		fragmentShader,
		vertexShader,
	});
	
	return new THREE.Mesh(geometry, material);
};

const input = document.querySelector('.js-input-image');
const outputContainer = document.querySelector('.js-output-container');

const gpuComputeTextureSize = 512;
const plane = createPlane();

const camera = new THREE.Camera();
camera.position.z = 1;

const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(gpuComputeTextureSize, gpuComputeTextureSize);

const scene = new THREE.Scene();
scene.add(plane);

let gpuCompute,
		textureSorted,
		variableSorted;

const initGpuCompute = (initialTextureData = null, dispose = false) => {
	if (dispose) {
		variableSorted.renderTargets.forEach(rt => rt.dispose());
		textureSorted.dispose();
	}
	
	gpuCompute = new THREE.GPUComputationRenderer(gpuComputeTextureSize, gpuComputeTextureSize, renderer);
	textureSorted = gpuCompute.createTexture();
	
	const rowColors = new Array(gpuComputeTextureSize).fill(0).map((n, i) => ({
		r: (Math.sin(i) * .124 + 1) / 2,
		g: (Math.sin(i + .234) * .563 + 1) / 2,
		b: (Math.sin(i + .988) * .348 + 1) / 2,
	}));
	
	if (initialTextureData) {
		textureSorted.image.data.set(initialTextureData);
	} else {
		for (let i = 0, channels = 4; i < textureSorted.image.data.length; i += channels) {
			const pixelIndex = Math.floor(i / channels);
			const y = Math.floor(pixelIndex / gpuComputeTextureSize);
			
			const color = rowColors[(pixelIndex + y * 15838) % rowColors.length];
			
			textureSorted.image.data[i + 0] = color.r;
			textureSorted.image.data[i + 1] = color.g;
			textureSorted.image.data[i + 2] = color.b;
			textureSorted.image.data[i + 3] = 1;
		}	
	}

	variableSorted = gpuCompute.addVariable('uTexture', shaderSortFragment, textureSorted);
	gpuCompute.setVariableDependencies(variableSorted, [variableSorted]);

	const gpuComputeCompileError = gpuCompute.init();
	
	variableSorted.material.uniforms.uIteration = { value: 0 };

	if (gpuComputeCompileError !== null) {
		console.error(gpuComputeCompileError);
	}
};

const getImageData = (image, size = 1) => {
	const canvas = document.createElement('canvas');
	canvas.height = size;
	canvas.width = size;
	
	const context = canvas.getContext('2d');
	context.scale(1, -1);
	context.drawImage(image, 0, 0, size, -size);
	
	return context.getImageData(0, 0, size, size).data;
};

const animate = (callback) => {
	const update = () => {
		requestAnimationFrame(update);
		callback();
	};
	
	update();
};

initGpuCompute();
outputContainer.appendChild(renderer.domElement);

input.addEventListener('change', () => {
	const file = input.files[0];
	const image = new Image();
	
	if (!file) return;
	
	readFile(file)
		.then(dataUrl => new Promise((resolve, reject) => {
			image.addEventListener('load', () => resolve())
			image.src = dataUrl;
		}))
		.then(() => {
			const textureData = new Float32Array(getImageData(image, gpuComputeTextureSize))
				.map(value => value / 256);
		
			initGpuCompute(textureData, true);
		});
});

const render = () => {
	gpuCompute.compute();
	
	const texture = gpuCompute.getCurrentRenderTarget(variableSorted).texture;
	plane.material.uniforms.uMap.value = texture;
	variableSorted.material.uniforms.uIteration.value++;
	
	renderer.render(scene, camera);
};

animate(render);
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js
  2. https://threejs.org/examples/js/misc/GPUComputationRenderer.js