Skip to content

Commit

Permalink
add tests, convert almost all the rest of the lints
Browse files Browse the repository at this point in the history
  • Loading branch information
mcnuttandrew committed Feb 14, 2024
1 parent 24ca96a commit 53aa7b4
Show file tree
Hide file tree
Showing 34 changed files with 851 additions and 628 deletions.
1 change: 1 addition & 0 deletions LintLanguageDocs.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Color Manipulations:
Maps:
{map: Variable | Value[], func: Operation, varb: Variable}
{sort: Variable | Value[], func: Operation, varb: Variable}
{reverse: Variable | Value[]}
{filter: Variable | Value[], func: EXPR, varb: Variable}

```yaml
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,41 @@ First time you start it up you should also run `yarn prep data`
- [ ] Tour?
- [ ] roles, palette level semantics
- [ ] Design adjustments for smaller screens
- [ ] Language Docs??
- [x] Make lints fast / non blocking as much as possible

# Language todos

- [ ] Clone Rule
- [ ] "No out of gamut"
- [ ] Affect rules
- [ ] Add more blame robustness, may pay to try to reason across all of the operator families (insight: keep a list of the blamable variables in the environment to support tracing)
- [ ] per cols 4 all: color blindness metric should maybe be sensitive to task?
- [ ] Sequential check fix is incorrect for things with equi-ligthness
- [ ] Macros story: "not similar", "sequences", "where": { "!=": {"left": "index(a)", "right": "index(b)"} },

# General Todos

- [ ] Make name discrim hueristc fix more resilient
- [ ] Merge the info and fixes tooltips in the lints
- [ ] Color Space selections should persist
- [ ] Make name discrim hueristc fix more resilient, see switching to basic colors
- [ ] Search palettes-by-lint screen
- [ ] Drag to re-order points?
- [ ] off by one persistance error in undo/redo
- [ ] Changing spaces is pretty bad on lab <-> oklab, cf ("#35ffbf", "#87b995", "#e84f82")
- [ ] "Easy on ramp" progressive disclosure
- [ ] Labels, tooltips, etc
- [ ] Handles get stuck on channel picker again, ugh
- [ ] Bug: Color channel usage slightly cursed (doesn't update positions correctly)
- [ ] Bug: rotate in polar coordinates doesn't work right
- [ ] Performance stuff for linter (separate message generation into something very throttled, maybe move to web workers? Cache as hard as possible)
- [ ] LCH colors upside down god damn it

Parameterization epic:

- [ ] Allow no palettes, allows renaming of non-current palettes, which would be enable by:
- [ ] Parameterize the scatter column completely, allow edits to the compare stuff
- [ ] Hover broken on compare
- [ ] Hover broken on compare, also make sure it takes the right stringify (same as the color channel)
- [ ] Select compare from palettes drop down menu

# Nice to have
Expand Down
1 change: 1 addition & 0 deletions public/lint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"contrast": {
"type": "object",
"additionalProperties": false,
"description": "Compute the contrast between two colors using a given algorithm. The algorithm can be APCA, WCAG21, Michelson, Weber, Lstar or DeltaPhi.",
"properties": {
"left": { "$ref": "#/definitions/LintRef" },
"right": { "$ref": "#/definitions/LintRef" },
Expand Down
196 changes: 161 additions & 35 deletions src/lib/ColorLint.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,168 @@
import { expect, test } from "vitest";

import { Color } from "./Color";
import type { Palette } from "../stores/color-store";
import { makePalFromHexes } from "./utils";

import ColorNameDiscriminability, { getName } from "./lints/name-discrim";
import BUILT_INS from "./lints/built-in-lints";
import { CreateCustomLint } from "./lints/CustomLint";
import { suggestLintFix } from "./linter-tools/lint-fixer";

function makePalFromHexes(hexes: string[]): Palette {
return {
colors: hexes.map((hex) => Color.colorFromHex(hex, "lab")),
background: Color.colorFromHex("#ffffff", "lab"),
name: "test",
type: "categorical",
evalConfig: {},
colorSpace: "lab",
};
}
// Lints
import AvoidExtremes from "./lints/avoid-extremes";
import BackgroundContrast from "./lints/background-contrast";
import CatOrderSimilarity from "./lints/cat-order-similarity";
import ColorBlindness from "./lints/color-blindness";
import ColorNameDiscriminability, { getName } from "./lints/name-discrim";
import Fair from "./lints/fair";
import MaxColors from "./lints/max-colors";
import MutuallyDistinct from "./lints/mutually-distinct";
import SequentialOrder from "./lints/sequential-order";
import UglyColors from "./lints/ugly-colors";

const unique = <T>(arr: T[]): T[] => [...new Set(arr)];

test("ColorLint - AvoidExtremes", () => {
const examplePal = makePalFromHexes([
"#000000",
"#ffffff",
"#ff0000",
"#00ff00",
"#0000ff",
]);
const newLint = CreateCustomLint(AvoidExtremes);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(false);
expect(exampleLint.message).toMatchSnapshot();
});

test("ColorLint - MutuallyDistinct", () => {
const examplePal = makePalFromHexes([
"#000000",
"#ffffff",
"#ff0000",
"#00ff00",
"#0000ff",
]);
const newLint = CreateCustomLint(MutuallyDistinct);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

// TODO add a failing case
const examplePal2 = makePalFromHexes(["#d2b48c", "#f5f5dc", "#d7fcef"]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - MaxColors", () => {
const examplePal = makePalFromHexes(["#000000"]);
const newLint = CreateCustomLint(MaxColors);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromHexes([...new Array(20)].map(() => "#000000"));
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - UglyColors", () => {
const examplePal = makePalFromHexes(["#000000"]);
const newLint = CreateCustomLint(UglyColors);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromHexes(["#000000", "#56FF22"]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - Fair Nominal", () => {
const examplePal = makePalFromHexes(["#000000"]);
const newLint = CreateCustomLint(Fair[0]);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromHexes(["#debdb5", "#2a2a2a", "#76fc00"]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - Fair Sequential", () => {
const examplePal = makePalFromHexes(["#000000"]);
const newLint = CreateCustomLint(Fair[1]);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromHexes(["#debdb5", "#2a2a2a", "#76fc00"]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - SequentialOrder", () => {
const examplePal = makePalFromHexes([
"#0084a9",
"#009de5",
"#5fb1ff",
"#bbc3ff",
"#ecddff",
]);
const newLint = CreateCustomLint(SequentialOrder);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromHexes([
"#0084a9",
"#009de5",
"#5fb1ff",
"#ecddff",
"#bbc3ff",
]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - CatOrderSimilarity", () => {
const examplePal = makePalFromHexes([
"#0084a9",
"#009de5",
"#8ca9fa",
"#bbc3ff",
"#ecddff",
]);
const newLint = CreateCustomLint(CatOrderSimilarity);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromHexes([
"#0084a9",
"#009de5",
"#5fb1ff",
"#bbc3ff",
"#ecddff",
]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - ColorNameDiscriminability", async () => {
const examplePal = makePalFromHexes(["#5260d1", "#005ebe"]);
const exampleLint = new ColorNameDiscriminability(examplePal).run();
const lint = CreateCustomLint(ColorNameDiscriminability);
const exampleLint = new lint(examplePal).run();
expect(exampleLint.passes).toBe(false);
expect(exampleLint.message).toBe(
"Color Name discriminability check failed. The following color names are repeated: Royalblue (#5260d1, #005ebe)"
"The following pairs of colors have the same name: #5260d1 and #005ebe"
);
const fix = await suggestLintFix(examplePal, exampleLint);
const oldColorNames = unique<string>(
Expand All @@ -51,54 +187,44 @@ test("ColorLint - ColorBlind", async () => {
"#00becf",
];
const examplePal = makePalFromHexes(tableau10);
const cbDeuteranopia = CreateCustomLint(
BUILT_INS.find((x) => x.id === "colorblind-friendly-deuteranopia-built-in")!
);
const cbDeuteranopia = CreateCustomLint(ColorBlindness[0]);
const exampleLint1 = new cbDeuteranopia(examplePal).run();
expect(exampleLint1.passes).toBe(false);
expect(exampleLint1.message).toMatchSnapshot();

const cbProtanopia = CreateCustomLint(
BUILT_INS.find((x) => x.id === "colorblind-friendly-protanopia-built-in")!
);
const cbProtanopia = CreateCustomLint(ColorBlindness[1]);
const exampleLint2 = new cbProtanopia(examplePal).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();

const cbTritanopia = CreateCustomLint(
BUILT_INS.find((x) => x.id === "colorblind-friendly-tritanopia-built-in")!
);
const cbTritanopia = CreateCustomLint(ColorBlindness[2]);
const exampleLint3 = new cbTritanopia(examplePal).run();
expect(exampleLint3.passes).toBe(false);
expect(exampleLint3.message).toMatchSnapshot();

const cbGrayscale = CreateCustomLint(
BUILT_INS.find((x) => x.id === "colorblind-friendly-grayscale-built-in")!
);
const cbGrayscale = CreateCustomLint(ColorBlindness[3]);
const exampleLint4 = new cbGrayscale(examplePal).run();
expect(exampleLint4.passes).toBe(false);
expect(exampleLint4.message).toMatchSnapshot();
});

const ughWhat = ["#00ffff", "#00faff", "#00e4ff", "#fdfdfc", "#00ffff"];
test("ColorLint - BackgroundDifferentiability", async () => {
test("ColorLint - Background Contrast", async () => {
const examplePal = makePalFromHexes(ughWhat);
const BackgroundDifferentiability = CreateCustomLint(
BUILT_INS.find((x) => x.id === "background-contrast-built-in")!
);
const exampleLint = new BackgroundDifferentiability(examplePal).run();
const BackgroundContrastLint = CreateCustomLint(BackgroundContrast);
const exampleLint = new BackgroundContrastLint(examplePal).run();
expect(exampleLint.passes).toBe(false);
expect(exampleLint.message).toBe(
"This palette has some colors (#fdfdfc) that are close to the background color"
"These colors (#fdfdfc) do not have a sufficient contrast ratio with the background and may be hard to discriminate in some contexts."
);
const fix = await suggestLintFix(examplePal, exampleLint).then((x) => x[0]);
expect(fix.colors.map((x) => x.toHex())).toMatchSnapshot();

examplePal.background = Color.colorFromHex("#00e4ff", "lab");
const exampleLint2 = new BackgroundDifferentiability(examplePal).run();
const exampleLint2 = new BackgroundContrastLint(examplePal).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toBe(
"This palette has some colors (#0ff, #00faff, #00e4ff, #0ff) that are close to the background color"
"These colors (#00e4ff) do not have a sufficient contrast ratio with the background and may be hard to discriminate in some contexts."
);
const fix2 = await suggestLintFix(examplePal, exampleLint2).then((x) => x[0]);
expect(fix2.colors.map((x) => x.toHex())).toMatchSnapshot();
Expand Down
34 changes: 32 additions & 2 deletions src/lib/__snapshots__/ColorLint.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`ColorLint - BackgroundDifferentiability 1`] = `
exports[`ColorLint - AvoidExtremes 1`] = `"Colors at either end of the lightness spectrum #000, #fff are hard to discriminate in some contexts, and are sometimes advised against"`;

exports[`ColorLint - Background Contrast 1`] = `
[
"#0ff",
"#00faff",
Expand All @@ -10,7 +12,7 @@ exports[`ColorLint - BackgroundDifferentiability 1`] = `
]
`;

exports[`ColorLint - BackgroundDifferentiability 2`] = `
exports[`ColorLint - Background Contrast 2`] = `
[
"#007070",
"#006f72",
Expand All @@ -20,10 +22,38 @@ exports[`ColorLint - BackgroundDifferentiability 2`] = `
]
`;

exports[`ColorLint - CatOrderSimilarity 1`] = `"Some sequences of colors are too similar based on dE scores: . Try reordering them or making them more distinguishable"`;

exports[`ColorLint - CatOrderSimilarity 2`] = `"Some sequences of colors are too similar based on dE scores: #009de5 and #5fb1ff. Try reordering them or making them more distinguishable"`;

exports[`ColorLint - ColorBlind 1`] = `"This palette is not colorblind friendly for deuteranopia color blindness (ie can't see green). The following pairs are undifferentiable: (#0078b4 and #8c69bc, #ff7e0e and #c4bc27, #3d9f2f and #da2827, #e179c1 and #00becf)"`;

exports[`ColorLint - ColorBlind 2`] = `"This palette is not colorblind friendly for protanopia color blindness (ie can't see red). The following pairs are undifferentiable: (#0078b4 and #8c69bc, #ff7e0e and #3d9f2f, #da2827 and #8e564b)"`;

exports[`ColorLint - ColorBlind 3`] = `"This palette is not colorblind friendly for tritanopia color blindness (ie can't see blue). The following pairs are undifferentiable: (#ff7e0e and #e179c1, #8c69bc and #7f7f7f)"`;

exports[`ColorLint - ColorBlind 4`] = `"This palette is not colorblind friendly for grayscale color blindness . The following pairs are undifferentiable: (#0078b4 and #da2827, #0078b4 and #8c69bc, #0078b4 and #8e564b, #0078b4 and #7f7f7f, #ff7e0e and #3d9f2f, #ff7e0e and #e179c1, #ff7e0e and #c4bc27, #ff7e0e and #00becf, #3d9f2f and #8c69bc, #3d9f2f and #e179c1, #3d9f2f and #7f7f7f, #da2827 and #8c69bc, #da2827 and #8e564b, #da2827 and #7f7f7f, #8c69bc and #8e564b, #8c69bc and #7f7f7f, #e179c1 and #c4bc27, #e179c1 and #00becf, #c4bc27 and #00becf)"`;

exports[`ColorLint - Fair Nominal 1`] = `"This palette is unfair (meaning that some values may unduely stand out). Note that this check is naturally at odds with color blind friendly palettes. Maximum chroma range: 80, maximum luminance range: 50."`;

exports[`ColorLint - Fair Nominal 2`] = `"This palette is unfair (meaning that some values may unduely stand out). Note that this check is naturally at odds with color blind friendly palettes. Maximum chroma range: 80, maximum luminance range: 50."`;

exports[`ColorLint - Fair Sequential 1`] = `"This palette is unfair (meaning that some values may unduely stand out). Note that this check is naturally at odds with color blind friendly palettes. Maximum chroma range: 50."`;

exports[`ColorLint - Fair Sequential 2`] = `"This palette is unfair (meaning that some values may unduely stand out). Note that this check is naturally at odds with color blind friendly palettes. Maximum chroma range: 50."`;

exports[`ColorLint - MaxColors 1`] = `"This palette has too many colors and may be hard to discriminate in some contexts. Maximum: 10."`;

exports[`ColorLint - MaxColors 2`] = `"This palette has too many colors and may be hard to discriminate in some contexts. Maximum: 10."`;

exports[`ColorLint - MutuallyDistinct 1`] = `"Some colors in this palette () are not differentiable from each other."`;

exports[`ColorLint - MutuallyDistinct 2`] = `"Some colors in this palette (#f5f5dc and #d7fcef) are not differentiable from each other."`;

exports[`ColorLint - SequentialOrder 1`] = `"This pal should be ordered by lightness if being used as a sequential palette. may be to blame."`;

exports[`ColorLint - SequentialOrder 2`] = `"This pal should be ordered by lightness if being used as a sequential palette. #ecddff, #bbc3ff may be to blame."`;

exports[`ColorLint - UglyColors 1`] = `"This palette has some colors (specifically ) that are close to what are known as ugly colors"`;

exports[`ColorLint - UglyColors 2`] = `"This palette has some colors (specifically #56ff22) that are close to what are known as ugly colors"`;
9 changes: 8 additions & 1 deletion src/lib/blindness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,15 @@ function dl_simulate_cvd(
return brettelFunctions[deficiency](color);
}

const simulationCache = new Map<string, Color>();
export default function simulate_cvd(
deficiency: DLDeficiency,
color: Color
): Color {
const key = deficiency + color.toString();
if (simulationCache.has(key)) {
return simulationCache.get(key)!;
}
const colorIOcolor = color.toColorIO();
const isachroma =
deficiency == "achromatopsia" || deficiency == "achromatomaly";
Expand All @@ -124,5 +129,7 @@ export default function simulate_cvd(
const newCoords = dl_simulate_cvd(deficiency, coords);
const newColorIO = new ColorIO(spaceName, newCoords).to(color.spaceName);

return color.fromChannels(newColorIO.coords);
const result = color.fromChannels(newColorIO.coords);
simulationCache.set(key, result);
return result;
}
Loading

0 comments on commit 53aa7b4

Please sign in to comment.