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:
commit
e4ea0b0b99
6 changed files with 183 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.make.*
|
BIN
clp-2023.jpg
Normal file
BIN
clp-2023.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 331 KiB |
BIN
colourful-monotiles.jpg
Normal file
BIN
colourful-monotiles.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 380 KiB |
48
index.html
Normal file
48
index.html
Normal 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
98
script.js
Normal 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
36
style.css
Normal 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;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue