First commit.

It filters the colours by doing matrix multiplication in JS on the pixel array. It could almost definitely be made faster using a WebGL shader.
This commit is contained in:
Christian Lawson-Perfect 2025-04-04 08:21:57 +00:00
commit e4ea0b0b99
6 changed files with 183 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.make.*

BIN
clp-2023.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

BIN
colourful-monotiles.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

48
index.html Normal file
View file

@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>CVD simulator</title>
<script type="module" src="script.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Colour vision deficiency simulator</h1>
<p>This page aims to give you a rough idea of what people with different colour vision see.</p>
</header>
<main>
<form>
<p>
<label>Pick an image, or drag and drop one: </label>
<input type="file" name="file" id="file" accept="image/*">
</p>
</form>
<div id="grid">
<figure id="normal">
<img src="colourful-monotiles.jpg">
<figcaption>Unmodified</figcaption>
</figure>
<figure id="protan">
<canvas></canvas>
<figcaption>Simulated protanomaly</figcaption>
</figure>
<figure id="deuteran">
<canvas></canvas>
<figcaption>Simulated deuteranomaly</figcaption>
</figure>
<figure id="tritan">
<canvas></canvas>
<figcaption>Simulated tritanomaly</figcaption>
</figure>
</div>
<p>This is based on the method described in <a href="https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html#Tutorial">A Physiologically-based Model for Simulation of Color Vision Deficiency</a>.</p>
<p>To simulate a colour vision deficiency, the <math><mo>(</mo><mi>r</mi><mo>,</mo><mi>g</mi><mo>,</mo><mi>b</mi><mo>)</mo></math> vector for each pixel is multiplied by a matrix representing the eye's response in each kind of colour receptor.</p>
</main>
<footer>
<p>made by <a href="https://somethingorotherwhatever.com">clp</a></h1>
</footer>
</body>
</html>

98
script.js Normal file
View file

@ -0,0 +1,98 @@
const img = document.querySelector('img');
function matmul(m,v) {
return m.map(r => r[0]*v[0] + r[1]*v[1] + r[2]*v[2]);
}
/* From https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html#Tutorial
*/
const protan = [
[0.152286 , 1.052583 , -0.204868],
[0.114503 , 0.786281 , 0.099216],
[-0.003882 , -0.048116 , 1.051998],
];
const deuteran = [
[0.367322 , 0.860646 , -0.227968],
[0.280085 , 0.672501 , 0.047413],
[-0.011820 , 0.042940 , 0.968881],
]
const tritan = [
[1.255528 , -0.076749 , -0.178779],
[-0.078411 , 0.930809 , 0.147602],
[0.004733 , 0.691367 , 0.303900],
];
async function filter(canvas, matrix) {
const {naturalWidth, naturalHeight} = img;
const width = Math.min(2*img.width, naturalWidth);
const height = naturalHeight/naturalWidth * width;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img,0,0,width,height);
const {data} = ctx.getImageData(0,0,width,height);
for(let i=0;i<data.length;i+=4) {
const v = data.slice(i,i+3);
const [r,g,b] = matmul(matrix, v);
data[i] = r;
data[i+1] = g;
data[i+2] = b;
}
const nimagedata = new ImageData(data,width,height);
ctx.putImageData(nimagedata,0,0);
}
async function go() {
console.log('loaded');
const sims = {
protan, deuteran, tritan
}
console.log(img.width, img.height);
Object.entries(sims).forEach(async ([k,m]) => {
const canvas = document.getElementById(k).querySelector('canvas');
const blob = await filter(canvas, m);
sim_img.src = URL.createObjectURL(blob);
})
}
if(img.complete) {
go();
} else {
img.addEventListener('load', go, {once:true});
}
document.body.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
})
function load_file(file) {
img.src = URL.createObjectURL(file);
img.addEventListener('load', go, {once:true});
}
document.body.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
const [file] = e.dataTransfer.files;
if(!file) {
return;
}
load_file(file);
})
document.getElementById('file').addEventListener('change', ({target}) => {
const [file] = target.files;
console.log(file);
if(!file) {
return;
}
load_file(file);
})

36
style.css Normal file
View file

@ -0,0 +1,36 @@
:root {
--spacing: 1em;
color-scheme: light dark;
}
body {
font-family: sans-serif;
}
#grid {
margin: 5svh 0;
display: grid;
grid-template: repeat(2,1fr) / repeat(2,1fr);
width: 90svw;
height: 90svh;
gap: var(--spacing);
}
figure {
max-height: 100%;
display: grid;
overflow: hidden;
grid-template-rows: 1fr min-content;
margin: 0;
}
figcaption {
text-align: center;
}
img, canvas {
width: 100%;
height: 100%;
object-fit: cover;
}