From 6afe205696123e433f8b71611b05c042da7be428 Mon Sep 17 00:00:00 2001 From: Andrew Michael McNutt Date: Fri, 15 Mar 2024 16:40:37 -0700 Subject: [PATCH] trying to write a strong program for diverging --- public/lang-docs.md | 162 ++++++++++++++++ src/lib/ColorLint.test.ts | 5 + src/lib/__snapshots__/ColorLint.test.ts.snap | 10 + src/lib/__snapshots__/LintDocs.test.ts.snap | 162 ++++++++++++++++ src/lib/ai-docs.md | 6 + src/lib/lint-language/LintLanguage.test.ts | 115 +++++++----- src/lib/lint-language/lint-language.ts | 15 +- src/lib/lint-language/lint-type.ts | 7 +- src/lib/linter.ts | 6 +- src/lib/lints/diverging-order.ts | 187 ++++++++++--------- src/linting/LintCustomizationPreview.svelte | 12 +- src/linting/LintCustomizationTab.svelte | 3 + src/linting/NewLintSuggestion.svelte | 1 + 13 files changed, 548 insertions(+), 143 deletions(-) diff --git a/public/lang-docs.md b/public/lang-docs.md index 1e85ffe6..1308a156 100644 --- a/public/lang-docs.md +++ b/public/lang-docs.md @@ -44,10 +44,15 @@ Math Operations: {"\*": {left: Number | Variable, right: Number | Variable}} {"+": {left: Number | Variable, right: Number | Variable}} {"/": {left: Number | Variable, right: Number | Variable}} +{"//": {left: Number | Variable, right: Number | Variable}} {"-": {left: Number | Variable, right: Number | Variable}} {absDiff: {left: Number | Variable, right: Number | Variable}} {"%": {left: Number | Variable, right: Number | Variable}} +Notes + +- // is division with round + Value Comparisons: {dist: {left: Color | Variable, right: Color | Variable}, space: COLOR_SPACE } {deltaE: {left: Color | Variable, right: Color | Variable}, algorithm: '2000' | etc } @@ -61,6 +66,7 @@ Aggregates {mean: Variable | Number[]} {std: Variable | Number[]} {first: Variable | Number[]} +{middle: Variable | Number[]} {last: Variable | Number[]} {extent: Variable | Number[]} @@ -1302,6 +1308,162 @@ Program: +### Diverging Palettes order + +Description: Diverging palettes should have a middle color that is the lightest or darkest color. This is because if they are not, then they will not be differentiable from each other in some contexts. + +Natural Language: (sort(filter(colors, x => index(x) < count(colors) // 2), y => lab.l(y)) == map(filter(colors, x => index(x) < count(colors) // 2), y => lab.l(y)) AND sort(filter(colors, x => index(x) > count(colors) // 2), y => lab.l(y)) == reverse(map(filter(colors, x => index(x) > count(colors) // 2), y => lab.l(y))) AND (ALL z IN colors WHERE index(z) != count(colors) // 2 SUCH THAT lab.l(middle(colors)) > lab.l(z) OR ALL z IN colors WHERE index(z) != count(colors) // 2 SUCH THAT lab.l(middle(colors)) < lab.l(z))) + +Palettes that will fail this test: + +- #000, #fff, #ff7e0e, #0f0, #0084a9, #00f with a #fff background + +- #be4704, #008000, #801242 with a #fff background + + + +Palettes that will pass this test: + +- #ff7e0e with a #fff background + +- #67001f, #b2182b, #d6604d, #f4a582, #fddbc7, #fff, #e0e0e0, #bababa, #878787, #4d4d4d, #1a1a1a with a #fff background + +- #67001f, #fff, #1a1a1a with a #fff background + + +Program: + +```json +{ + "$schema": "http://localhost:3000/lint-schema.json", + "and": [ + { + "==": { + "left": { + "sort": { + "filter": "colors", + "varb": "x", + "func": { + "<": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + }, + "right": { + "map": { + "filter": "colors", + "varb": "x", + "func": { + "<": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + } + } + }, + { + "==": { + "left": { + "sort": { + "filter": "colors", + "varb": "x", + "func": { + ">": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + }, + "right": { + "reverse": { + "map": { + "filter": "colors", + "varb": "x", + "func": { + ">": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + } + } + } + }, + { + "or": [ + { + "all": { + "in": "colors", + "varb": "z", + "where": { + "!=": { + "left": "index(z)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + }, + "predicate": { + ">": { + "left" : { "lab.l": {"middle": "colors"} }, + "right": { "lab.l": "z" } + } + } + } + }, + { + "all": { + "in": "colors", + "varb": "z", + "where": { + "!=": { + "left": "index(z)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + }, + "predicate": { + "<": { + "left" : { "lab.l": {"middle": "colors"} }, + "right": { "lab.l": "z" } + } + } + } + } + ] + } + ] +} + +``` + + + + + ### Even Distribution Description: Categorical values should have an even distribution around the hue circle in LCH color space diff --git a/src/lib/ColorLint.test.ts b/src/lib/ColorLint.test.ts index f7f459b5..b80582aa 100644 --- a/src/lib/ColorLint.test.ts +++ b/src/lib/ColorLint.test.ts @@ -16,6 +16,7 @@ import CatOrderSimilarity from "./lints/cat-order-similarity"; import CVDCheck from "./lints/cvd-check"; import ColorNameDiscriminability, { getName } from "./lints/name-discrim"; import ColorTags from "./lints/color-tags"; +import DivergingOrder from "./lints/diverging-order"; import EvenDistribution from "./lints/even-distribution"; import Fair from "./lints/fair"; import Gamut from "./lints/in-gamut"; @@ -139,6 +140,10 @@ test("ColorLint - CVD: Grayscale", async () => { autoTest(CVDCheck[3]); }); +test("ColorLint - Diverging", async () => { + autoTest(DivergingOrder); +}); + const ughWhat = ["#00ffff", "#00faff", "#00e4ff", "#fdfdfc", "#00ffff"]; test("ColorLint - Background Contrast", async () => { const examplePal = makePalFromString(ughWhat); diff --git a/src/lib/__snapshots__/ColorLint.test.ts.snap b/src/lib/__snapshots__/ColorLint.test.ts.snap index 5f0afa98..8250de60 100644 --- a/src/lib/__snapshots__/ColorLint.test.ts.snap +++ b/src/lib/__snapshots__/ColorLint.test.ts.snap @@ -64,6 +64,16 @@ exports[`ColorLint - Contrast (3) contrastTextAAA 1`] = `"These colors () do not exports[`ColorLint - Contrast (3) contrastTextAAA 2`] = `"These colors (#af3b4b) do not have a sufficient contrast ratio with the background and may be hard to discriminate in some contexts."`; +exports[`ColorLint - Diverging 1`] = `"This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively."`; + +exports[`ColorLint - Diverging 2`] = `"This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively."`; + +exports[`ColorLint - Diverging 3`] = `"This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively."`; + +exports[`ColorLint - Diverging 4`] = `"This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively."`; + +exports[`ColorLint - Diverging 5`] = `"This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively."`; + exports[`ColorLint - EvenDistribution 1`] = `"This palette does not evenly distribute the colors around its range correctly. Try making the spacing between the colors more regular to resolve this issue. "`; exports[`ColorLint - EvenDistribution 2`] = `"This palette does not evenly distribute the colors around its range correctly. Try making the spacing between the colors more regular to resolve this issue. "`; diff --git a/src/lib/__snapshots__/LintDocs.test.ts.snap b/src/lib/__snapshots__/LintDocs.test.ts.snap index c6457d70..bcb74b64 100644 --- a/src/lib/__snapshots__/LintDocs.test.ts.snap +++ b/src/lib/__snapshots__/LintDocs.test.ts.snap @@ -47,10 +47,15 @@ Math Operations: {"\\*": {left: Number | Variable, right: Number | Variable}} {"+": {left: Number | Variable, right: Number | Variable}} {"/": {left: Number | Variable, right: Number | Variable}} +{"//": {left: Number | Variable, right: Number | Variable}} {"-": {left: Number | Variable, right: Number | Variable}} {absDiff: {left: Number | Variable, right: Number | Variable}} {"%": {left: Number | Variable, right: Number | Variable}} +Notes + +- // is division with round + Value Comparisons: {dist: {left: Color | Variable, right: Color | Variable}, space: COLOR_SPACE } {deltaE: {left: Color | Variable, right: Color | Variable}, algorithm: '2000' | etc } @@ -64,6 +69,7 @@ Aggregates {mean: Variable | Number[]} {std: Variable | Number[]} {first: Variable | Number[]} +{middle: Variable | Number[]} {last: Variable | Number[]} {extent: Variable | Number[]} @@ -1305,6 +1311,162 @@ Program: +### Diverging Palettes order + +Description: Diverging palettes should have a middle color that is the lightest or darkest color. This is because if they are not, then they will not be differentiable from each other in some contexts. + +Natural Language: (sort(filter(colors, x => index(x) < count(colors) // 2), y => lab.l(y)) == map(filter(colors, x => index(x) < count(colors) // 2), y => lab.l(y)) AND sort(filter(colors, x => index(x) > count(colors) // 2), y => lab.l(y)) == reverse(map(filter(colors, x => index(x) > count(colors) // 2), y => lab.l(y))) AND (ALL z IN colors WHERE index(z) != count(colors) // 2 SUCH THAT lab.l(middle(colors)) > lab.l(z) OR ALL z IN colors WHERE index(z) != count(colors) // 2 SUCH THAT lab.l(middle(colors)) < lab.l(z))) + +Palettes that will fail this test: + +- #000, #fff, #ff7e0e, #0f0, #0084a9, #00f with a #fff background + +- #be4704, #008000, #801242 with a #fff background + + + +Palettes that will pass this test: + +- #ff7e0e with a #fff background + +- #67001f, #b2182b, #d6604d, #f4a582, #fddbc7, #fff, #e0e0e0, #bababa, #878787, #4d4d4d, #1a1a1a with a #fff background + +- #67001f, #fff, #1a1a1a with a #fff background + + +Program: + +\`\`\`json +{ + "$schema": "http://localhost:3000/lint-schema.json", + "and": [ + { + "==": { + "left": { + "sort": { + "filter": "colors", + "varb": "x", + "func": { + "<": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + }, + "right": { + "map": { + "filter": "colors", + "varb": "x", + "func": { + "<": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + } + } + }, + { + "==": { + "left": { + "sort": { + "filter": "colors", + "varb": "x", + "func": { + ">": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + }, + "right": { + "reverse": { + "map": { + "filter": "colors", + "varb": "x", + "func": { + ">": { + "left": "index(x)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + } + }, + "varb": "y", + "func": {"lab.l": "y"} + } + } + } + }, + { + "or": [ + { + "all": { + "in": "colors", + "varb": "z", + "where": { + "!=": { + "left": "index(z)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + }, + "predicate": { + ">": { + "left" : { "lab.l": {"middle": "colors"} }, + "right": { "lab.l": "z" } + } + } + } + }, + { + "all": { + "in": "colors", + "varb": "z", + "where": { + "!=": { + "left": "index(z)", + "right": { + "//": { "left": {"count": "colors"}, "right": 2 } + } + } + }, + "predicate": { + "<": { + "left" : { "lab.l": {"middle": "colors"} }, + "right": { "lab.l": "z" } + } + } + } + } + ] + } + ] +} + +\`\`\` + + + + + ### Even Distribution Description: Categorical values should have an even distribution around the hue circle in LCH color space diff --git a/src/lib/ai-docs.md b/src/lib/ai-docs.md index a9a4d76c..71e52229 100644 --- a/src/lib/ai-docs.md +++ b/src/lib/ai-docs.md @@ -40,10 +40,15 @@ Math Operations: {"\*": {left: Number | Variable, right: Number | Variable}} {"+": {left: Number | Variable, right: Number | Variable}} {"/": {left: Number | Variable, right: Number | Variable}} +{"//": {left: Number | Variable, right: Number | Variable}} {"-": {left: Number | Variable, right: Number | Variable}} {absDiff: {left: Number | Variable, right: Number | Variable}} {"%": {left: Number | Variable, right: Number | Variable}} +Notes + +- // is division with round + Value Comparisons: {dist: {left: Color | Variable, right: Color | Variable}, space: COLOR_SPACE } {deltaE: {left: Color | Variable, right: Color | Variable}, algorithm: '2000' | etc } @@ -57,6 +62,7 @@ Aggregates {mean: Variable | Number[]} {std: Variable | Number[]} {first: Variable | Number[]} +{middle: Variable | Number[]} {last: Variable | Number[]} {extent: Variable | Number[]} diff --git a/src/lib/lint-language/LintLanguage.test.ts b/src/lib/lint-language/LintLanguage.test.ts index c0441f3a..ed1e64c8 100644 --- a/src/lib/lint-language/LintLanguage.test.ts +++ b/src/lib/lint-language/LintLanguage.test.ts @@ -681,60 +681,65 @@ test("LintLanguage Sequential Colors", () => { ); }); -test.skip("LintLanguage Diverging Colors - dense notation", () => { - const sequential = { +test.only("LintLanguage Diverging Colors", () => { + const middleIndex = { "//": { left: { count: "colors" }, right: 2 } }; + const leftFilter = { + filter: "colors", + varb: "x", + func: { "<": { left: "index(x)", right: middleIndex } }, + }; + const rightFilter = { + filter: "colors", + varb: "x", + func: { ">": { left: "index(x)", right: middleIndex } }, + }; + const middlePred = { + left: { "lab.l": { middle: "colors" } }, + right: { "lab.l": "z" }, + }; + const allLighter = { + all: { + in: "colors", + varb: "z", + where: { "!=": { left: "index(z)", right: middleIndex } }, + predicate: { ">": middlePred }, + }, + }; + const allDarker = { + all: { + in: "colors", + varb: "z", + where: { "!=": { left: "index(z)", right: middleIndex } }, + predicate: { "<": middlePred }, + }, + }; + const prog: LintProgram = { and: [ - { "<": { left: "index(a)", right: "index(c)" } }, + // order { "==": { - left: "index(a)", - right: { "-": { left: "index(b)", right: 1 } }, - }, - }, - ], - }; - const prog = { - or: [ - { - exist: { - in: "colors", - varb: "c", - predicate: { - all: { - in: "colors", - varbs: ["a", "b"], - where: sequential, - predicate: { - and: [ - { "<": { left: { "lab.l": "a" }, right: { "lab.l": "c" } } }, - { ">": { left: { "lab.l": "b" }, right: { "lab.l": "a" } } }, - ], - }, - }, - }, + left: { sort: leftFilter, varb: "y", func: { "lab.l": "y" } }, + right: { map: leftFilter, varb: "y", func: { "lab.l": "y" } }, }, }, { - exist: { - in: "colors", - varb: "c", - predicate: { - all: { - in: "colors", - varbs: ["a", "b"], - where: sequential, - predicate: { - and: [ - { ">": { left: { "lab.l": "a" }, right: { "lab.l": "c" } } }, - { "<": { left: { "lab.l": "b" }, right: { "lab.l": "a" } } }, - ], - }, + "==": { + left: { sort: rightFilter, varb: "y", func: { "lab.l": "y" } }, + right: { + reverse: { + map: rightFilter, + varb: "y", + func: { "lab.l": "y" }, }, }, }, }, + // darkest or lightest is in the middle + + { or: [allLighter, allDarker] }, ], }; + const divergingColors = toPal([ "#67001f", "#b2182b", @@ -748,9 +753,9 @@ test.skip("LintLanguage Diverging Colors - dense notation", () => { "#4d4d4d", "#1a1a1a", ]); - const result = LLEval(prog, divergingColors); + const result = LLEval(prog, divergingColors, { debugCompare: false }); expect(result.result).toBe(true); - const result2 = LLEval(prog, toPal(["#be4704", "#008000", "#e00050"])); + const result2 = LLEval(prog, toPal(["#be4704", "#e00050", "#008000"])); expect(result2.result).toBe(false); }); @@ -863,6 +868,28 @@ test("LintLanguage Array Compare Color", () => { expect(result.blame).toStrictEqual([]); }); +test("LintLanguage Array Filter", () => { + const program: LintProgram = { + "==": { + left: { + filter: "colors", + varb: "x", + func: { "<": { left: "index(x)", right: 3 } }, + }, + right: toColors(["#fff", "#000"]), + }, + }; + + const printed = prettyPrintLL(program); + expect(printed).toBe("filter(colors, x => index(x) < 3) == [#fff, #000]"); + + const result = LLEval(program, toPal(["#fff", "#000", "#eee", "#ddd"]), { + debugCompare: false, + }); + expect(result.result).toBe(true); + expect(result.blame).toStrictEqual([]); +}); + test("LintLanguage Fair Weighting", () => { const program: LintProgram = { "<": { diff --git a/src/lib/lint-language/lint-language.ts b/src/lib/lint-language/lint-language.ts index 01225343..53924a40 100644 --- a/src/lib/lint-language/lint-language.ts +++ b/src/lib/lint-language/lint-language.ts @@ -354,7 +354,7 @@ export class LLNumber extends LLNode { } } -const LLNumberOpTypes = ["+", "-", "*", "/", "absDiff", "%"] as const; +const LLNumberOpTypes = ["+", "-", "*", "/", "//", "absDiff", "%"] as const; export class LLNumberOp extends LLNode { constructor( private type: (typeof LLNumberOpTypes)[number], @@ -376,6 +376,8 @@ export class LLNumberOp extends LLNode { return { result: left * right, env }; case "/": return { result: left / right, env }; + case "//": + return { result: Math.round(left / right), env }; case "absDiff": return { result: Math.abs(left - right), env }; case "%": @@ -500,7 +502,9 @@ export class LLPredicate extends LLNode { // array if (Array.isArray(leftEval)) { if (leftEval.length !== rightEval.length) { - throw new Error("Array length mismatch"); + throw new Error( + `Array length mismatch. Left: ${leftEval.length}, Right: ${rightEval.length}` + ); } return { result: leftEval.every((x, i) => @@ -654,7 +658,7 @@ export class LLValueFunction extends LLNode { return { result, env }; } static tryToConstruct(node: any, options: OptionsConfig) { - const inputTypes = [LLColor, LLVariable]; + const inputTypes = [LLAggregate, LLColor, LLVariable]; // find the appropriate type and do a simple type check const op = getOp(VFTypes)(node); if (!op) return false; @@ -822,7 +826,7 @@ export class LLQuantifier extends LLNode { const newEnv = varbIndex.reduce((acc, [varb, [index, x]]) => { return acc.set(varb, x).set(`index(${varb})`, idxType(index + 1)); }, env); - + (""); if (this.where && !this.where.evaluate(newEnv).result) { return "skip"; } @@ -915,6 +919,7 @@ const reduceTypes = [ "mean", "std", "first", + "middle", "last", "extent", ] as const; @@ -940,6 +945,8 @@ export class LLAggregate extends LLNode { return { result: children[0], env }; case "last": return { result: children[children.length - 1], env }; + case "middle": + return { result: children[Math.floor(children.length / 2)], env }; case "sum": return { result: children.reduce((a, b) => a + b, 0), env }; case "std": diff --git a/src/lib/lint-language/lint-type.ts b/src/lib/lint-language/lint-type.ts index 8e3e893b..5cc2509d 100644 --- a/src/lib/lint-language/lint-type.ts +++ b/src/lib/lint-language/lint-type.ts @@ -84,6 +84,8 @@ export type LintMathOps = | { "-": { left: LintValue; right: LintValue } } | { "*": { left: LintValue; right: LintValue } } | { "/": { left: LintValue; right: LintValue } } + // division with round + | { "//": { left: LintValue; right: LintValue } } | { "%": { left: LintValue; right: LintValue } } | { absDiff: { left: LintValue; right: LintValue } }; @@ -129,6 +131,7 @@ export type LintAggregate = | { count: LintArrayValue } | { extent: LintArrayValue } | { first: LintArrayValue } + | { middle: LintArrayValue } | { last: LintArrayValue } | { max: LintArrayValue } | { mean: LintArrayValue } @@ -149,12 +152,12 @@ export type LintColorFunction = | { name: LintVariable | LintColor } | { inGamut: LintVariable | LintColor } | { - toSpace: LintVariable | LintColor; + toSpace: LintVariable | LintColor | LintAggregate; space: ColorSpace; channel: ColorChannel; } | LintColorTagCheck - | { [cmd: string]: LintVariable | string }; + | { [cmd: string]: LintVariable | LintAggregate | string }; export type LintColorTagCheck = { isTag: LintVariable | LintColor; diff --git a/src/lib/linter.ts b/src/lib/linter.ts index c0c6b930..a33b0140 100644 --- a/src/lib/linter.ts +++ b/src/lib/linter.ts @@ -34,6 +34,7 @@ export const BUILT_INS: CustomLint[] = [ ...SizeDiscrim, AvoidExtremes, CatOrderSimilarity, + DivergingOrder, EvenDistribution, Gamut, MaxColors, @@ -49,10 +50,7 @@ export function runLintChecks( ): ColorLint[] { const ignoreList = palette.evalConfig; const globallyIgnoredLints = palette.evalConfig?.globallyIgnoredLints || []; - const lints = [ - DivergingOrder, - ...customLints.map((x) => CreateCustomLint(x)), - ] as (typeof ColorLint)[]; + const lints = customLints.map((x) => CreateCustomLint(x)); return ( lints .map((x) => new x(palette)) diff --git a/src/lib/lints/diverging-order.ts b/src/lib/lints/diverging-order.ts index 6524c4b7..07e90288 100644 --- a/src/lib/lints/diverging-order.ts +++ b/src/lib/lints/diverging-order.ts @@ -1,78 +1,122 @@ -import { ColorLint } from "../ColorLint"; -import type { PalType } from "../../types"; +import { JSONToPrettyString, makePalFromString } from "../utils"; +import type { CustomLint } from "../ColorLint"; + import { Color } from "../Color"; import type { ColorWrap } from "../../types"; import type { LintFixer } from "../linter-tools/lint-fixer"; -const meanPoint2d = (points: Color[]) => { - const labPoints = points.map((x) => x.toColorIO().to("lab").coords); - const xs = labPoints.map((x) => x[1]); - const ys = labPoints.map((x) => x[2]); - const x = xs.reduce((a, b) => a + b, 0) / xs.length; - const y = ys.reduce((a, b) => a + b, 0) / ys.length; - return { x, y }; +const middleIndex = { "//": { left: { count: "colors" }, right: 2 } }; +const leftFilter = { + filter: "colors", + varb: "x", + func: { "<": { left: "index(x)", right: middleIndex } }, }; -const findMinDistPoint = (points: Color[], pos: { x: number; y: number }) => { - const labPoints = points.map((x) => x.toColorIO().to("lab").coords); - const { x, y } = pos; - const distances = labPoints.map(([x1, y1]) => Math.hypot(x1 - x, y1 - y)); - const minDist = Math.min(...distances); - return points[distances.indexOf(minDist)]; +const rightFilter = { + filter: "colors", + varb: "x", + func: { ">": { left: "index(x)", right: middleIndex } }, }; -export default class DivergingOrder extends ColorLint { - name = "Diverging Palettes order"; - taskTypes = ["diverging"] as PalType[]; - group = "usability" as const; - requiredTags = []; - description: string = `Diverging palettes should have a middle color that is the lightest or darkest color. This is because if they are not, then they will not be differentiable from each other in some contexts.`; - _runCheck() { - const { colors } = this.palette; - if (colors.length <= 2) { - return { passCheck: true, data: false }; - } +const middlePred = { + left: { "lab.l": { middle: "colors" } }, + right: { "lab.l": "z" }, +}; +const allLighter = { + all: { + in: "colors", + varb: "z", + where: { "!=": { left: "index(z)", right: middleIndex } }, + predicate: { ">": middlePred }, + }, +}; +const allDarker = { + all: { + in: "colors", + varb: "z", + where: { "!=": { left: "index(z)", right: middleIndex } }, + predicate: { "<": middlePred }, + }, +}; +const lint: CustomLint = { + name: "Diverging Palettes order", + program: JSONToPrettyString({ + $schema: `${location.href}lint-schema.json`, + and: [ + // order + { + "==": { + left: { sort: leftFilter, varb: "y", func: { "lab.l": "y" } }, + right: { map: leftFilter, varb: "y", func: { "lab.l": "y" } }, + }, + }, + { + "==": { + left: { sort: rightFilter, varb: "y", func: { "lab.l": "y" } }, + right: { + reverse: { + map: rightFilter, + varb: "y", + func: { "lab.l": "y" }, + }, + }, + }, + }, + // darkest or lightest is in the middle - const summarizedDirections = colors - .slice(0, -1) - .map((x, i) => - x.color.luminance() > colors[i + 1].color.luminance() ? 1 : -1 - ) - .reduce((acc, x) => { - if (acc.length === 0) return [x]; - if (acc[acc.length - 1] === x) return acc; - return [...acc, x]; - }, [] as number[]); + { or: [allLighter, allDarker] }, + ], + }), - return { passCheck: summarizedDirections.length === 2, data: false }; - } - 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.`; - } - subscribedFix: string = "fixDivergingOrder"; -} + taskTypes: ["diverging"] as const, + requiredTags: [], + level: "error", + group: "usability", + description: `Diverging palettes should have a middle color that is the lightest or darkest color. This is because if they are not, then they will not be differentiable from each other in some contexts.`, + failMessage: `This palette should have a middle color that is the lightest or darkest color, from which the other colors grow darker or lighter respectively.`, + id: "extreme-colors-built-in", + blameMode: "none", + subscribedFix: "fixDivergingOrder", + expectedPassingTests: [ + makePalFromString(["#ff7e0e"]), + makePalFromString([ + "#67001f", + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#fff", + "#e0e0e0", + "#bababa", + "#878787", + "#4d4d4d", + "#1a1a1a", + ]), + makePalFromString(["#67001f", "#fff", "#1a1a1a"]), + ], + expectedFailingTests: [ + makePalFromString([ + "#000000", + "#ffffff", + "#ff7e0e", + "#00ff00", + "#0084a9", + "#0000ff", + ]), + makePalFromString(["#be4704", "#008000", "#801242"]), + ], +}; +export default lint; 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; - const sortByLum = (a: ColorWrap, b: ColorWrap) => { const aL = a.color.luminance(); const bL = b.color.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++) { @@ -93,36 +137,3 @@ export const fixDivergingOrder: LintFixer = async (palette) => { ]; return [{ ...palette, colors }]; }; -// { -// "exist": { -// "in": "colors", -// "varb": "c", -// "predicate": { -// "all": { -// "in": "colors", -// "varbs": ["a", "b"], -// "where": { -// "and": [ -// { "<": {"left": "index(a)", "right": "index(c)"} }, -// { -// "==": { -// "left": "index(a)", -// "right": { "-": {"left": "index(b)", "right": 1} } -// } -// } -// ] -// }, -// "predicate": { -// "and": [ -// { -// "<": { "left": {"lab.l": "a"}, "right": {"lab.l": "c"} } -// }, -// { -// ">": { "left": {"lab.l": "b"}, "right": {"lab.l": "a"} } -// } -// ] -// } -// } -// } -// } -// } diff --git a/src/linting/LintCustomizationPreview.svelte b/src/linting/LintCustomizationPreview.svelte index 73b1473f..e317fddf 100644 --- a/src/linting/LintCustomizationPreview.svelte +++ b/src/linting/LintCustomizationPreview.svelte @@ -1,4 +1,5 @@
{/each} - +
+ updatePal({ ...pal, background: newColor })} bg={pal.background} diff --git a/src/linting/LintCustomizationTab.svelte b/src/linting/LintCustomizationTab.svelte index 526b6a6f..aa5e222b 100644 --- a/src/linting/LintCustomizationTab.svelte +++ b/src/linting/LintCustomizationTab.svelte @@ -13,6 +13,7 @@ import type { Palette } from "../types"; import type { LintResult, CustomLint } from "../lib/ColorLint"; import LintCustomizationPreview from "./LintCustomizationPreview.svelte"; + import NewLintSuggestion from "./NewLintSuggestion.svelte"; $: lint = $lintStore.lints.find( (lint) => lint.id === $lintStore.focusedLint @@ -100,6 +101,7 @@ {#if !lint}
Select a lint
+
{#each Object.keys(sortedLintsByGroup) as group}
{group}
@@ -517,6 +519,7 @@ {#each failingTestResults as failing, idx}
{ const newTests = [...lint.expectedFailingTests].filter( (_, i) => i !== idx diff --git a/src/linting/NewLintSuggestion.svelte b/src/linting/NewLintSuggestion.svelte index 5aeef27d..50626659 100644 --- a/src/linting/NewLintSuggestion.svelte +++ b/src/linting/NewLintSuggestion.svelte @@ -84,6 +84,7 @@ failMessage: "", program: "true", }); + configStore.setEvalDisplayMode("lint-customization"); }} > Just give me a blank one