first commit

This commit is contained in:
Christian Lawson-Perfect 2025-02-09 20:31:56 +00:00
commit 58eea569ae
5 changed files with 7753 additions and 0 deletions

1
.gitignore vendored Normal file
View file

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

4765
culori.js Normal file

File diff suppressed because it is too large Load diff

209
index.html Normal file
View file

@ -0,0 +1,209 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Which colours are similar?</title>
<script type="module" src="script.js"></script>
<style>
textarea {
width: 100%;
resize: vertical;
height: 5em;
}
svg {
width: 90svmin;
height: 90svmin;
& .label {
dominant-baseline: middle;
font-size: 12px;
stroke: white;
stroke-width: 5px;
paint-order: stroke fill;
}
& g {
& .mark {
stroke-width: 1;
stroke: black;
&.named {
stroke: white;
}
}
}
}
</style>
</head>
<body>
<header>
<h1>Which colours are similar?</h1>
</header>
<main>
<p><label for="color_defs">Unnamed colours, one on each line:</label></p>
<textarea id="color_defs">
hsl(0,50%,50%);
hsl(120,50%,50%);
hsl(1.7, 64.5%, 18%);
hsl(1.7, 64.5%, 28%);
hsl(204,72%,50%);
hsl(204,72%,95%);
hsl(30, 20%, 50%);
hsl(30,20%,50%);
hsl(30, 20%, 95%);
hsl(30,20%,95%);
#000000;
#222;
#23527c;
#269abc;
#2b542c;
#31708f;
#31b0d5;
#333;
#333333;
#337ab7;
#34444f;
#3c763d;
#46b8da;
#4cae4c;
#555555;
#5bc0de;
#66afe9;
#67b168;
#737373;
#777777;
#843534;
#85c3eb;
#888888;
#8a6d3b;
#999;
#a2d1f0;
#a6e1ec;
#a94442;
#ac2925;
#adadad;
#bce8f1;
#c9302c;
#cacaca;
#ccc;
#cccccc;
#ccffcc;
#ce8483;
#d43f3a;
#d9534f;
#d9edf7;
#dcdcdc;
#ddd;
#dddddd;
#e0e0e0;
#e3e3e3;
#e4b9c0;
#e5e5e5;
#e6e6e6;
#e8e8e8;
#ebccd1;
#eee;
#f2dede;
#f5f5f5;
#f7e1b5;
#faebcc;
#fcf8e3;
#fff;
#ffffff;
</textarea>
<p><label for="named_colors">Named colours, one on each line</label></p>
<textarea id="named_colors"> --alert-bg: #191919;
--alert-bg: #f5f5f5;
--button-bg: #333333;
--button-bg: #e9e9e9;
--button-border-color: #585858;
--button-border-color: #b0b0b0;
--code-bg: #f9f2f4;
--code-color: #4b504e;
--danger-alert-bg: #820000;
--danger-alert-bg: #be0000;
--danger-alert-color: #ffdfd1;
--danger-button-bg: #be0000;
--danger-button-bg: #be0000;
--danger-button-border-color: #650000;
--danger-button-border-color: #650000;
--danger-color: #d40000;
--danger-color: #ffbaad;
--help-color: #333333;
--help-color: #dbdbdb;
--hr-color: #eeeeee;
--info-alert-bg: #003469;
--info-alert-bg: #deeeff;
--info-alert-color: #002e5b;
--info-alert-color: #e6f8ff;
--info-button-bg: #324fad;
--info-button-bg: #324fad;
--info-button-border-color: #060060;
--info-button-border-color: #060060;
--info-color: #0c6dcf;
--info-color: #b8c9ff;
--input-bg: #111111;
--input-bg: #f8f8f8;
--link-color: #0000ee;
--link-color: #7cd6ff;
--main-color: #a2d1f0;
--main-darker: #85c3eb;
--muted-alert-bg: #191919;
--muted-alert-bg: #e9e9e9;
--muted-alert-color: #eeeeee;
--muted-button-bg: #636363;
--muted-button-bg: #636363;
--muted-button-border-color: #444444;
--muted-button-border-color: #444444;
--muted-color: #636363;
--muted-color: #cacbcd;
--pre-border: #cccccc;
--primary-button-bg: #00626f;
--primary-button-bg: #76f2ff;
--primary-button-border-color: #008996;
--primary-button-border-color: #00a6bf;
--score-correct-color: #00e254;
--score-partial-color: #bc8929;
--score-partial-color: #d1992e;
--success-alert-bg: #005200;
--success-alert-bg: #7bffac;
--success-alert-color: #003400;
--success-alert-color: #7cffad;
--success-button-bg: #006400;
--success-button-bg: #008000;
--success-button-border-color: #008000;
--success-button-border-color: #2f9a2b;
--success-color: #008000;
--success-color: #7cffad;
--table-border-color: #666666;
--table-border-color: #cccccc;
--td-bg: #1f1f1f;
--td-bg: #f8f8f8;;
--td-stripe-bg: #f9f9f9;
--td-warning-color: #996700;
--th-bg: #222222;
--th-bg: #e6e6e6;
--top-nav-bg: #213f52;
--top-nav-bg: #a2d1f0;
--top-nav-border:#19465f;
--top-nav-border: #85c3eb;
--top-nav-muted-color: #444444;
--top-nav-muted-color: #d8d8d8;
--warning-alert-bg: #9b2f00;
--warning-alert-bg: #fff485;
--warning-alert-color: #5e2e00;
--warning-alert-color: #fff486;
--warning-button-bg: #bd4800;
--warning-button-bg: #ffc251;
--warning-button-border-color: #a93c00;
--warning-button-border-color: #e2a62e;
--warning-color: #996700;
--warning-color: #fdc04f;
</textarea>
<p><strong>Key</strong>: luminance <strong></strong>, hue <strong></strong>, chroma <strong>roundness</strong> </p>
<svg id="display" viewBox="-50 -50 600 600"></svg>
</main>
</body>
</html>

2667
numbas_styles.css Normal file

File diff suppressed because one or more lines are too long

111
script.js Normal file
View file

@ -0,0 +1,111 @@
import * as culori from './culori.js';
window.culori = culori;
const convertToOklch = culori.converter('oklch');
function parse_oklch(def) {
return convertToOklch(culori.parse(def));
}
const color_defs_input = document.getElementById('color_defs');
const named_colors_input = document.getElementById('named_colors');
const display = document.getElementById('display');
function el(name, attr, content) {
const element = document.createElementNS('http://www.w3.org/2000/svg', name);
if(attr) {
for(let [k,v] of Object.entries(attr)) {
element.setAttribute(k, v);
}
}
if(content) {
element.innerHTML = content;
}
return element;
}
function update() {
const defs = Array.from(new Set(color_defs_input.value.split(/\n/g).map(line=>line.trim().replace(';','')).filter(l=>l))).toSorted();
const colors = defs.map(def => { return {def, color: parse_oklch(def)} });
const named_colors = named_colors_input.value.trim().split(/\n/g).map(line=>line.trim().match(/.*(--.*?):\s*(.*);/)).filter(m=>m).map(([_,name,def]) => {return {name,def,color: parse_oklch(def)}});
display.innerHTML = '';
const [width, height] = [500, 500];
function project_color(color) {
const {c,h,l} = color;
const r = (1 - Math.sqrt(1-l)) * width*0.4 + width*0.1;
const an = Math.PI / 180 * (h || 0);
return {x: Math.cos(an) * r + width/2, y: Math.sin(an) * r + height/2};
}
const margin = 0.1;
colors.forEach(({def,color},i) => {
const jitter = 1;
const {c,h,l} = color;
const {x,y} = project_color(color);
const size = 10;
const g = el('g', {transform: `translate(${x} ${y})`});
const radius = size * c/0.2;
const mark = el('rect', {x:-size, y:-size, width:2*size, height: 2*size, rx: radius, ry: radius, fill: def, class: 'mark'});
g.append(mark);
const name = el('text', {x: x + size*1.2,y: y, class: 'label'}, def);
display.append(g);
mark.addEventListener('pointerover', () => display.append(name));
mark.addEventListener('pointerleave', () => display.removeChild(name));
})
named_colors.forEach(({def,name,color},i) => {
if(color === undefined) {
return;
}
const {c,h,l} = color;
const {x,y} = project_color(color);
const size = 10;
const g = el('g', {transform: `translate(${x} ${y})`});
const radius = size * c/0.2;
const mark = el('polygon', {points: `-6,0 6,-6 6,6`, fill: def, class: 'mark named'});
g.append(mark);
const tooltip = el('text', {x: x + size*1.2,y: y, class: 'label'}, `${name}: ${def}`);
display.append(g);
mark.addEventListener('pointerover', () => display.append(tooltip));
mark.addEventListener('pointerleave', () => display.removeChild(tooltip));
})
}
color_defs_input.addEventListener('input', update);
update();
function analyse_stylesheet(css) {
const colors = [];
const lines = css.split('\n');
let block = null;
let selector = null;
lines.forEach((line,i) => {
const bm = line.match(/^(\S.*?)\s*\{?$/);
if(bm) {
block = i;
selector = bm[1].trim();
} else {
const m = line.match(/\s*([^:]+):.*?(#[a-f0-9]{3,6}|(?:rgb|oklch|hsl)\([^\)]*?\))/i);
if(m) {
const [_, rule, color] = m;
console.log(selector, rule, color);
colors.push({line:i, selector, rule, color})
}
}
});
colors.sort((a,b) => a.color < b.color ? -1 : a.color > b.color ? 1 : 0);
console.table(colors);
}