diff --git a/LintLanguageDocs.md b/LintLanguageDocs.md index a696cd1d..45035b01 100644 --- a/LintLanguageDocs.md +++ b/LintLanguageDocs.md @@ -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 diff --git a/README.md b/README.md index ef06f981..627fd4f0 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,14 @@ 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 @@ -28,22 +32,26 @@ First time you start it up you should also run `yarn prep data` # 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 diff --git a/public/lint-schema.json b/public/lint-schema.json index eb2e98c1..f7a8b2c6 100644 --- a/public/lint-schema.json +++ b/public/lint-schema.json @@ -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" }, diff --git a/src/lib/ColorLint.test.ts b/src/lib/ColorLint.test.ts index 4cd62b42..c17d54b0 100644 --- a/src/lib/ColorLint.test.ts +++ b/src/lib/ColorLint.test.ts @@ -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 = (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( @@ -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(); diff --git a/src/lib/__snapshots__/ColorLint.test.ts.snap b/src/lib/__snapshots__/ColorLint.test.ts.snap index 33493975..59d5e5e1 100644 --- a/src/lib/__snapshots__/ColorLint.test.ts.snap +++ b/src/lib/__snapshots__/ColorLint.test.ts.snap @@ -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", @@ -10,7 +12,7 @@ exports[`ColorLint - BackgroundDifferentiability 1`] = ` ] `; -exports[`ColorLint - BackgroundDifferentiability 2`] = ` +exports[`ColorLint - Background Contrast 2`] = ` [ "#007070", "#006f72", @@ -20,6 +22,10 @@ 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)"`; @@ -27,3 +33,27 @@ exports[`ColorLint - ColorBlind 2`] = `"This palette is not colorblind friendly 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"`; diff --git a/src/lib/blindness.ts b/src/lib/blindness.ts index 2cf7667e..b93c0d92 100644 --- a/src/lib/blindness.ts +++ b/src/lib/blindness.ts @@ -111,10 +111,15 @@ function dl_simulate_cvd( return brettelFunctions[deficiency](color); } +const simulationCache = new Map(); 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"; @@ -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; } diff --git a/src/lib/lint-language/LintLanguage.test.ts b/src/lib/lint-language/LintLanguage.test.ts index 1732bea9..b6dd1653 100644 --- a/src/lib/lint-language/LintLanguage.test.ts +++ b/src/lib/lint-language/LintLanguage.test.ts @@ -475,12 +475,12 @@ test("LintLanguage Measure Contrast", () => { contrast: { left: colors[0], right: colors[1] }, algorithm: "APCA", }, - right: -107.88473522404158, + right: 107.88473522404158, }, }; const result = LLEval(program, [] as any); expect(prettyPrintLL(program)).toBe( - "contrast(#000, #fff, APCA) == -107.88473522404158" + "contrast(#000, #fff, APCA) == 107.88473522404158" ); expect(result.result).toBe(true); expect(result.blame).toStrictEqual([]); diff --git a/src/lib/lint-language/lint-language.ts b/src/lib/lint-language/lint-language.ts index ff74eb5d..d0099d3a 100644 --- a/src/lib/lint-language/lint-language.ts +++ b/src/lib/lint-language/lint-language.ts @@ -583,10 +583,27 @@ const getOp = ops.find((x) => { const pk = node[x.primaryKey]; if (!pk) return false; + // special short circuit bc theres a collision between Color and name + if (node.spaceName) { + return false; + } const allParamsFound = x.params.every((key) => key in node); const noExtraParams = Object.keys(node).every( (key) => x.params.includes(key) || key === x.primaryKey ); + const allowedParamsMessage = x.params.length + ? `Allowed params are ${x.params.map((x) => `"${x}"`).join(", ")}` + : "No params allowed" + Object.keys(node); + if (!allParamsFound) { + throw new Error( + `Missing params for ${x.primaryKey}. ${allowedParamsMessage}` + ); + } + if (!noExtraParams) { + throw new Error( + `Extra params for ${x.primaryKey}. ${allowedParamsMessage}` + ); + } return allParamsFound && noExtraParams; }); const getParams = (op: any, node: any) => @@ -603,13 +620,16 @@ const LLPairFunctionTypes = [ primaryKey: "deltaE", params: ["algorithm"] as string[], op: (valA: Color, valB: Color, params: Params) => - valA.deltaE(valB, params.algorithm as any), + valA.symmetricDeltaE(valB, params.algorithm as any), }, { primaryKey: "contrast", params: ["algorithm"] as string[], - op: (valA: Color, valB: Color, params: Params) => - valA.toColorIO().contrast(valB.toColorIO(), params.algorithm as any), + op: (valA: Color, valB: Color, params: Params) => { + const a = valA.toColorIO(); + const b = valB.toColorIO(); + return Math.abs(a.contrast(b, params.algorithm as any)); + }, }, ]; export class LLPairFunction extends LLNode { @@ -681,7 +701,7 @@ export class LLQuantifier extends LLNode { evaluate(env: Environment) { this.evalCheck(env); // weird restoration of type information to make the forward stuff work - const type = tryTypes([LLValueArray, LLValue], env.options); + const type = tryTypes([LLValueArray, LLValue, LLMap], env.options); const idxType = tryTypes([LLValue], env.options); // materialize all combinations of the variables for each of the inputs const inputData = this.input @@ -801,7 +821,7 @@ const reduceTypes = [ export class LLReduces extends LLNode { constructor( private type: (typeof reduceTypes)[number], - private children: LLValueArray | LLVariable + private children: LLValueArray | LLVariable | LLMap ) { super(); } @@ -855,13 +875,13 @@ export class LLReduces extends LLNode { } } -const mapTypes = ["map", "filter", "sort"] as const; +const mapTypes = ["map", "filter", "sort", "reverse"] as const; // example syntax // {map: colors, func: {cvdSim: {type: "protanomaly"}}, varb: "x"} export class LLMap extends LLNode { constructor( private type: (typeof mapTypes)[number], - private children: LLValueArray | LLVariable, + private children: LLValueArray | LLVariable | LLMap, private func: LLValueFunction | LLPairFunction | LLNumberOp, private varb: string ) { @@ -886,22 +906,29 @@ export class LLMap extends LLNode { case "filter": return { result: children.filter(evalFunc), env }; case "sort": - return { - result: children.map(evalFunc).sort(), - env, - }; + const childrenCopy = [...children].map(evalFunc); + childrenCopy.sort(); + return { result: childrenCopy, env }; + case "reverse": + const childrenCopy2 = [...children]; + childrenCopy2.reverse(); + return { result: childrenCopy2, env }; } } static tryToConstruct(node: any, options: OptionsConfig) { const op = mapTypes.find((x) => node[x]); if (!op) return false; - const childType = - node[op] && tryTypes([LLValueArray, LLVariable], options)(node[op]); - const varb = node.varb; + node[op] && + tryTypes([LLValueArray, LLVariable, LLMap], options)(node[op]); + let varb = node.varb; let func; if (op === "filter") { func = tryTypes([LLExpression], options)(node.func); + } else if (op === "reverse") { + // reverse doesn't take any arguments besides the target + varb = " "; + func = " "; } else { func = tryTypes( [LLValueFunction, LLPairFunction, LLNumberOp], @@ -932,7 +959,6 @@ function parseToAST(root: any, options: OptionsConfig) { const DEFAULT_OPTIONS = { debugParse: false, debugEval: false, - stages: false, debugCompare: false, }; export function LLEval( @@ -944,10 +970,7 @@ export function LLEval( const inputEnv = new Environment(palette, {}, opts, {}); const ast = parseToAST({ id: [root] }, opts); - if (options.stages) { - console.log(ast.toString()); - console.log("EVALUATION EVALUATION EVALUATION EVALUATION"); - } + const { result, env } = ast.evaluate(inputEnv); const blame = Object.entries(env.colorBlame) .filter((x) => x[1]) diff --git a/src/lib/lint-language/lint-type.ts b/src/lib/lint-language/lint-type.ts index 724afd4c..02f4866d 100644 --- a/src/lib/lint-language/lint-type.ts +++ b/src/lib/lint-language/lint-type.ts @@ -61,6 +61,7 @@ type LintPairOps = type LintMap = // | { map: LintVariable | LintValue[]; func: LintColorFunction | LintPairOps } // | { sort: LintVariable | LintValue[]; func: LintColorFunction | LintPairOps } + // | {reverse: LintVariable | LintValue[]} { filter: LintVariable | LintValue[]; func: LintExpression }; type LintReduce = Record< diff --git a/src/lib/linter-tools/lint-fixer.ts b/src/lib/linter-tools/lint-fixer.ts index ef111b6b..66b9ce30 100644 --- a/src/lib/linter-tools/lint-fixer.ts +++ b/src/lib/linter-tools/lint-fixer.ts @@ -1,16 +1,15 @@ import type { Palette } from "../../stores/color-store"; import { suggestFix } from "../api-calls"; import type { LintResult } from "../lints/ColorLint"; -import { manualLints } from "../linter"; import { Color } from "../Color"; export async function suggestLintAIFix( palette: Palette, - message: string, + lint: LintResult, engine: string ) { const colorSpace = palette.colorSpace; - return suggestFix(palette, message, engine as any).then((x) => { + return suggestFix(palette, lint.message, engine as any).then((x) => { if (x.length === 0) { throw new Error("No suggestions"); } @@ -30,23 +29,28 @@ export async function suggestLintAIFix( }); } -const fixDirectory: Record = {}; -manualLints.forEach((x) => { - const demo = new x({} as any); - const name = demo.name; - if (demo.hasHeuristicFix) { - fixDirectory[name] = x.suggestFix; - } -}); +export type LintFixer = (pal: Palette, lint: LintResult) => Promise; +import { fixBackgroundDifferentiability } from "../lints/background-contrast"; +import { fixDivergingOrder } from "../lints/diverging-order"; +import { fixColorNameDiscriminability } from "../lints/name-discrim"; +import { fixSequentialOrder } from "../lints/sequential-order"; +import { fixMaxColors } from "../lints/max-colors"; +const fixDirectory: Record = { + fixBackgroundDifferentiability, + fixDivergingOrder, + fixColorNameDiscriminability, + fixSequentialOrder, + fixMaxColors, +}; export async function suggestLintFix( palette: Palette, lint: LintResult, _engine?: string ): Promise { - if (fixDirectory[lint.name]) { - return fixDirectory[lint.name](palette); + if (fixDirectory[lint.subscribedFix]) { + return fixDirectory[lint.subscribedFix](palette, lint); } - console.log("check failed", lint.name, fixDirectory); + console.log("check failed", lint, fixDirectory); return []; } diff --git a/src/lib/linter-tools/lint-worker.worker.ts b/src/lib/linter-tools/lint-worker.worker.ts index d33a2597..52600347 100644 --- a/src/lib/linter-tools/lint-worker.worker.ts +++ b/src/lib/linter-tools/lint-worker.worker.ts @@ -65,7 +65,7 @@ async function dispatch(cmd: Command) { description: x.description, isCustom: x.isCustom, taskTypes: x.taskTypes as any, - hasHeuristicFix: x.hasHeuristicFix, + subscribedFix: x.subscribedFix, }; }); simpleLintCache.set(cmd.content, result); diff --git a/src/lib/linter.ts b/src/lib/linter.ts index b4deb0b6..2acb81a3 100644 --- a/src/lib/linter.ts +++ b/src/lib/linter.ts @@ -1,29 +1,13 @@ import { ColorLint } from "./lints/ColorLint"; import type { Palette } from "../stores/color-store"; -import NameDiscrim from "./lints/name-discrim"; import Discrims from "./lints/size-discrim"; -import ColorSimilarity from "./lints/color-similarity"; -import BackgroundDifferentiability from "./lints/background-differentiability"; -import SequentialOrder from "./lints/sequential-order"; import DivergingOrder from "./lints/diverging-order"; -// import BackgroundContrast from "./lints/contrast"; -import Fair from "./lints/fair"; import EvenDistribution from "./lints/even-distribution"; import type { CustomLint } from "./lints/CustomLint"; import { CreateCustomLint } from "./lints/CustomLint"; -export const manualLints = [ - NameDiscrim, - ...Discrims, - ColorSimilarity, - BackgroundDifferentiability, - SequentialOrder, - DivergingOrder, - // BackgroundContrast, - ...Fair, - // EvenDistribution, -]; +export const manualLints = [...Discrims, DivergingOrder]; export function runLintChecks( palette: Palette, diff --git a/src/lib/lints/ColorLint.ts b/src/lib/lints/ColorLint.ts index 26485083..d74f7133 100644 --- a/src/lib/lints/ColorLint.ts +++ b/src/lib/lints/ColorLint.ts @@ -11,7 +11,7 @@ export interface LintResult { description: string; isCustom: false | string; taskTypes: TaskType[]; - hasHeuristicFix: boolean; + subscribedFix: string; } export class ColorLint { @@ -21,13 +21,13 @@ export class ColorLint { checkData: CheckData; palette: Palette; message: string = ""; - hasHeuristicFix: boolean = false; config: { val?: ParamType } = {}; isCustom: false | string = false; group: string = ""; description: string = ""; blameMode: "pair" | "single" | "none" = "none"; level: LintLevel = "error"; + subscribedFix: string = "none"; constructor(Palette: Palette) { this.palette = Palette; @@ -35,22 +35,19 @@ export class ColorLint { this.passes = false; } - run() { - const { passCheck, data } = this._runCheck(); + run(options: any = {}) { + const { passCheck, data } = this._runCheck(options); this.passes = passCheck; this.checkData = data as CheckData; this.message = this.buildMessage(); return this; } - _runCheck(): { passCheck: boolean; data: CheckData } { + _runCheck(_options: any): { passCheck: boolean; data: CheckData } { return { passCheck: true, data: {} as CheckData }; } // Fail Message buildMessage(): string { return ""; } - static suggestFix(palette: Palette): Promise { - return Promise.resolve([]); - } } diff --git a/src/lib/lints/CustomLint.ts b/src/lib/lints/CustomLint.ts index f0fade5e..ebc1544b 100644 --- a/src/lib/lints/CustomLint.ts +++ b/src/lib/lints/CustomLint.ts @@ -1,4 +1,3 @@ -import type { LintProgram } from "../lint-language/lint-type"; import { ColorLint } from "./ColorLint"; import type { TaskType } from "./ColorLint"; import { @@ -18,6 +17,7 @@ export interface CustomLint { failMessage: string; id: string; blameMode: "pair" | "single" | "none"; + subscribedFix?: string; } export function CreateCustomLint(props: CustomLint) { @@ -27,24 +27,24 @@ export function CreateCustomLint(props: CustomLint) { level = props.level; group = props.group; description = props.description; - hasHeuristicFix = false; isCustom = props.id; blameMode = props.blameMode; + subscribedFix = props.subscribedFix || "none"; - _runCheck() { + _runCheck(options: any) { const prog = Json.parse(props.program); const { blame, result } = LLEval(prog, this.palette, { debugCompare: false, + ...options, }); if (result) return { passCheck: true, data: blame }; - return { - passCheck: result, - data: - props.blameMode !== "none" - ? permutativeBlame(prog, this.palette, props.blameMode) - : [], - }; + let newBlame: number[] | number[][] = []; + if (this.blameMode !== "none") { + newBlame = permutativeBlame(prog, this.palette, this.blameMode); + } + + return { passCheck: result, data: newBlame }; } buildMessage() { diff --git a/src/lib/lints/__snapshots__/avoid-extremes.ts.snap b/src/lib/lints/__snapshots__/avoid-extremes.ts.snap new file mode 100644 index 00000000..562529ca --- /dev/null +++ b/src/lib/lints/__snapshots__/avoid-extremes.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +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"`; diff --git a/src/lib/lints/avoid-extremes.ts b/src/lib/lints/avoid-extremes.ts new file mode 100644 index 00000000..e674bbce --- /dev/null +++ b/src/lib/lints/avoid-extremes.ts @@ -0,0 +1,33 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; + +// https://www.sciencedirect.com/science/article/pii/S0167947308005549?casa_token=s8jmZqboaYgAAAAA:7lsAu7YUHVBTQA_eaKJ_3FFGv309684j_NTisGO9mIr3UZNIJ6hlAlxPQo04xzsowG7-dH0vzm4 +const lint: CustomLint = { + name: "Avoid extreme colors", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varb: "a", + predicate: { + all: { + in: ["#000000", "#ffffff"], + varb: "b", + predicate: { + not: { similar: { left: "a", right: "b", threshold: 12 } }, + }, + }, + }, + }, + }), + + taskTypes: ["sequential", "diverging", "categorical"] as const, + level: "warning", + group: "design", + description: `Colors at either end of the lightness spectrum can be hard to discriminate in some contexts, and are sometimes advised against.`, + failMessage: `Colors at either end of the lightness spectrum {{blame}} are hard to discriminate in some contexts, and are sometimes advised against`, + id: "extreme-colors-built-in", + blameMode: "single", +}; +export default lint; diff --git a/src/lib/lints/background-contrast.ts b/src/lib/lints/background-contrast.ts new file mode 100644 index 00000000..eb2f796c --- /dev/null +++ b/src/lib/lints/background-contrast.ts @@ -0,0 +1,67 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; +import { Color } from "../Color"; +import type { LintFixer } from "../linter-tools/lint-fixer"; + +const getColorsCloseToBackground = (colors: Color[], background: Color) => { + return colors.reduce((acc, x, idx) => { + const pass = x.symmetricDeltaE(background) < 15; + return pass ? [...acc, idx] : acc; + }, [] as number[]); +}; + +const lint: CustomLint = { + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varb: "a", + + predicate: { + ">": { + left: { + contrast: { left: "a", right: "background" }, + algorithm: "WCAG21", + }, + right: 1.1, + }, + }, + }, + }), + name: "Background Contrast", + taskTypes: ["sequential", "diverging", "categorical"] as const, + level: "error", + group: "accessibility", + description: `All colors in a palette should have a sufficient contrast ratio with the background color. This is because if they are not, then they will not be differentiable from each other in some contexts. Valid algorithms are "APCA", "WCAG21", "Michelson", "Weber", "Lstar", "DeltaPhi".`, + failMessage: `These colors ({{blame}}) do not have a sufficient contrast ratio with the background and may be hard to discriminate in some contexts.`, + id: "background-contrast-built-in", + blameMode: "single" as const, + subscribedFix: "fixBackgroundDifferentiability", +}; +export default lint; + +export const fixBackgroundDifferentiability: LintFixer = async (palette) => { + const { colors, background, colorSpace } = palette; + const backgroundL = background.toColorIO().to("lab").coords[0]; + const bgCloserToWhite = backgroundL > 50; + const clamp = (x: number) => Math.max(0, Math.min(100, x)); + const newL = clamp(!bgCloserToWhite ? backgroundL * 1.5 : backgroundL * 0.5); + const colorsCloseToBackground = getColorsCloseToBackground( + colors, + background + ); + const newColors = colors.map((x, idx) => { + if (!colorsCloseToBackground.includes(idx)) { + return x; + } + const color = colors[idx]; + const newColor = Color.toColorSpace(color, "lab"); + const [_l, a, b] = newColor.toChannels(); + return Color.toColorSpace( + newColor.fromChannels([newL, a, b]), + colorSpace as any + ); + }); + return [{ ...palette, colors: newColors }]; +}; diff --git a/src/lib/lints/background-differentiability.ts b/src/lib/lints/background-differentiability.ts deleted file mode 100644 index 19756643..00000000 --- a/src/lib/lints/background-differentiability.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ColorLint } from "./ColorLint"; -import type { TaskType } from "./ColorLint"; -import { Color } from "../Color"; -import type { Palette } from "../../stores/color-store"; - -const hexJoin = (colors: Color[]) => colors.map((x) => x.toHex()).join(", "); - -const getColorsCloseToBackground = (colors: Color[], background: Color) => { - return colors.reduce((acc, x, idx) => { - const pass = x.symmetricDeltaE(background) < 15; - return pass ? [...acc, idx] : acc; - }, [] as number[]); -}; - -export default class BackgroundDifferentiability extends ColorLint< - number[], - false -> { - name = "All colors differentiable from background"; - taskTypes = ["sequential", "diverging", "categorical"] as TaskType[]; - group = "accessibility"; - description: string = `All colors in a palette should be differentiable from the background color. This is because if they are not, then they will not be differentiable from each other in some contexts.`; - _runCheck() { - const { colors, background } = this.palette; - const colorsCloseToBackground = getColorsCloseToBackground( - colors, - background - ); - - const passCheck = colorsCloseToBackground.length === 0; - return { passCheck, data: colorsCloseToBackground }; - } - buildMessage(): string { - const colors = this.palette.colors; - const str = hexJoin(this.checkData.map((x) => colors[x])); - return `This palette has some colors (${str}) that are close to the background color`; - } - hasHeuristicFix = true; - static async suggestFix(palette: Palette) { - const { colors, background, colorSpace } = palette; - const backgroundL = background.toColorIO().to("lab").coords[0]; - const bgCloserToWhite = backgroundL > 50; - const clamp = (x: number) => Math.max(0, Math.min(100, x)); - const newL = clamp( - !bgCloserToWhite ? backgroundL * 1.5 : backgroundL * 0.5 - ); - const colorsCloseToBackground = getColorsCloseToBackground( - colors, - background - ); - const newColors = colors.map((x, idx) => { - if (!colorsCloseToBackground.includes(idx)) { - return x; - } - const color = colors[idx]; - const newColor = Color.toColorSpace(color, "lab"); - const [_l, a, b] = newColor.toChannels(); - return Color.toColorSpace( - newColor.fromChannels([newL, a, b]), - colorSpace as any - ); - }); - return [{ ...palette, colors: newColors }]; - } -} diff --git a/src/lib/lints/built-in-lints.ts b/src/lib/lints/built-in-lints.ts index 5294d3ab..42a291e9 100644 --- a/src/lib/lints/built-in-lints.ts +++ b/src/lib/lints/built-in-lints.ts @@ -1,185 +1,26 @@ import type { CustomLint } from "./CustomLint"; -import type { LintProgram } from "../lint-language/lint-type"; -import { JSONStringify } from "../utils"; -import type { TaskType } from "./ColorLint"; -const toString = (x: LintProgram) => JSONStringify(JSON.stringify(x)); -// @ts-ignore -const $schema = `${location.href}lint-schema.json`; +// lints +import AvoidExtremes from "./avoid-extremes"; +import BackgroundDifferentiability from "./background-contrast"; +import CatOrderSimilarity from "./cat-order-similarity"; +import ColorBlindness from "./color-blindness"; +import Fair from "./fair"; +import MaxColors from "./max-colors"; +import MutuallyDistinct from "./mutually-distinct"; +import SequentialOrder from "./sequential-order"; +import UglyColors from "./ugly-colors"; -const blindTypes = [ - "deuteranopia", - "protanopia", - "tritanopia", - "grayscale", -] as const; -const blindnessLabels: Record<(typeof blindTypes)[number], string> = { - deuteranopia: "(ie can't see green)", - protanopia: "(ie can't see red)", - tritanopia: "(ie can't see blue)", - grayscale: "", -}; const BUILT_INS: CustomLint[] = [ - // https://www.sciencedirect.com/science/article/pii/S0167947308005549?casa_token=s8jmZqboaYgAAAAA:7lsAu7YUHVBTQA_eaKJ_3FFGv309684j_NTisGO9mIr3UZNIJ6hlAlxPQo04xzsowG7-dH0vzm4 - { - name: "Avoid extreme colors", - program: toString({ - // @ts-ignore - $schema, - all: { - in: "colors", - varb: "a", - predicate: { - all: { - in: ["#000000", "#ffffff"], - varb: "b", - predicate: { - not: { similar: { left: "a", right: "b", threshold: 12 } }, - }, - }, - }, - }, - }), - - taskTypes: ["sequential", "diverging", "categorical"] as const, - level: "warning", - group: "design", - description: `Colors at either end of the lightness spectrum can be hard to discriminate in some contexts, and are sometimes advised against.`, - failMessage: `Colors at either end of the lightness spectrum {{blame}} are hard to discriminate in some contexts, and are sometimes advised against`, - id: "extreme-colors-built-in", - blameMode: "single", - }, - { - name: "Avoid too many colors", - program: toString({ - // @ts-ignore - $schema, - "<": { left: { count: "colors" }, right: 10 }, - }), - taskTypes: ["sequential", "diverging", "categorical"] as const, - level: "warning", - group: "design", - description: - "Palettes should have a maximum number of colors. Higher numbers of colors can make it hard to identify specific values.", - failMessage: `This palette has too many colors and may be hard to discriminate in some contexts. Maximum: 10.`, - id: "too-many-colors-built-in", - blameMode: "single", - }, - { - name: "Palette does not have ugly colors", - program: toString({ - // @ts-ignore - $schema, - all: { - in: "colors", - varb: "a", - predicate: { - all: { - in: ["#56FF00", "#0010FF", "#6A7E25", "#FF00EF", "#806E28"], - varb: "b", - predicate: { - ">": { - left: { deltaE: { left: "a", right: "b" }, algorithm: "2000" }, - - right: 10, - }, - }, - }, - }, - }, - }), - taskTypes: ["categorical"], - description: `Colors that are close to what are known as ugly colors are sometimes advised against. See https://www.colourlovers.com/palette/1416250/The_Ugliest_Colors for more details.`, - failMessage: `This palette has some colors (specifically {{blame}}) that are close to what are known as ugly colors`, - level: "warning", - group: "design", - id: "ugly-colors-built-in", - blameMode: "single", - }, - ...blindTypes.map((type) => ({ - // old algorithm - https://github.dev/gka/palettes - // let distanceNorm = colorA.symmetricDeltaE(colorB); - // if (distanceNorm < smallestPerceivableDistance) continue; - // let distanceSim = colorA.symmetricDeltaE(colorB); - // let isNotOk = - // distanceNorm / distanceSim > ratioThreshold && - // distanceSim < smallestPerceivableDistance; - program: toString({ - // @ts-ignore - $schema, - all: { - in: "colors", - varbs: ["a", "b"], - where: { "!=": { left: "index(a)", right: "index(b)" } }, - predicate: { - not: { - similar: { - left: { cvdSim: "a", type }, - right: { cvdSim: "b", type }, - threshold: 9, - }, - }, - }, - }, - }), - name: `Colorblind Friendly for ${type}`, - taskTypes: ["sequential", "diverging", "categorical"] as TaskType[], - group: "accessibility", - description: `All colors in a palette should be differentiable by people with ${type} ${blindnessLabels[type]}. This is because if they are not, then they will not be differentiable from each other in some contexts.`, - level: "error" as const, - failMessage: `This palette is not colorblind friendly for ${type} color blindness ${blindnessLabels[type]}. The following pairs are undifferentiable: ({{blame}})`, - id: `colorblind-friendly-${type}-built-in`, - blameMode: "pair" as const, - })), - - { - program: toString({ - all: { - in: "colors", - varbs: ["a", "b"], - where: { "!=": { left: "index(a)", right: "index(b)" } }, - predicate: { - ">": { - left: { dist: { left: "a", right: "b" }, space: "lab" }, - right: 15, - }, - }, - }, - }), - name: "Mutually Distinct", - taskTypes: ["categorical"] as const, - group: "usability", - level: "error", - description: `All colors in a palette should be different from each other. This is because if they are not, then they will not be differentiable from each other in some contexts.`, - failMessage: `Some colors in this palette ({{blame}}) are not differentiable from each other.`, - id: "mutually-distinct-built-in", - blameMode: "pair" as const, - }, - { - program: toString({ - all: { - in: "colors", - varb: "a", - predicate: { - ">": { - left: { - contrast: { left: "a", right: "background" }, - algorithm: "APCA", - }, - right: 4.5, - }, - }, - }, - }), - name: "Background Contrast", - taskTypes: ["sequential", "diverging", "categorical"] as const, - level: "error", - group: "accessibility", - description: `All colors in a palette should have a sufficient contrast ratio with the background color. This is because if they are not, then they will not be differentiable from each other in some contexts. Valid algorithms are APCA, WCAG21, Michelson, Weber, Lstar, DeltaPhi.`, - failMessage: `These colors ({{blame}}) do not have a sufficient contrast ratio with the background and may be hard to discriminate in some contexts.`, - id: "background-contrast-built-in", - blameMode: "single" as const, - }, + ...ColorBlindness, + ...Fair, + AvoidExtremes, + BackgroundDifferentiability, + CatOrderSimilarity, + MaxColors, + MutuallyDistinct, + SequentialOrder, + UglyColors, ]; export default BUILT_INS; diff --git a/src/lib/lints/cat-order-similarity.ts b/src/lib/lints/cat-order-similarity.ts new file mode 100644 index 00000000..382f7338 --- /dev/null +++ b/src/lib/lints/cat-order-similarity.ts @@ -0,0 +1,35 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; + +const lint: CustomLint = { + name: "Colors distinguishable in order", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varbs: ["a", "b"], + where: { + "==": { + left: "index(a)", + right: { "-": { left: "index(b)", right: 1 } }, + }, + }, + predicate: { + ">": { + left: { deltaE: { left: "a", right: "b" }, algorithm: "2000" }, + right: 10, + }, + }, + }, + }), + taskTypes: ["categorical"] as const, + level: "warning", + group: "design", + description: + "Opt for colors that are perceptually distinguishable in a logical sequence when designing visual elements like charts or graphs. This ensures that viewers can easily recognize the order or progression of data points. For categorical this means that when only a small number of colors are used, they should be as different as possible. For sequential and diverging, this means that the colors should be as different as possible in order.", + failMessage: `Some sequences of colors are too similar based on dE scores: {{blame}}. Try reordering them or making them more distinguishable`, + id: "cat-order-similarity-built-in", + blameMode: "pair", +}; +export default lint; diff --git a/src/lib/lints/color-blindness.ts b/src/lib/lints/color-blindness.ts new file mode 100644 index 00000000..0cebeae3 --- /dev/null +++ b/src/lib/lints/color-blindness.ts @@ -0,0 +1,49 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; + +// old algorithm - https://github.dev/gka/palettes +// let distanceNorm = colorA.symmetricDeltaE(colorB); +// if (distanceNorm < smallestPerceivableDistance) continue; +// let distanceSim = colorA.symmetricDeltaE(colorB); +// let isNotOk = +// distanceNorm / distanceSim > ratioThreshold && +// distanceSim < smallestPerceivableDistance; + +const blindnessLabels = { + deuteranopia: "(ie can't see green)", + protanopia: "(ie can't see red)", + tritanopia: "(ie can't see blue)", + grayscale: "", +}; +const blindTypes = Object.keys( + blindnessLabels +) as (keyof typeof blindnessLabels)[]; +const lints: CustomLint[] = blindTypes.map((type) => ({ + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varbs: ["a", "b"], + where: { "!=": { left: "index(a)", right: "index(b)" } }, + predicate: { + not: { + similar: { + left: { cvdSim: "a", type }, + right: { cvdSim: "b", type }, + threshold: 9, + }, + }, + }, + }, + }), + name: `Colorblind Friendly for ${type}`, + taskTypes: ["sequential", "diverging", "categorical"], + group: "accessibility", + description: `All colors in a palette should be differentiable by people with ${type} ${blindnessLabels[type]}. This is because if they are not, then they will not be differentiable from each other in some contexts.`, + level: "error" as const, + failMessage: `This palette is not colorblind friendly for ${type} color blindness ${blindnessLabels[type]}. The following pairs are undifferentiable: ({{blame}})`, + id: `colorblind-friendly-${type}-built-in`, + blameMode: "pair" as const, +})); +export default lints; diff --git a/src/lib/lints/color-similarity.ts b/src/lib/lints/color-similarity.ts deleted file mode 100644 index 127ecb02..00000000 --- a/src/lib/lints/color-similarity.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ColorLint } from "./ColorLint"; -import type { TaskType } from "./ColorLint"; - -export default class ColorSimilarity extends ColorLint { - name = "Colors are differentiable in order"; - taskTypes = ["sequential", "diverging", "categorical"] as TaskType[]; - group: string = "usability"; - description: string = `Opt for colors that are perceptually distinguishable in a logical sequence when designing visual elements like charts or graphs. This ensures that viewers can easily recognize the order or progression of data points. For categorical this means that when only a small number of colors are used, they should be as different as possible. For sequential and diverging, this means that the colors should be as different as possible in order.`; - - _runCheck() { - const { colors } = this.palette; - const des = []; - for (let i = 0; i < colors.length - 1; i++) { - des.push(colors[i].deltaE(colors[i + 1])); - } - const failingIndexes = - des.reduce((acc, x, idx) => { - if (x < 10) { - return [...acc, idx]; - } - return acc; - }, [] as number[]) || []; - const passCheck = failingIndexes?.length === 0; - return { passCheck, data: failingIndexes }; - } - buildMessage(): string { - const { colors } = this.palette; - const pairs = this.checkData - .map((x) => { - const a = colors[x].toHex(); - const b = colors[x + 1].toHex(); - return `(${a} ${b})`; - }) - .join(", "); - return `Some colors are too similar based on dE scores: ${pairs}. Try reordering them or making them more distinguishable.`; - } -} diff --git a/src/lib/lints/contrast.ts b/src/lib/lints/contrast.ts deleted file mode 100644 index 8cdaba4f..00000000 --- a/src/lib/lints/contrast.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Color } from "../Color"; -import { ColorLint } from "./ColorLint"; -import type { TaskType } from "./ColorLint"; - -type Algorithm = - | "APCA" - | "WCAG21" - | "Michelson" - | "Weber" - | "Lstar" - | "DeltaPhi"; - -export default class BackgroundContrast extends ColorLint { - name = "Background Contrast"; - taskTypes = ["sequential", "diverging", "categorical"] as TaskType[]; - defaultParam = "APCA" as Algorithm; - paramOptions: { type: "enum"; options: string[] } = { - type: "enum", - options: ["APCA", "WCAG21", "Michelson", "Weber", "Lstar", "DeltaPhi"], - }; - level: "error" | "warning" = "error"; - group: string = "accessibility"; - description: string = `All colors in a palette should have a sufficient contrast ratio with the background color. This is because if they are not, then they will not be differentiable from each other in some contexts.`; - - _runCheck() { - const { background } = this.palette; - const bg = background.toColorIO(); - const failingColors = this.palette.colors.filter((x) => { - const contrast = Math.abs(x.toColorIO().contrast(bg, this.config.val!)); - return contrast < 4.5; - }); - return { - passCheck: failingColors.length === 0, - data: failingColors, - }; - } - buildMessage(): string { - const colors = this.checkData?.map((x) => x.toHex()); - return `These colors (${colors}) do not have a sufficient contrast ratio with the background and may be hard to discriminate in some contexts. Contrast is calculated using the ${this.config.val} algorithm.`; - } -} diff --git a/src/lib/lints/diverging-order.ts b/src/lib/lints/diverging-order.ts index c6287d73..2b671225 100644 --- a/src/lib/lints/diverging-order.ts +++ b/src/lib/lints/diverging-order.ts @@ -2,6 +2,7 @@ import { ColorLint } from "./ColorLint"; import type { TaskType } from "./ColorLint"; import { Color } from "../Color"; import type { Palette } from "../../stores/color-store"; +import type { LintFixer } from "../linter-tools/lint-fixer"; const meanPoint2d = (points: Color[]) => { const labPoints = points.map((x) => x.toColorIO().to("lab").coords); @@ -43,46 +44,47 @@ export default class SequentialOrder extends ColorLint { buildMessage(): string { return `This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively.`; } - hasHeuristicFix = true; - static async suggestFix(palette: Palette) { - // figure out if its centered on a light color or a dark color? - // a dumb hueristic is just look at what the center color is in lab space, and see if its darker or lighter than most colors + subscribedFix: string = "fixDivergingOrder"; +} + +export const fixDivergingOrder: LintFixer = async (palette) => { + // figure out if its centered on a light color or a dark color? + // a dumb hueristic is just look at what the center color is in lab space, and see if its darker or lighter than most colors - let colors = [...palette.colors]; - // const medianPoint = findMinDistPoint(colors, meanPoint2d(colors)); - // console.log(medianPoint.toHex()); - // let darkerThanMedian = colors.filter( - // (x) => x.luminance() < medianPoint.luminance() - // ).length; + let colors = [...palette.colors]; + // const medianPoint = findMinDistPoint(colors, meanPoint2d(colors)); + // console.log(medianPoint.toHex()); + // let darkerThanMedian = colors.filter( + // (x) => x.luminance() < medianPoint.luminance() + // ).length; - const sortByLum = (a: Color, b: Color) => { - const aL = a.luminance(); - const bL = b.luminance(); - if (aL === bL) return 0; - return aL > bL ? 1 : -1; - }; - // if (darkerThanMedian < colors.length / 2) { - // console.log("reversing"); - // colors = colors.reverse(); - // } + const sortByLum = (a: Color, b: Color) => { + const aL = a.luminance(); + const bL = b.luminance(); + if (aL === bL) return 0; + return aL > bL ? 1 : -1; + }; + // if (darkerThanMedian < colors.length / 2) { + // console.log("reversing"); + // colors = colors.reverse(); + // } - // const lightPoint = colors.at(-1)!; - const leftColors = [colors.at(-1)!]; - const rightColors = [colors.at(-2)!]; - for (let i = 0; i < colors.length - 2; i++) { - const color = colors[i]; - const leftColor = leftColors.at(-1)!; - const rightColor = rightColors.at(-1)!; - if (color.deltaE(leftColor) < color.deltaE(rightColor)) { - leftColors.push(color); - } else { - rightColors.push(color); - } + // const lightPoint = colors.at(-1)!; + const leftColors = [colors.at(-1)!]; + const rightColors = [colors.at(-2)!]; + for (let i = 0; i < colors.length - 2; i++) { + const color = colors[i]; + const leftColor = leftColors.at(-1)!; + const rightColor = rightColors.at(-1)!; + if (color.deltaE(leftColor) < color.deltaE(rightColor)) { + leftColors.push(color); + } else { + rightColors.push(color); } - colors = [ - ...leftColors.sort(sortByLum), - ...rightColors.sort(sortByLum).reverse(), - ]; - return [{ ...palette, colors }]; } -} + colors = [ + ...leftColors.sort(sortByLum), + ...rightColors.sort(sortByLum).reverse(), + ]; + return [{ ...palette, colors }]; +}; diff --git a/src/lib/lints/fair.ts b/src/lib/lints/fair.ts index 8519caa6..0b9df72d 100644 --- a/src/lib/lints/fair.ts +++ b/src/lib/lints/fair.ts @@ -1,83 +1,128 @@ -import { Color } from "../Color"; -import { ColorLint } from "./ColorLint"; -import type { TaskType } from "./ColorLint"; -import { extent } from "../utils"; +// import { Color } from "../Color"; +// import { ColorLint } from "./ColorLint"; +// import type { TaskType } from "./ColorLint"; +// import { extent } from "../utils"; +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; // magic numbers supplied by the paper const cRangeUnfair = 80; const lRangeUnfair = 50; -const identifyUnfair = ( - colors: Color[], - threshold: number, - channel: number -) => { - const channelRange = extent(colors.map((x) => x.toChannels()[channel])); - const outOfBoundColors = colors.filter( - (x) => x.toChannels()[channel] > channelRange[0] + threshold - ); - return outOfBoundColors; -}; +// const identifyUnfair = ( +// colors: Color[], +// threshold: number, +// channel: number +// ) => { +// const channelRange = extent(colors.map((x) => x.toChannels()[channel])); +// const outOfBoundColors = colors.filter( +// (x) => x.toChannels()[channel] > channelRange[0] + threshold +// ); +// return outOfBoundColors; +// }; -const fairMessage = (outOfBandL: Color[], outOfBandC: Color[]) => { - const lPass = outOfBandL.length === 0; - const cPass = outOfBandC.length === 0; - if (lPass && cPass) { - return ""; - } - const baseMessage = `This palette is unfair (meaning that some values may unduely stand out).`; - const lMsg = lPass - ? "" - : `The following colors have a luminance value that is out of range: ${outOfBandL - .map((x) => x.toHex()) - .join(", ")}.`; - const cMsg = cPass - ? "" - : `The following colors have a chroma value that is out of range: ${outOfBandC - .map((x) => x.toHex()) - .join(", ")}.`; - return `${baseMessage} ${lMsg} ${cMsg}`; -}; +// const fairMessage = (outOfBandL: Color[], outOfBandC: Color[]) => { +// const lPass = outOfBandL.length === 0; +// const cPass = outOfBandC.length === 0; +// if (lPass && cPass) { +// return ""; +// } +// const baseMessage = `This palette is unfair (meaning that some values may unduely stand out).`; +// const lMsg = lPass +// ? "" +// : `The following colors have a luminance value that is out of range: ${outOfBandL +// .map((x) => x.toHex()) +// .join(", ")}.`; +// const cMsg = cPass +// ? "" +// : `The following colors have a chroma value that is out of range: ${outOfBandC +// .map((x) => x.toHex()) +// .join(", ")}.`; +// return `${baseMessage} ${lMsg} ${cMsg}`; +// }; -class FairBase extends ColorLint { - name = "Fair"; - group = "design"; - // hasParam = true; - level: "error" | "warning" = "warning"; +// class FairBase extends ColorLint { +// name = "Fair"; +// group = "design"; +// // hasParam = true; +// level: "error" | "warning" = "warning"; - description: string = `Do the colors stand out equally? A color palette is described as fair if both chroma and luminance ranges are below a certain threshold and unfair if one of them is above a certain threshold. For sequential and diverging palettes, only the chroma range is considered.`; - buildMessage(): string { - const { outOfBandL, outOfBandC } = this.checkData; - return fairMessage(outOfBandL, outOfBandC); - } -} +// description: string = `Do the colors stand out equally? A color palette is described as fair if both chroma and luminance ranges are below a certain threshold and unfair if one of them is above a certain threshold. For sequential and diverging palettes, only the chroma range is considered.`; +// buildMessage(): string { +// const { outOfBandL, outOfBandC } = this.checkData; +// return fairMessage(outOfBandL, outOfBandC); +// } +// } -class FairSequential extends FairBase { - taskTypes = ["sequential", "diverging"] as TaskType[]; +// class FairSequential extends FairBase { +// taskTypes = ["sequential", "diverging"] as TaskType[]; - _runCheck() { - const { colors } = this.palette; - const outOfBandL = identifyUnfair(colors, lRangeUnfair, 0); - return { - passCheck: outOfBandL.length === 0, - data: { outOfBandL, outOfBandC: [] }, - }; - } -} +// _runCheck() { +// const { colors } = this.palette; +// const outOfBandL = identifyUnfair(colors, lRangeUnfair, 0); +// return { +// passCheck: outOfBandL.length === 0, +// data: { outOfBandL, outOfBandC: [] }, +// }; +// } +// } -class FairNominal extends FairBase { - taskTypes = ["categorical"] as TaskType[]; +// class FairNominal extends FairBase { +// taskTypes = ["categorical"] as TaskType[]; - _runCheck() { - const { colors } = this.palette; - const outOfBandL = identifyUnfair(colors, lRangeUnfair, 0); - const outOfBandC = identifyUnfair(colors, cRangeUnfair, 1); - return { - passCheck: !outOfBandL.length && !outOfBandC.length, - data: { outOfBandL, outOfBandC }, - }; - } -} +// _runCheck() { +// const { colors } = this.palette; +// const outOfBandL = identifyUnfair(colors, lRangeUnfair, 0); +// const outOfBandC = identifyUnfair(colors, cRangeUnfair, 1); +// return { +// passCheck: !outOfBandL.length && !outOfBandC.length, +// data: { outOfBandL, outOfBandC }, +// }; +// } +// } -const Fair = [FairSequential, FairNominal]; -export default Fair; +const lRangePredicate = { + "<": { + left: { extent: { sort: "colors", varb: "x", func: { "lch.l": "x" } } }, + right: lRangeUnfair, + }, +}; +const cRangePredicate = { + "<": { + left: { extent: { sort: "colors", varb: "x", func: { "lch.c": "x" } } }, + right: cRangeUnfair, + }, +}; +const failMsgBase = `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.`; +// json program version +const FairNominal: CustomLint = { + name: "Fair", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + and: [lRangePredicate, cRangePredicate], + }), + taskTypes: ["categorical"] as const, + level: "warning", + group: "design", + description: + "Do the colors stand out equally? A color palette is described as fair if both chroma and luminance ranges are below a certain threshold and unfair if one of them is above a certain threshold.", + failMessage: `${failMsgBase} Maximum chroma range: ${cRangeUnfair}, maximum luminance range: ${lRangeUnfair}.`, + id: "fair-nominal-built-in", + blameMode: "single", + subscribedFix: "fixMaxColors", +}; +const FairSequential: CustomLint = { + ...FairNominal, + taskTypes: ["sequential", "diverging"] as const, + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + and: [lRangePredicate], + }), + failMessage: `${failMsgBase} Maximum chroma range: ${lRangeUnfair}.`, + id: "fair-sequential-built-in", + description: + "Do the colors stand out equally? A color palette is described as fair if the luminance ranges are below a certain threshold and unfair if one of them is above a certain threshold. ", +}; +export default [FairNominal, FairSequential]; diff --git a/src/lib/lints/max-colors.ts b/src/lib/lints/max-colors.ts new file mode 100644 index 00000000..d8018b4c --- /dev/null +++ b/src/lib/lints/max-colors.ts @@ -0,0 +1,27 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; +import type { LintFixer } from "../linter-tools/lint-fixer"; + +const lint: CustomLint = { + name: "Max Colors", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + "<": { left: { count: "colors" }, right: 10 }, + }), + taskTypes: ["sequential", "diverging", "categorical"] as const, + level: "warning", + group: "design", + description: + "Palettes should have a maximum number of colors. Higher numbers of colors can make it hard to identify specific values.", + failMessage: `This palette has too many colors and may be hard to discriminate in some contexts. Maximum: 10.`, + id: "too-many-colors-built-in", + blameMode: "single", + subscribedFix: "fixMaxColors", +}; +export default lint; + +export const fixMaxColors: LintFixer = async (palette) => { + const colors = palette.colors; + return [{ ...palette, colors: colors.slice(0, 10) }]; +}; diff --git a/src/lib/lints/mutually-distinct.ts b/src/lib/lints/mutually-distinct.ts new file mode 100644 index 00000000..8444da39 --- /dev/null +++ b/src/lib/lints/mutually-distinct.ts @@ -0,0 +1,29 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; +const lint: CustomLint = { + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varbs: ["a", "b"], + where: { "!=": { left: "index(a)", right: "index(b)" } }, + predicate: { + ">": { + left: { dist: { left: "a", right: "b" }, space: "lab" }, + right: 15, + }, + }, + }, + }), + name: "Mutually Distinct", + taskTypes: ["categorical"] as const, + group: "usability", + level: "error", + description: `All colors in a palette should be different from each other. This is because if they are not, then they will not be differentiable from each other in some contexts.`, + failMessage: `Some colors in this palette ({{blame}}) are not differentiable from each other.`, + id: "mutually-distinct-built-in", + blameMode: "pair" as const, +}; + +export default lint; diff --git a/src/lib/lints/name-discrim.ts b/src/lib/lints/name-discrim.ts index 33f36d70..9248532d 100644 --- a/src/lib/lints/name-discrim.ts +++ b/src/lib/lints/name-discrim.ts @@ -1,9 +1,10 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; import namer from "color-namer"; -import { ColorLint } from "./ColorLint"; -import type { TaskType } from "./ColorLint"; import { Color } from "../Color"; import type { Palette } from "../../stores/color-store"; import { titleCase } from "../utils"; +import type { LintFixer } from "../linter-tools/lint-fixer"; function findSmallest(arr: A[], accessor: (x: A) => number): A { let smallest = arr[0]; @@ -18,30 +19,6 @@ export function colorNameSimple(colors: Color[]) { return colors.map((x) => ({ word: getName(x), hex: x.toHex() })); } -function simpleDiscrim(colors: Color[]) { - const names = colorNameSimple(colors); - const counts = names.reduce((acc, x) => { - if (!acc[x.word]) { - acc[x.word] = []; - } - acc[x.word].push(x); - return acc; - }, {} as Record); - - const remaining = Object.entries(counts) - .filter((x) => x[1].length > 1) - .map(([x, y]) => [x, y.map((el) => el.hex).join(", ")]); - // .map((x) => x[0]); - if (remaining.length === 0) { - return false; - } - - const countsStr = remaining - .map(([word, vals]) => `${word} (${vals})`) - .join(", "); - return `Color Name discriminability check failed. The following color names are repeated: ${countsStr}`; -} - const nameCache = new Map(); export const getName = (color: Color) => { const hex = color.toHex().toUpperCase(); @@ -67,7 +44,33 @@ function suggestFixForColorsWithCommonNames(colors: Color[]): Color[] { }); } -function buildFix(colors: Color[]): Color[] { +const lint: CustomLint = { + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varbs: ["a", "b"], + where: { "!=": { left: "index(a)", right: "index(b)" } }, + predicate: { "!=": { left: { name: "a" }, right: { name: "b" } } }, + }, + }), + name: "Color Name Discriminability", + taskTypes: ["sequential"] as const, + level: "error", + group: "usability", + description: `Being able to identify colors by name is important for usability and for memorability.`, + failMessage: `The following pairs of colors have the same name: {{blame}}`, + id: "color-name-discriminability-built-in", + blameMode: "pair" as const, + subscribedFix: "fixColorNameDiscriminability", +}; +export default lint; + +export const fixColorNameDiscriminability: LintFixer = async ( + palette: Palette +) => { + const colors = palette.colors; const colorNamesByIndex = colors.reduce((acc, color, index) => { const name = getName(color); acc[name] = (acc[name] || []).concat(index); @@ -83,28 +86,5 @@ function buildFix(colors: Color[]): Color[] { newColors[i] = updatedColors[j]; }); }); - return newColors; -} - -export default class ColorNameDiscriminability extends ColorLint< - string, - false -> { - name = "Color Name Discriminability"; - taskTypes = ["sequential", "diverging", "categorical"] as TaskType[]; - group = "usability"; - description: string = - "Being able to identify colors by name is important for usability and for memorability. "; - _runCheck() { - const { colors } = this.palette; - const passCheck = simpleDiscrim(colors); - return { passCheck: !passCheck, data: passCheck || "" }; - } - buildMessage(): string { - return this.checkData; - } - hasHeuristicFix = true; - static async suggestFix(palette: Palette) { - return [{ ...palette, colors: buildFix(palette.colors) }]; - } -} + return [{ ...palette, colors: newColors }]; +}; diff --git a/src/lib/lints/sequential-order.ts b/src/lib/lints/sequential-order.ts index 96ac5a5d..7bb01786 100644 --- a/src/lib/lints/sequential-order.ts +++ b/src/lib/lints/sequential-order.ts @@ -1,36 +1,46 @@ -import { ColorLint } from "./ColorLint"; -import type { TaskType } from "./ColorLint"; +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; import { Color } from "../Color"; import type { Palette } from "../../stores/color-store"; +import type { LintFixer } from "../linter-tools/lint-fixer"; + +const lint: CustomLint = { + name: "Sequential Pal Order", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + or: [ + { + "==": { + left: { sort: "colors", varb: "x", func: { "lch.l": "x" } }, + right: { map: "colors", varb: "x", func: { "lch.l": "x" } }, + }, + }, + { + "==": { + left: { sort: "colors", varb: "x", func: { "lch.l": "x" } }, + right: { + reverse: { map: "colors", varb: "x", func: { "lch.l": "x" } }, + }, + }, + }, + ], + }), + taskTypes: ["sequential", "diverging", "categorical"] as const, + level: "error", + group: "usability", + description: + "Sequential palettes should be ordered by lightness. This is a defining property of a sequential palette and ensures that values are understood as having an increase (or decreasing) value.", + failMessage: `This pal should be ordered by lightness if being used as a sequential palette. {{blame}} may be to blame.`, + id: "sequential-order-built-in", + blameMode: "single", + subscribedFix: "fixSequentialOrder", +}; +export default lint; const getLightness = (color: Color) => color.toColorIO().to("lab").coords[0]; -export default class SequentialOrder extends ColorLint { - name = "Sequential Palettes should be ordered by lightness"; - taskTypes = ["sequential"] as TaskType[]; - group = "usability"; - description = `Sequential palettes should be ordered by lightness. This is a defining property of a sequential palette and ensures that values are understood as having an increase (or decreasing) value.`; - _runCheck() { - const { colors } = this.palette; - if (colors.length < 2) { - return { passCheck: true, data: false }; - } - let direction = getLightness(colors[0]) < getLightness(colors[1]) ? 1 : -1; - for (let i = 1; i < colors.length; i++) { - const check = - direction * (getLightness(colors[i - 1]) - getLightness(colors[i])); - if (check > 0) { - return { passCheck: false, data: false }; - } - } - return { passCheck: true, data: false }; - } - buildMessage(): string { - return `This pal should be ordered by lightness if being used as a sequential palette`; - } - hasHeuristicFix = true; - static async suggestFix(palette: Palette) { - const colors = [...palette.colors]; - colors.sort((a, b) => getLightness(a) - getLightness(b)); - return [{ ...palette, colors }]; - } -} +export const fixSequentialOrder: LintFixer = async (palette: Palette) => { + const colors = [...palette.colors]; + colors.sort((a, b) => getLightness(a) - getLightness(b)); + return [{ ...palette, colors }]; +}; diff --git a/src/lib/lints/ugly-colors.ts b/src/lib/lints/ugly-colors.ts new file mode 100644 index 00000000..69a04e0e --- /dev/null +++ b/src/lib/lints/ugly-colors.ts @@ -0,0 +1,35 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "./CustomLint"; + +const lint: CustomLint = { + name: "Palette does not have ugly colors", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varb: "a", + predicate: { + all: { + in: ["#56FF00", "#0010FF", "#6A7E25", "#FF00EF", "#806E28"], + varb: "b", + predicate: { + ">": { + left: { deltaE: { left: "a", right: "b" }, algorithm: "2000" }, + + right: 10, + }, + }, + }, + }, + }, + }), + taskTypes: ["categorical"], + description: `Colors that are close to what are known as ugly colors are sometimes advised against. See https://www.colourlovers.com/palette/1416250/The_Ugliest_Colors for more details.`, + failMessage: `This palette has some colors (specifically {{blame}}) that are close to what are known as ugly colors`, + level: "warning", + group: "design", + id: "ugly-colors-built-in", + blameMode: "single", +}; +export default lint; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index db925fe3..3754a043 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,6 @@ import { Color, colorPickerConfig } from "./Color"; import type { PalType, Palette } from "../stores/color-store"; +import type { LintProgram } from "./lint-language/lint-type"; import { Formatter, FracturedJsonOptions, EolStyle } from "fracturedjsonjs"; const options = new FracturedJsonOptions(); @@ -13,6 +14,20 @@ formatter.Options = options; export function JSONStringify(obj: string) { return formatter.Reformat(obj); } +export function JSONToPrettyString(program: LintProgram) { + return JSONStringify(JSON.stringify(program)); +} + +export 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", + }; +} export const pick = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]; diff --git a/src/linting/EvalResponse.svelte b/src/linting/EvalResponse.svelte index c37e2ee9..0a406c93 100644 --- a/src/linting/EvalResponse.svelte +++ b/src/linting/EvalResponse.svelte @@ -88,7 +88,7 @@ - {#if check.hasHeuristicFix} + {#if check.subscribedFix !== "none"} diff --git a/src/linting/LintCustomizationModal.svelte b/src/linting/LintCustomizationModal.svelte index c650c7ab..a959c627 100644 --- a/src/linting/LintCustomizationModal.svelte +++ b/src/linting/LintCustomizationModal.svelte @@ -10,6 +10,7 @@ import { CreateCustomLint } from "../lib/lints/CustomLint"; import { buttonStyle } from "../lib/styles"; import { JSONStringify } from "../lib/utils"; + import type { CustomLint } from "../lib/lints/CustomLint"; $: lint = $lintStore.lints.find( (lint) => lint.id === $lintStore.focusedLint @@ -23,17 +24,18 @@ $: currentPal = $colorStore.palettes[$colorStore.currentPal]; // run this lint let errors: any = null; - function runLint(lint: any) { + function runLint(lint: CustomLint, options: any) { try { const customLint = CreateCustomLint(lint); - const result = new customLint(currentPal).run(); + const result = new customLint(currentPal).run(options); errors = null; return result; } catch (e) { errors = e; } } - $: lintRun = runLint(lint); + let debugCompare = false; + $: lintRun = runLint(lint, { debugCompare }); let showDoubleCheck = false; $: currentTaskTypes = lint.taskTypes as string[]; $: checkData = lintRun?.checkData || []; @@ -250,6 +252,16 @@ }} language="json" /> +
+ Show Compare Debug In Terminal +
{/if} diff --git a/vitest.config.ts b/vitest.config.ts index 7f72b8fa..d4324356 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,5 +4,6 @@ export default defineConfig({ test: { setupFiles: ["@vitest/web-worker"], environment: "jsdom", + includeSource: ["src/**/*.{js,ts}"], }, });