diff --git a/.gitignore b/.gitignore index 988e1adc..5b6d43c9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ bun.lockb # Generated files **/.docusaurus **/.cache-loader - +./test.ts roadmap lodash-main lessons-learnt-from-my-first-open-source-project.md diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6bbe65b6..c026b611 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,11 +1,11 @@ { "recommendations": [ "denoland.vscode-deno", - "biomejs.biome", "oven.bun-vscode", "yoavbls.pretty-ts-errors", "bradlc.vscode-tailwindcss", "unifiedjs.vscode-mdx", - "github.vscode-github-actions" + "github.vscode-github-actions", + "aaron-bond.better-comments" ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 95cf5354..cf98e2e5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,13 +13,13 @@ "detail": "Check the source files for Deno compliancy or regressions." }, { - "type": "bun", + "type": "npm", "script": "bun test", "label": "Test 🧪", "detail": "Run the test suites for all publicly exported functions." }, { - "type": "bun", + "type": "npm", "script": "bun run build.ts", "detail": "Build library and types to the local filesystem. For development only.", "label": "Build 🏗", diff --git a/build.md b/build.md new file mode 100644 index 00000000..e69de29b diff --git a/build.ts b/build.ts index 9af8c5cd..49ec5e78 100755 --- a/build.ts +++ b/build.ts @@ -31,6 +31,6 @@ await build({ target: "node", external: ["culori"], naming: "node/huetiful.esm.js", - }).then(logger("Node")); + }).then(logger("Node")) await $`bun tsup --format=esm ./lib/index.ts --dts-only --outDir=./build`; await $`du -sh build/*`; diff --git a/bun.lockb b/bun.lockb index c98fb3bc..42c15311 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/deno.lock b/deno.lock index b979a216..2c4f10c6 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,8 @@ "version": "4", "specifiers": { "jsr:@std/assert@*": "1.0.7", + "jsr:@std/assert@^1.0.8": "1.0.8", + "jsr:@std/expect@*": "1.0.8", "jsr:@std/internal@^1.0.5": "1.0.5" }, "jsr": { @@ -11,6 +13,19 @@ "jsr:@std/internal" ] }, + "@std/assert@1.0.8": { + "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/expect@1.0.8": { + "integrity": "27e40d8f3aefb372fc6a703fb0b69e34560e72a2f78705178babdffa00119a5f", + "dependencies": [ + "jsr:@std/assert@^1.0.8", + "jsr:@std/internal" + ] + }, "@std/internal@1.0.5": { "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" } diff --git a/lib/collection.ts b/lib/collection.ts index 45b35e2e..8496c40c 100644 --- a/lib/collection.ts +++ b/lib/collection.ts @@ -1,24 +1,16 @@ import { - and, chnDiff, ctrst, dstnce, - eq, + entries, filteredColl, iterator, map, mcchn, - or, sortedColl, values, } from "./internal.ts"; -import { - achromatic, - family, - luminance, - mc, - token, -} from "./utils.ts" +import { achromatic, family, luminance, mc, token } from "./utils.ts"; import { averageAngle, averageNumber } from "culori/fn"; import { limits } from "./constants.ts"; import type { @@ -31,7 +23,6 @@ import type { } from "./types.d.ts"; import type { ColorToken } from "./types.d.ts"; - /** * Computes statistical values about the factors the passed in collection. * @@ -71,11 +62,12 @@ import type { ColorToken } from "./types.d.ts"; * @param options The optional overrides to customize the computing behaviour for the factors. */ function stats(collection: Collection = [], options: StatsOptions = { - factor: undefined, relative: false, colorspace: 'lch', against: 'cyan' -} -): Stats { - - const { factor, relative, against, colorspace } = options + factor: undefined, + relative: false, + colorspace: "lch", + against: "cyan", +}): Stats { + const { factor, relative, against, colorspace } = options; const hexColors = map(collection, token); const getStatsObject = (fact: Factor) => { @@ -83,8 +75,8 @@ function stats(collection: Collection = [], options: StatsOptions = { sortedColl(a, b, "asc")(hexColors); // @ts-ignore:` - return or( - and(eq(relative, true), { + return ( + relative === true && { chroma: sortedTokens( fact, chnDiff( @@ -93,14 +85,10 @@ function stats(collection: Collection = [], options: StatsOptions = { ), ), luminance: (() => { - - - const cb1 = - (a?: ColorToken) => - (b?: ColorToken) => - Math.abs( - luminance(a) - luminance(b), - ); + const cb1 = (a?: ColorToken) => (b?: ColorToken) => + Math.abs( + luminance(a) - luminance(b), + ); return sortedTokens(fact, cb1(against)); })(), lightness: sortedTokens( @@ -118,7 +106,7 @@ function stats(collection: Collection = [], options: StatsOptions = { fact, ctrst(against), ), - }), + } || { chroma: sortedTokens( fact, @@ -133,43 +121,42 @@ function stats(collection: Collection = [], options: StatsOptions = { fact, mc(`${colorspace}.h`), ), - }, + } )[fact]; }; - const len: number = values(collection).length; - const factorStats = (fact: Factor) => { - // @ts-ignore: - /** - * - * @param b The callback func for computing the targeted factor. Must be unary - * @param c The function to wrap the resulting collection of computed factors in. - */ - const callback = - (b: (x?: ColorToken) => number) => - (c: (x: number[]) => number) => - c(map(collection, b) as number[]); + const len: number = values(collection).length, + factorStats = (fact: Factor) => { + // @ts-ignore: + /** + * @param b The callback func for computing the targeted factor. Must be unary + * @param c The function to wrap the resulting collection of computed factors in. + */ + const callback = + (b: (x?: ColorToken) => number) => + (c: (x: number[]) => number) => + c(map(collection, b) as number[]); - // @ts-ignore: - return { - chroma: callback(mc(mcchn("c", colorspace)))( - averageNumber, - ), - distance: callback(dstnce(against))( - averageNumber, - ), + // @ts-ignore: + return { + chroma: callback(mc(mcchn("c", colorspace)))( + averageNumber, + ), + distance: callback(dstnce(against))( + averageNumber, + ), - hue: callback(mc(`${colorspace}.h`))( - averageAngle, - ), - lightness: callback(mc(mcchn("l", colorspace)))( - averageNumber, - ), - contrast: callback(ctrst(against))( - averageNumber, - ), - luminance: callback(luminance)(averageNumber), - }[fact]; - }; + hue: callback(mc(`${colorspace}.h`))( + averageAngle, + ), + lightness: callback(mc(mcchn("l", colorspace)))( + averageNumber, + ), + contrast: callback(ctrst(against))( + averageNumber, + ), + luminance: callback(luminance)(averageNumber), + }[fact]; + }; const commonStats = (fact: Factor) => { const [x, y] = [ getStatsObject(fact)[0], @@ -177,22 +164,22 @@ function stats(collection: Collection = [], options: StatsOptions = { ]; return { - against: or( - and( - or( - relative, - eq( - fact, - or("contrast", "distance"), - ), - ), - against, - ), - null, + against: ( + ( + ( + relative || + ( + fact === + "contrast" || "distance" + ) + ) && + against + ) || + null ), colors: [x.color, y.color], // @ts-ignore: - mean: factorStats(fact)(collection), + mean: factorStats(fact), extremums: [x[fact], y[fact]], families: [family(x.color), family(y.color)], @@ -200,16 +187,15 @@ function stats(collection: Collection = [], options: StatsOptions = { }; // @ts-ignore: - const statsObject = iterator(factor, commonStats) as Stats + const statsObject = iterator(factor, commonStats) as Stats; // @ts-ignore: - statsObject.achromatic = - values(collection).filter(achromatic).length / len; + statsObject.achromatic = values(collection).filter(achromatic).length / len; // @ts-ignore: statsObject.colorspace = colorspace; - return statsObject + return statsObject; } /** @@ -252,49 +238,58 @@ console.log( // [ 'brown', 'red', 'green', 'purple' ] */ -function sortBy(collection: Collection = [], options: SortByOptions = { - factor: undefined, relative: false, - colorspace: 'lch', against: 'cyan', order: 'asc' -}): Collection { +function sortBy(collection: Collection = [], options?: SortByOptions): Collection { // @ts-ignore: - const { against, colorspace, factor, order, relative } = - options + let { against, colorspace, factor, order, relative } = options || {}; + + factor = factor || undefined + against = against || 'cyan' + colorspace = colorspace || 'lch' + relative = relative || false + order = order || 'asc' // lightness and chroma channel constants respectively const [lightnessChannel, chromaChannel] = [ "l", "c", ].map((w) => mcchn(w, colorspace, false)); + for (const c in entries(collection)) + // @ts-ignore: + collection[c[0]] = token(c[1], { kind: 'obj', targetMode: 'lch' }); + + // returns factor cbs determined by the options const callback = (fact: Factor) => { const lmnce = (b: ColorToken) => - Math.abs(luminance(against) - luminance(b)), sort = (a: unknown) => - sortedColl(fact, a, order); + Math.abs(luminance(against) - luminance(b)), + sort = (a: unknown) => sortedColl(fact, a, order); const u = (ch: string) => mc(`${colorspace}.${ch}`) as unknown as string; // @ts-ignore: fact is used as the index - return or( - and(relative, { - chroma: sort( - chnDiff(against, u(chromaChannel)), - ), - hue: sort(chnDiff(against, u("h"))), - luminance: sort(lmnce), - lightness: sort( - chnDiff(against, u(lightnessChannel)), - ), - }), - { - chroma: sort(u(chromaChannel)), - hue: sort(u("h")), - luminance: sort(luminance), - distance: sort(dstnce(against)), - contrast: sort(ctrst(against)), - lightness: sort(u(lightnessChannel)), - }, - )[fact](collection); + return (relative && { + chroma: sort( + chnDiff(against, u(chromaChannel)), + ), + hue: sort(chnDiff(against, u("h"))), + luminance: sort(lmnce), + lightness: sort( + chnDiff(against, u(lightnessChannel)), + ), + } || + { + chroma: sort(u(chromaChannel)), + hue: sort(u("h")), + luminance: sort(luminance), + distance: sort(dstnce(against)), + contrast: sort(ctrst(against)), + lightness: sort(u(lightnessChannel)), + }) + [fact](collection); }; + + + // @ts-ignore: return iterator(factor, callback); } @@ -344,11 +339,6 @@ function sortBy(collection: Collection = [], options: SortByOptions = { // // set the callbacks depending on the type of factorStats // } - - - - - /** * Filters a collection of colors using the specified `factor` as the criterion. * @@ -370,7 +360,7 @@ function sortBy(collection: Collection = [], options: SortByOptions = { * * `'hue'` - Returns colors in the specified hue ranges between 0 to 360. * * :::tip - * + * * For the `chroma` and `lightness` factors, the range is internally normalized to the supported ranges by the `colorspace` in use if it is out of range. * This means a value in the range `[0,1]` will return, for example if you pass `startLightness` as `0.3` it means `0.3 (or 30%)` of the channel's supported range. * But if the value of either `start` or `end` is above 1 AND the `colorspace` in use has an `end` range higher than 1 then the value is treated as is else the value is treated as if in the range `[0,100]` and will return the normalized value. @@ -402,9 +392,17 @@ function sortBy(collection: Collection = [], options: SortByOptions = { */ function filterBy(collection: Collection = [], options: FilterByOptions = { - against: 'cyan', colorspace: 'lch', factor: undefined, ranges: undefined + against: "cyan", + colorspace: "lch", + factor: undefined, + ranges: undefined, }): Collection { - const { against, colorspace, factor, ranges } = options + let { against, colorspace, factor, ranges } = options || {}; + + // handling defaults internally helps avoid undefined values as compared to passing it to the parameter list + against = against || "cyan"; + colorspace = colorspace || "lch"; + factor = factor || undefined; const filter = (cb: unknown) => (fact: Factor) => filteredColl(fact, cb)(collection, start, end), @@ -428,9 +426,9 @@ function filterBy(collection: Collection = [], options: FilterByOptions = { const callback = (fact: Factor) => { // @ts-ignore: - start = or(ranges[fact][0], def_ranges[fact][0]); + start = ranges[fact][0] || def_ranges[fact][0]; // @ts-ignore: - end = or(ranges[fact][1], def_ranges[fact][1]); + end = ranges[fact][1] || def_ranges[fact][1]; return { chroma: filter( diff --git a/lib/generators.ts b/lib/generators.ts index d764fc66..1a4c4606 100644 --- a/lib/generators.ts +++ b/lib/generators.ts @@ -48,7 +48,7 @@ import type { InterpolatorOptions, PairedSchemeOptions, SchemeOptions, - TokenOptions, + } from "./types.d.ts"; @@ -87,13 +87,13 @@ console.log(hueShiftedPalette); '#3b0c3a' ] */ -function hueshift( - baseColor?: Color, options?: Options +function hueshift( + baseColor?: ColorToken, options: HueshiftOptions = {} ): Collection { let { num, hueStep, minLightness, maxLightness, easingFn, tokenOptions } = or( options, {}, - ) as Options; + ) as HueshiftOptions; easingFn = or(easingFn, ef); // @ts-ignore: @@ -102,7 +102,7 @@ function hueshift( baseColor = token(baseColor, { kind: "obj", targetMode: "lch", - }) as Color; + }) as ColorToken; const z = [baseColor]; // // if value is beyond max normalize all the values ensuring that the end is higher than start @@ -148,8 +148,8 @@ function hueshift( }, ]; - z.push(x as Color); - z.unshift(y as Color); + z.push(x as ColorToken); + z.unshift(y as ColorToken); } return Array.from(new Set(z)).map((c) => token(c, tokenOptions)); @@ -173,9 +173,8 @@ console.log(pastel("green")) // #036103ff */ -function pastel( - baseColor: Color, - options?: Options, +function pastel( + baseColor?: ColorToken, ): ColorToken { const w = [ [0.3582677165354331, 0.996078431372549, 16538982.504333857], @@ -204,8 +203,8 @@ function pastel( h: token(baseColor, { targetMode: "hsv", kind: "obj" }).h, }); - // @ts-ignore: - return token(q, options?.tokenOptions); + + return q } /** @@ -227,11 +226,11 @@ function pastel( console.log(pair("green",{hueStep:6,num:4,tone:'dark'})) // [ '#008116ff', '#006945ff', '#184b4eff', '#007606ff' ] */ -function pair( - baseColor: Color, - options?: Options, +function pair( + baseColor?: ColorToken, + options?: PairedSchemeOptions, ): Collection | ColorToken { - let { num, via, hueStep, colorspace } = or(options, {}) as Options; + let { num, via, hueStep, colorspace } = options as PairedSchemeOptions via = or(via, "light"); hueStep = or(hueStep, 5); colorspace = or(colorspace, "lch65"); @@ -251,10 +250,6 @@ function pair( light: { l: 100, c: 0, h: 0, mode: colorspace }, }[via as string]; - // Since the interpolation returns half duplicate values we double the sample value - // Guard the num param against negative values and floats - - // Return a slice of the array from the start to the half length of the array return interpolator([baseColor, tone, destinationColor], { colorspace: "lch", @@ -427,10 +422,10 @@ function discover( ); // Create the classic palettes per valid color token in the collection - for (const key of colorTokenKeys) { + for (const key of colorTokenKeys) // @ts-ignore: palettes[key] = scheme(colors[key], { kind: kind }); - } + // @ts-ignore: let currentPalette = []; @@ -438,26 +433,26 @@ function discover( if (eq(typeof kind, "string")) { // @ts-ignore: palettes[key] = availableColors(key, palettes); - if (gt(currentPalette.length, 1)) { + if (gt(currentPalette.length, 1)) // @ts-ignore: palettes[key] = palettes[key].filter((a, b) => // @ts-ignore: not(customInRange(a, currentPalette[b])) ); - } + // @ts-ignore: currentPalette = palettes[key]; } else { // if the color token value is an object, iterate through the available palette keys // @ts-ignore: - for (const paletteType of keys(palettes[key])) { + for (const paletteType of keys(palettes[key])) // @ts-ignore: palettes[key][paletteType] = availableColors( paletteType, // @ts-ignore: palettes[key], ); - } + } } @@ -481,10 +476,10 @@ console.log(earthtone("pink",'lch',{earthtones:'clay',samples:5 })) */ function earthtone( - baseColor: ColorToken, - options: EarthtoneOptions, + baseColor?: ColorToken, + options: EarthtoneOptions = {}, ): ColorToken | Array { - let { num, earthtones, colorspace, kind, closed } = options || {}; + let { num, earthtones, colorspace, kind, closed } = options; earthtones = or(earthtones, "dark"); @@ -552,13 +547,13 @@ console.log(scheme("triadic")("#a1bd2f")) */ // @ts-ignore: function scheme( - baseColor: ColorToken = 'cyan', + baseColor?: ColorToken, options: SchemeOptions = { colorspace: 'lch', kind: ['analogous'], easingFn: ef }, ): Collection { - const { colorspace, kind, easingFn } = options || {}; + const { colorspace, kind, easingFn } = options || {} // @ts-ignore: baseColor = token(baseColor, { targetMode: colorspace, kind: "obj" }); diff --git a/lib/internal.ts b/lib/internal.ts index 790ceb8d..5161db15 100644 --- a/lib/internal.ts +++ b/lib/internal.ts @@ -13,6 +13,7 @@ import { mc } from "./utils.ts"; import { contrast } from "./accessibility.ts"; import type { Collection, ColorToken, Factor, Colorspaces } from "./types.d.ts"; +import { hue } from "./constants.ts"; const { keys, entries, values } = Object; @@ -67,14 +68,14 @@ const dstnce = (a: unknown) => (b: unknown) => */ function iterator( t: string[] | undefined, - z: (x: unknown) => unknown, - y = ["hue", "chroma", "lightness", "distance", "contrast", "luminance"], + z: (x: unknown) => unknown, y = ["hue", "chroma", "lightness", "distance", "contrast", "luminance"] + ) { - const p = {}; + const p = {} + // @ts-ignore: + // @ts-ignore: - if (gt(values(t).length, 1)) - // @ts-ignore: - if (isArray(t)) for (const k of values(t)) p[k] = z(k); + if (isArray(t) && t?.length >= 1) for (const k of values(t)) p[k] = z(k); // @ts-ignore: if (eq(t, undefined)) for (const k of y) p[k] = z(k); @@ -156,7 +157,19 @@ function colorObj(a: string, b: unknown) { } -function customFindKey(u: object, v: number) { + + + +// customCConcat ??? +// capture the amount of elements in hue +// for each element; slice the array from index 0 (the color family string index) +// +function customFindKey(u: typeof hue, v: number) { + + + + + // If the color is achromatic return the string gray return keys(u) .filter((a) => { @@ -172,25 +185,6 @@ function customFindKey(u: object, v: number) { .toString(); } -function customConcat(h = {}) { - return and( - eq(typeof h, "object"), - (() => { - const res = []; - const k = keys(h); - - - - for (const g of k) { - //@ts-ignore - res.push(...h[g]); - } - - return res.flat(1); - })(), - ); -} - function adjustHue(val: number) { let out = 0 @@ -203,7 +197,12 @@ function chnDiff(x?: ColorToken, s?: string) { return (y?: ColorToken) => { const cb = (c?: ColorToken) => mc(s)(c); - return or(and(lt(cb(x), cb(y)), take(cb(y), cb(x))), take(cb(x), cb(y))); + return ( + + ( + + lt(cb(x), cb(y)) && take(cb(y), cb(x))) || take(cb(x), cb(y)) + ); }; } @@ -388,9 +387,9 @@ function sortedColl(fact: Factor, cb: unknown, o = "asc") { - if (isArray(c)) { - return data - } + if (isArray(c)) + return data; + const out = new Map(); @@ -418,31 +417,31 @@ function filteredColl(fact: Factor, cb: unknown) { - if (and(eq(typeof s, "number"), eq(typeof e, "number"))) { + if (and(eq(typeof s, "number"), eq(typeof e, "number"))) + data = data .filter((j) => inRange(j[fact], s as number, e as number)) - } + const startOp = reOp(s) as keyof typeof operators const endOp = reOp(e) as keyof typeof operators const start: number = Number.parseFloat(reNum(s).toString()) const end: number = Number.parseFloat(reNum(e).toString()) - if (and(startOp, endOp)) { + if (and(startOp, endOp)) data = data .filter((l) => and(operators[startOp](l[fact], start), operators[endOp](l[fact], end)) - ) + ); - } - else { + else data = data.filter((l) => end ? and(operators[or(startOp, endOp)](l[fact], start), inRange(l[fact], end)) : operators[or(startOp, endOp)](l[fact], start) - ) + ); + - } return data.map((l) => l.color) @@ -461,7 +460,7 @@ function getSrcMode(c: ColorToken): Colorspaces { // @ts-ignore - return and(isArray(c), neq(typeof c[0], "number")) ? c[0] : eq(typeof c, 'object') ? c?.mode : 'rgb' + return isArray(c) && typeof c[0] != "number" ? c[0] : typeof c === 'object' ? c?.mode : 'rgb' } const ctrst = (a: unknown) => (b: unknown) => @@ -481,7 +480,6 @@ export { filteredColl, customFindKey, colorObj, - customConcat, inRange, rand, isInt, diff --git a/lib/palettes.ts b/lib/palettes.ts index dc620687..7895fc0c 100644 --- a/lib/palettes.ts +++ b/lib/palettes.ts @@ -1,15 +1,14 @@ import { differenceHyab, nearest as nrst } from "culori/fn"; import type { Collection, - ColorToken, + DivergingScheme, QualitativeScheme, ScaleValues, SequentialScheme, - Swatch, Tailwind, } from "./types.d.ts"; -import { and, eq, gt, keys, or, values } from "./internal.ts"; +import { and, eq, isArray, keys, or, values } from "./internal.ts"; const tailwind = { indigo: { @@ -259,26 +258,38 @@ const tailwind = { /** * Returns the specified scheme from the passed in color map - * @param {string} s The palette type to return. - * @param {Collection} obj The color map with the `scheme`s as keys and `ColorToken | Array` as values. - * @returns {Collection} The collection of colors from the specified `scheme`. + * @param s The palette type to return. + * @param obj The color map with the `scheme`s as keys and `ColorToken | Array` as values. + */ -function hasScheme(s: string, obj: Collection) { +function hasScheme(s: string = '', obj: Collection = {}) { // Map all schemes keys to lower case - const o = keys(obj).map((k) => k.toLowerCase()), p = s.toLowerCase(); + // @ts-ignore: + const o = keys(obj), cb = x => obj[o.find((v) => v.toLowerCase() === x.toLowerCase())] + + let res = {} + - return gt(o.indexOf(p), -1) + if (isArray(s)) + + for (const x of s) + // @ts-ignore: + res[x.toLowerCase()] = cb(x); + else // @ts-ignore: - ? obj[p] - : Error(`${s} is an invalid scheme option.`); + res = cb(s); + + + return res + || Error(`${s} is an invalid scheme option.`); } /** * A wrapper function for ColorBrewer's map of sequential color schemes. - * @param scheme The name of the scheme. + * @param scheme The name of the scheme. * @returns {Collection|import('../types.js').ColorToken} A collection of colors in the specified colorspace. The default is hex if `colorspace` is `undefined.` * @example * - * import { sequential } from 'huetiful-js + * import { sequential } from 'huetiful-js' console.log(sequential("OrRd")) @@ -295,7 +306,7 @@ console.log(sequential("OrRd")) */ function sequential( - scheme?: Scheme, + scheme?: Scheme | Array, ): Scheme[] { const so = { OrRd: [ @@ -530,7 +541,7 @@ console.log(diverging("Spectral")) '#bf5b17', '#666666' ] */ -function diverging(scheme?: Scheme): Scheme[] { +function diverging(scheme?: Scheme | Array): Scheme[] { const so = { Spectral: [ "#9e0142", @@ -672,7 +683,7 @@ console.log(qualitative("Accent")) */ function qualitative( - scheme?: Scheme, + scheme?: Scheme | Array, ): Scheme[] { const so = { Set2: [ @@ -784,18 +795,17 @@ function qualitative( console.log(nearest(cols, 'blue', 3)); // [ '#a855f7', '#8b5cf6', '#d946ef' ] */ -function nearest(collection: Collection | "tailwind", options:{num?:number;against?:ColorToken}) { - let { against, num } = options || {}; - num = or(num, 1); - against = or(against, "cyan"); - const f = (a: unknown, b: unknown) => { - const o = nrst( - values(a as object), - differenceHyab(), - (c) => c as string, - )(b as string, num); - return or(and(eq(num, 1), o[0]), o); - }; +function nearest(collection: Collection | "tailwind", options: { num: 1; against: 'cyan' }) { + const { against, num } = options, + + f = (a: unknown, b: unknown) => { + const o = nrst( + values(a as object), + differenceHyab(), + (c) => c as string, + )(b as string, num); + return or(and(eq(num, 1), o[0]), o); + }; return or( and(eq(collection, "tailwind"), f(colors("all"), against)), @@ -826,11 +836,11 @@ function nearest(collection: Collection | "tailwind", options:{num?:number;again // It returns a function that can be called with an optional value parameter console.log(colors('red')); // [ - '#fef2f2', '#fee2e2', - '#fecaca', '#fca5a5', - '#f87171', '#ef4444', - '#dc2626', '#b91c1c', - '#991b1b', '#7f1d1d' + '#fef2f2', '#fee2e2', + '#fecaca', '#fca5a5', + '#f87171', '#ef4444', + '#dc2626', '#b91c1c', + '#991b1b', '#7f1d1d' ] @@ -841,12 +851,12 @@ function nearest(collection: Collection | "tailwind", options:{num?:number;again */ -function colors( - shade?: F | "all", - value?: S, -) { +function colors( + shade?: Tailwind | "all", + value?: ScaleValues, +): Array { const w = tailwind; - value = value?.toString() as S; + value = value?.toString() as ScaleValues; const [d, k] = ["all", keys(w)]; const [p, q] = [ @@ -867,31 +877,33 @@ function colors( ].includes(i?.toString()), ]; - shade = shade?.toLowerCase() as F; - let o: unknown; - if (eq(shade, d)) { + shade = shade?.toLowerCase() as Tailwind; + let o: string[]; + if (eq(shade, d)) // @ts-ignore: if (q(value)) o = k.map((y) => w[y][value]); // @ts-ignore: else o = k.map((y) => values(w[y])).flat(2); - } else if (p(shade)) { + else if (p(shade)) // @ts-ignore: if (q(value)) o = w[shade][value as string]; else o = values(w[shade]); - // @ts-ignore: - } else if (or(!shade, and(!shade, !value))) o = k.map((h) => w[h]); + // @ts-ignore: + else if (or(!shade, and(!shade, !value))) o = k.map((h) => w[h]); + - return o as Swatch; + // @ts-ignore: + return o; } export { - + colors, diverging, nearest, qualitative, - + sequential, }; diff --git a/lib/types.d.ts b/lib/types.d.ts index 7b774d98..e21d4afd 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -576,25 +576,5 @@ export type ColorFamily = | "purple" | "gray"; -export type ComplimentaryOptions = { - /** - * Randomize the returned color. Useful if you wish to keep your palettes unique. - * - * Default is `false` - */ - randomOffset?: boolean; - - /** - * Array of two numbers in the range `[0,1]` which can be used to override the internal extremum defaults (min and max respectively) when generating a randomized complimentary color. - * - * Only works if `randomOffset` is `true`. - */ - extremums?: [number?, number?]; -}; - -export type Swatch = T extends Tailwind - ? V extends ScaleValues ? `${T}[${V}]` - : Array<`all[${V | "500"}]`> - : Array; export type LightnessOptions = { amount?: number; darken?: boolean }; diff --git a/lib/utils.ts b/lib/utils.ts index 65e19838..9da0748f 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -2,7 +2,6 @@ import { colorsNamed, formatHex, formatHex8, - interpolate, modeHsv, modeLab, modeLab65, @@ -17,9 +16,8 @@ import { } from "culori/fn"; import "culori/css"; import { - adjustHue, + and, - customConcat, eq, exprParser, floorCeil, @@ -30,15 +28,11 @@ import { gte, inRange, isArray, - keys, lte, max, min, - neq, - not, or, - rand, - take, + } from "./internal.ts"; import { hue } from "./constants.ts"; import type { @@ -46,111 +40,127 @@ import type { ColorFamily, ColorToken, ColorTuple, - ComplimentaryOptions, + LightnessOptions, TokenOptions, } from "./types.d.ts"; +import { interpolator } from "./generators.ts"; /** * - * Returns the color token's alpha channel value. + * Sets and retrieves the color token's alpha (or opacity). * - * If the the `amount` parameter is passed in, it sets the color token's alpha channel with the `amount` specified + * If the the `amount` argument is passed in, it sets the color token's alpha channel with the `amount` specified * and returns the color as a hex string. * * :::tip + * * * Also supports math expressions as a `string` for the `amount` parameter. * For example `*0.5` which means the value multiply the current alpha by `0.5` and set the product as the new alpha value. * In short `currentAlpha * 0.5 = newAlpha`. The supported symbols are `* - / +`. + * * ::: * * If the `alpha` channel is `undefined`, it defaults to `1`. - * + * * @param color The color with the opacity/alpha channel to retrieve or set. * @param amount The value to apply to the opacity channel. The value is between `[0,1]` * @example - * import { alpha } from 'huetiful-js' - * - * // Getting the alpha + import { alpha } from 'huetiful-js' + + // Getting the alpha console.log(alpha('#a1bd2f0d')) // 0.050980392156862744 -// Setting the alpha +// Setting the alpha -let myColor = alpha('b2c3f1', 0.5) +const myColor = alpha('b2c3f1', 0.5) console.log(myColor) // #b2c3f180 */ -function alpha(color: ColorToken = 'cyan', + +function alpha( + color: ColorToken = "cyan", amount: Amount | undefined = undefined, ): Amount extends undefined ? number : ColorToken { // @ts-ignore: - let alphaChannel: number; + let alphaChannel: number, + len: number, + hasMode: string | undefined, + hasAlpha: boolean, + alphaIdx: number + - if (isArray(color)) { - alphaChannel = eq( + + if (isArray(color)) + // @ts-ignore: + + [len, hasMode, hasAlpha, alphaIdx, alphaChannel] = + [color?.length, (color as ColorTuple).find((c) => + eq(typeof c, "string") + ), (color as ColorTuple).filter((channel) => eq(typeof channel, "number") - ) - .length, - 4, - ) - ? color[(color as ColorTuple)?.length - 1] - : 1; - } else if (eq(typeof color, "string")) { - alphaChannel = and( - gte((color as ColorTuple)?.length, 8), + ).length === 4, + + // @ts-ignore: + + hasAlpha ? len - 1 : hasMode ? 4 : 3, color[alphaIdx] + ]; + + + + if (eq(typeof color, "string")) + alphaChannel = ( + gte((color as ColorTuple)?.length, 8) && // @ts-ignore: - not(colorsNamed?.color?.toLowerCase()), + !colorsNamed[(color as string).toLowerCase()] ) ? Number.parseInt( (color as string)?.slice((color as string)?.length - 2), 16, - ) + ) / 255 : 1; - } else if (eq(typeof color, "object")) { + // @ts-ignore: + if (typeof color === "object" && !len) // @ts-ignore: alphaChannel = color?.alpha; - } - if (not(amount)) { - // @ts-ignore: - return alphaChannel ? alphaChannel : 1 - } - amount = or( + + + + // if amount is undefined, return the alpha channel + if (typeof amount === 'undefined') // @ts-ignore: - and(neq(typeof amount, "number"), exprParser(alphaChannel, amount)), + return alphaChannel && alphaChannel || 1; + + + + + switch (typeof amount) { + case 'number': + // @ts-ignore: + amount = (inRange(amount, 0, 1) && amount) || give(amount, 100) + break; // @ts-ignore: - or(and(inRange(amount, 0, 1), amount), give(amount, 100)), - ); + case 'string': amount = exprParser(alphaChannel, amount) - if (isArray(color)) { - // Get the alpha index - - color[ - or( - and( - or( - eq((color as ColorTuple).length, 5), - and( - neq(color[0], "string"), - eq((color as ColorTuple)?.length, 4), - ), - ), - take((color as ColorTuple).length, 1), - ), - 3, - ) - ] = amount; } - if (eq(typeof color, "object")) { + + if (isArray(color)) + // @ts-ignore: + color[alphaIdx] = amount; + + + // @ts-ignore: + if (typeof color === "object" && !len) // @ts-ignore: color.alpha = amount; - } else { + if (typeof color == 'number' || typeof color == 'string') { const colorObject = token(color, { kind: "obj" }); // @ts-ignore: colorObject.alpha = amount; @@ -158,7 +168,6 @@ function alpha(color: ColorToken = 'cyan', } // @ts-ignore: return color; - } /** @@ -168,7 +177,7 @@ function alpha(color: ColorToken = 'cyan', * @param modeChannel The mode and channel to be retrieved. For example `'rgb.b'` will return the value of the blue channel in the RGB color space of that color. * @example -| * + * import { mc } from 'huetiful-js' console.log(mc('rgb.g')('#a1bd2f')) @@ -176,55 +185,35 @@ console.log(mc('rgb.g')('#a1bd2f')) * */ - - -function mc(modeChannel = 'lch.h') { +function mc(modeChannel = '') { /** * @param color Any recognizable color token. * @param The value to set on the queried channel. Also supports expressions as strings e.g `"#fc23a1"` `"*0.5"` - * @returns {number|ColorToken} */ - return ( - color: ColorToken = 'cyan', - value?: number | string, - ): Value extends number | string ? ColorToken : number => { + return ( + color?: ColorToken, + value?: Value, + ): Value extends number ? ColorToken : number => { const [mode, channel] = modeChannel.split("."); + // @ts-ignore: - let colorObject = token(color, { targetMode: mode, kind: "obj" }); - let currentChannel: number; - - if (eq(typeof color, "object")) { - if (isArray(color)) { - currentChannel = // @ts-ignore: - (eq(typeof color[0], "string") ? color.slice(1) : color)[ - gmchn(mode).indexOf(channel) - ]; - } - } else { - // @ts-ignore: - currentChannel = colorObject[channel]; - } + const colorObject = token(color, { targetMode: mode, kind: "obj" }); + // @ts-ignore: + const currentChannel: number = colorObject[channel]; - if (value) { - if (eq(typeof value, "number")) { - // @ts-ignore: - colorObject[channel] = value; - } else if (eq(typeof value, "string")) { + if (typeof value != 'undefined') + // @ts-ignore: + colorObject[channel] = typeof value === 'number' // @ts-ignore: - colorObject = exprParser(colorObject[channel], value); - } else { - throw Error( - `${typeof value}} ${value} is not a valid value for a color token`, - ); - } + && value || exprParser(colorObject[channel], value); + // @ts-ignore: + return value && colorObject || currentChannel; + - } - // @ts-ignore: - return not(value) ? currentChannel : colorObject; }; } @@ -235,7 +224,7 @@ function mc(modeChannel = 'lch.h') { * @example import { achromatic } from "huetiful-js"; - +S achromatic('pink') // false @@ -270,33 +259,32 @@ console.log(grays.map(achromatic)); ] */ -function achromatic(color: ColorToken = 'cyan'): boolean { +function achromatic(color: ColorToken): boolean { // @ts-ignore: color = token(color, { kind: "obj", targetMode: "lch" }); // If a color has no lightness then it has no hue // so its technically not achromatic since white and black are not grayscale // @ts-ignore: - const isFalsy = (x: unknown) => - or(or(eq(typeof x, "undefined"), eq(x, 0)), Number.isNaN(x as number)); + const isFalsy = ( + x: unknown, + ) => ((typeof x === "undefined" || x === 0) || Number.isNaN(x as number)); - return or( - and( - and( // @ts-ignore: - or(isFalsy(color.l), gte(color.l, 100)), - // @ts-ignore: - or(not(isFalsy(color.c)), isFalsy(color.c)), - ), - false, - ), + return ( + ( // @ts-ignore: + (isFalsy(color.l) || color.l >= 100) && + // @ts-ignore: + (!(isFalsy(color.c)) || isFalsy(color.c)) + ) && + false + ) || // @ts-ignore: - or(and(isFalsy(color.c), true), false), - ); + (isFalsy(color.c) && true || false); } /** * Darkens the color by reducing the `lightness` channel by `amount` of the channel. For example `0.3` means reduce the lightness by `0.3` of the channel's current value. - * @param color The color to darken. + * @param color The color to darken or lighten. * @param amount The amount to darken with. The value is expected to be in the range `[0,1]`. Default is `0.1`. * @example * @@ -315,24 +303,24 @@ console.log(brighten('blue', 0.3)); */ function lightness( - color: ColorToken = 'cyan', - options: LightnessOptions = { - amount: 0.1, darken: false - }, + color: ColorToken, + options?: LightnessOptions ): ColorToken { - const { amount, darken } = options; + let { amount, darken } = options || {} as LightnessOptions + amount = amount || 0.1; + darken = darken || false; const f = () => { const colorObject = token(color, { kind: "obj", targetMode: "lab" }); - if (typeof amount === "number") { + if (amount) // @ts-ignore: colorObject.l = (darken ? max : min)([ 100, // @ts-ignore: colorObject.l + 100 * (darken ? -amount : amount), ]); - } + // @ts-ignore: return token(colorObject); }; @@ -341,47 +329,38 @@ function lightness( } /** - * Parses any recognizable color to the specified `kind` of `ColorToken` type. + * Parses any recognizable color to the specified `kind`. * - * The `kind` option supports the following types as options: + * The `kind` option supports the following types as options (case-insensitive): * * * `'arr'` - Parses the color token to an array of channel values with the `colorspace` as the first element if the `omitMode` parameter is set to `false` in the `options` object. * * * `'num'` - Parses the color token to its numerical equivalent to a number between `0` and `16,777,215`. * - * The `numberType` can be used to specify which type of number to return if the `kind` option is set to `'number'`: + * The `numberType` can be used to specify which type of number to return if the `kind` option is set to `'num'`: * - `'hex'` - Hexadecimal number * - `'bin'` - Binary number * - `'oct'` - Octal number * - `'expo'` - Decimal exponential notation * - * * `'str'` - Parses the color token to its hexadecimal string equivalent. + * - `'str'` - Parses the color token to its hexadecimal string equivalent. * - * * `'obj'` - Parses the color token to a plain color object in the `mode` specified by the `targetMode` parameter in the `options` object.t + * - `'obj'` - Parses the color token to a plain color object in the `mode` specified by the `targetMode` parameter in the `options` object.t * * :::tip - * + * * If the color token has an explicit `alpha` (specified by the `alpha` key in color objects and as the fourth and last number in a color array) the string will be 8 characters long instead of 6. * * ::: * @param color The color token to parse or convert. * @param options Options to customize the parsing and output behaviour. - * @returns + */ function token( - color: ColorToken = 'cyan', - options: TokenOptions = { - - normalizeRgb: true, - numType: undefined, - omitMode: false, - omitAlpha: false, kind: 'str' - } + color: ColorToken = "cyan", + options?: TokenOptions, ): ColorToken { - - - // the mode definitions const modeDefinitions = { lrgb: modeLrgb, @@ -402,17 +381,15 @@ function token( numType, omitAlpha, normalizeRgb, - } = options; - - - console.log(options) - - srcMode = srcMode ? - srcMode - : getSrcMode(color as ColorToken); - console.log(srcMode) - + } = options || {}; + // Always handle defaults internally because if some property is not specified it will be null + normalizeRgb = normalizeRgb || false; + numType = numType || undefined; + omitAlpha = omitAlpha || false; + kind = (kind || "str")?.toLowerCase() as typeof kind; + omitMode = omitMode || false; + srcMode = srcMode && srcMode || getSrcMode(color as ColorToken); /** * An array of channel keys from the source colorspace. If undefined it defaults to 'rgb' @@ -424,44 +401,49 @@ function token( srcChannelValues: number[]; /** * the alpha channel captured if it exists in the color token - * @type{number} */ // @ts-ignore: const alphaValue = alpha(color); - let result = {}; - - - + let result: { [x: string]: number | string } = {}; + result.mode = srcMode; // Get the channels from passed in color token // if the color token is a string or number we just convert it to an object - if (isArray(color)) { + if (isArray(color)) // @ts-ignore: srcChannelValues = (color as ColorTuple) - .filter((a) => eq(typeof a, "number") - ); - } + .filter((a) => eq(typeof a, "number")); + - if (eq(typeof color, "object")) + // @ts-ignore: check if it does not have a length as well + if (eq(typeof color, "object") && !color?.length) // @ts-ignore: srcChannelValues = srcChannels.map((a) => color[a]); + if (eq(typeof color, "string")) // @ts-ignore: - result = eq(typeof color, "number") - ? num2c() - : parseToken(c2str(), "rgb"); + result = (typeof color === "number" && + num2c()) || + parseToken(c2str(), "rgb"); + // @ts-ignore: if (srcChannelValues) - for (const channel of srcChannels) { - // @ts-ignore: + for (const channel of srcChannels) result[channel] = srcChannelValues[srcChannels.indexOf(channel)]; - } - console.log(srcChannels, srcChannelValues, result) + if ((srcMode === "rgb" && normalizeRgb)) + // @ts-ignore: + if (srcChannels.some((c) => Math.abs(result[c]) > 1)) + for (const k of srcChannels) (result[k] as number) /= 255; + + + + if (targetMode) result = parseToken(result, targetMode); + function parseToken(col: unknown, mode?: unknown) { // @ts-ignore: return useMode(modeDefinitions[mode])(or(col, result)); @@ -469,16 +451,14 @@ function token( /** * converts any color token to an array or object equivalent */ - function c2col(col: unknown) { - const res = targetMode ? parseToken(result, targetMode) : result; - srcChannels = targetMode ? gmchn(targetMode) : srcChannels; - - if (and(and(eq(srcMode, "rgb"), normalizeRgb), not(targetMode))) - // @ts-ignore: - if (srcChannels.some((c) => gt(Math.abs(result[c]), 1))) - // @ts-ignore: - for (const k of srcChannels) result[k] /= 255; + function c2col(col: "obj" | "arr") { + const res = targetMode && + parseToken(result, targetMode) || + result; + if (targetMode) + // 🖕🏿 + srcChannels = gmchn(targetMode); if (eq(col, "obj")) { @@ -486,13 +466,11 @@ function token( // @ts-ignore: ? delete res.mode // @ts-ignore: - - : (res.mode = or(and(targetMode, targetMode), srcMode)); - + : (res.mode = (targetMode && targetMode) || srcMode); // @ts-ignore: omitAlpha ? delete res.alpha : (res.alpha = alphaValue); - return res + return res; } if (eq(col, "arr")) srcChannelValues = []; @@ -500,14 +478,14 @@ function token( // @ts-ignore: srcChannelValues[srcChannels.indexOf(k)] = res[k]; + // @ts-ignore: omitAlpha ? srcChannelValues : srcChannelValues.push(alphaValue); omitMode ? srcChannelValues // @ts-ignore: : srcChannelValues.unshift(targetMode ? targetMode : srcMode); - return srcChannelValues - + return srcChannelValues; } /** @@ -516,9 +494,6 @@ function token( function c2num() { const rgbObject: object = parseToken(c2str(), "rgb"); - /** - * @type {number|string} - */ // @ts-ignore: const result = ((255 * rgbObject.r) << 16) + // @ts-ignore: @@ -526,9 +501,9 @@ function token( // @ts-ignore: 255 * rgbObject.b; - return or( - and( - numType, + return ( + ( + numType && result.toString( { bin: 2, @@ -536,9 +511,9 @@ function token( expo: 6, oct: 8, }[numType?.toLowerCase() as string], - ), - ), - result, + ) + ) || + result ); } @@ -562,11 +537,10 @@ function token( */ function num2c() { // Ported from chroma-js with slight modifications - // - // - return and( - and(eq(typeof color, "number"), gte(color as number, 0)), - lte(color as number, 0xffffff), + + return ( + (eq(typeof color, "number"), gte(color as number, 0)) && + lte(color as number, 0xffffff) ) ? { r: ((color as number) >> 16) / 255, @@ -588,9 +562,11 @@ function token( } /** - * Gets the luminance of the passed in color token. + * Sets/Gets the luminance of the passed in color token. * - * If the `amount` parameter is passed in, it will adjust the luminance by interpolating the color with black (to decrease luminance) or white (to increase the luminance) by the specified `amount`. + * If the `amount` argument is passed in, it will adjust the luminance by interpolating the color with black (to decrease luminance) or white (to increase the luminance) by the specified `amount`. + * + * If the `amount` argument is not passed in however, it will simply return the color token's luminance. * @param color The color to retrieve or adjust luminance. * @param amount The amount of luminance to set. The value range is normalised between [0,1] * @example @@ -625,16 +601,19 @@ console.log(luminance(myColor)) // 0.4999999136285792 */ function luminance( - color: ColorToken = 'cyan', + color?: ColorToken, amount: number | undefined = undefined, ): Amount extends number ? ColorToken : number { - color = token(color) + color = token(color, { kind: 'obj', srcMode: 'rgb' }); + + // @ts-ignore: let result: unknown; - if (!amount) { + if (typeof amount === 'undefined') // @ts-ignore: return wcagLuminance(color); - } + + const w = "#ffffff", b = "#000000"; @@ -642,74 +621,87 @@ function luminance( let MAX_ITER = 20; if (eq(typeof amount, "number")) { - const currentLuminance = wcagLuminance(color as string); //Must add the overrides object to change parameters like easings, fixups, and the mode to perform the computations in. // use a bilinear interpolation // @ts-ignore: const f = (u, v) => { - const [mid, low] = [ - interpolate([u, v])(0.5), - // @ts-ignore: - wcagLuminance(color), - ]; + // @ts-ignore: + const [mid, low] = + interpolator([u, v], { num: 2 }) + // @ts-ignore: + + // @ts-ignore: - if (Math.abs(amount - low > EPS) || !MAX_ITER--) { + if (Math.abs(amount - low > EPS) || !MAX_ITER--) // close enough return mid; - } - if (gt(low, amount)) { + + if (gt(low, amount)) return f(u, mid); - } - return f(mid, v); + + return f(mid, v); }; - if (gt(currentLuminance, amount)) { + if (gt(currentLuminance, amount)) result = f(b, color); - } else { + else result = f(color, w); - } + } // @ts-ignore: return token(result); - } /** - * Gets the hue family which a color belongs to with the overtone included (if it has one.). + * Returns the hue family which the passed in color belongs to with the "overtone" included (if it has one.). * * For example `'red'` or `'blue-green'`. If the color is achromatic it returns the string `'gray'`. * @param color The color to query its shade or hue family. + * @param bias Returns the name of the hue family which is biasing the passed in color using the `'lch'` colorspace. If it has no bias it returns `false` on the `bias` property of the returned object. + * :::note + * + * This `bias` parameter replaces the `overtone()` function prior version `3.0.x`. + * + ::: + * * @example * * import { family } from 'huetiful-js' console.log(family("#310000")) + // 'red' */ -function family - (color: ColorToken): BiasedHues & ColorFamily { - if (neq(achromatic(color), true)) { - const [hueAngle, hueFamilies] = [ - mc('lch.h')(color), - keys(hue), - ]; - +function family( + color: ColorToken, + bias = false, +): BiasedHues & ColorFamily | { + hue: BiasedHues & ColorFamily; + bias: false | ColorFamily; +} { + // @ts-ignore: + const res = !achromatic(color) && hue.find((arr) => { // @ts-ignore: - return hueFamilies.find((o) => { - // @ts-ignore: - const hueRanges = customConcat(hue[o]); - return inRange(hueAngle, min(hueRanges), max(hueRanges)); - }); - } + const hueRanges = arr.slice(1).flat(1) as number[]; + + return inRange( + mc("lch.h")(color), + min(hueRanges), + max(hueRanges), + ); + })[0] || "gray"; // @ts-ignore: - return "gray"; + return bias && { + hue: res, + bias: ((/-/.test(res) && res.split("-")[1]) || false), + } || res; } /** @@ -729,114 +721,40 @@ let sample = [ console.log(temp(sample[2])); -// false +// 'cool' -console.log(map(sample, isCool)); +console.log(map(sample, temp)); -// [ true, false, true] +// [ 'cool', 'warm', 'cool'] */ -function temp(color: ColorToken = 'cyan'): "cool" | "warm" { - return or( - and( - keys(hue).some((hueFamily) => +function temp(color: ColorToken = "cyan"): "cool" | "warm" { + return ( + ( + hue.some((arr) => inRange( floorCeil(mc("lch.h")(color)), // @ts-ignore: - hue[hueFamily].cool[0], + arr[2][0], // @ts-ignore: - hue[hueFamily].cool[1], + arr[2][1], ) - ), - "cool", - ), - "warm", + ) && + "cool" + ) || + "warm" ); } -/** - * Returns the name of the hue family which is biasing the passed in color using the `'lch'` colorspace. - * - * * If an achromatic color is passed in it returns the string `'gray'` - * * If the color has no bias it returns `false`. - * @param color The color to query its overtone. - - * @example - * - * import { overtone } from "huetiful-js"; - * -console.log(overtone("fefefe")) -// 'gray' - -console.log(overtone("cyan")) -// 'green' - -console.log(overtone("blue")) -// false - */ -function overtone( - color: ColorToken = 'cyan', -): ColorFamily | false { - const hueFamily = family(color); - - // We check if the color can be found in the defined ranges - // @ts-ignore: - return or( - and(achromatic(color), "gray"), - // @ts-ignore: - or(and(/-/.test(hueFamily), hueFamily.split("-")[1]), false), - ) -} - -/** - * Returns the complimentary color of the passed in color token. A complimentary color is 180 degrees away on the hue channel. - * @param baseColor The color to retrieve its complimentary equivalent. - * @param options Optional overrides to customize behaviour. - * @example - * - * import { complimentary } from "huetiful-js"; - * - * -console.log(complimentary("pink", true)) -//// { hue: 'blue-green', color: '#97dfd7ff' } - -console.log(complimentary("purple")) -// #005700 - */ -function complimentary(baseColor: ColorToken, options: ComplimentaryOptions = { - randomOffset: true, extremums: [0, 1] -}): ColorToken { - const { randomOffset, extremums } = options - const MIN_EXTREMUM = 0.965995, - MAX_EXTREMUM = 1; - const complimentaryHueAngle = adjustHue( - mc("lch.h")(baseColor) + - (randomOffset - ? 180 * - rand( - // @ts-ignore: - or(extremums[0], MIN_EXTREMUM), - // @ts-ignore: - or(extremums[1], MAX_EXTREMUM), - ) - : 180), - ); - - // @ts-ignore: - return token(mc("lch.h")(baseColor, complimentaryHueAngle)); -} - export { achromatic, alpha, - complimentary, family, lightness, luminance, mc, - overtone, temp, - token, + token }; diff --git a/lib/wrappers.ts b/lib/wrappers.ts index a2f61de1..d9c2e02c 100644 --- a/lib/wrappers.ts +++ b/lib/wrappers.ts @@ -31,11 +31,11 @@ import { luminance as _lmnce, alpha as _opac, achromatic, - complimentary, + family, lightness, mc, - overtone, + temp, token, } from "./utils.ts"; @@ -533,21 +533,6 @@ class Color { return this.#setThis(hueshift, options); } - /** - * Returns the complementary hue of the bound color. The function returns `'gray'` when you pass in an achromatic color. - * @param colorObj Optional boolean whether to return a custom object with the color `name` and `hueFamily` as keys or just the result color. Default is `false`. - * @example - - * import { color } from "huetiful-js"; - * - - console.log(color("pink").getComplimentaryHue(true)) - // { hue: 'blue-green', color: '#97dfd7ff' } - - */ - complimentary(): ColorToken { - return this.#setThis(complimentary); - } /** * Gets the hue family which a color belongs to with the overtone included (if it has one.). @@ -561,7 +546,7 @@ class Color { console.log(color("#310000").family()) // 'red' */ - family(): BiasedHues & ColorFamily { + family(): BiasedHues & ColorFamily | { hue: BiasedHues & ColorFamily; bias: false | ColorFamily } { // @ts-ignore: return this.#setThis(family); @@ -789,27 +774,6 @@ class Color { return this.#setThis(deficiency, options); } - /** - * Returns the name of the hue family which is biasing the passed in color. - * - * * If an achromatic color is passed in it returns the string `'gray'` - * * If the color has no bias it returns `false`. - * - * @example - * - console.log(color("fefefe").overtone()) - // 'gray' - - console.log(color("cyan").overtone()) - // 'green' - - console.log(color("blue").overtone()) - // false - */ - overtone(): ColorFamily { - // @ts-ignore: - return this.#setThis(overtone); - } /** * diff --git a/package.json b/package.json index 1abb132c..cbdc5929 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,55 @@ { - "name": "@prjctimg/huetiful", - "version": "3.0.6", - "type": "module", - "main": "./build/huetiful.esm.js", - "browser": "./build/huetiful.min.js", - "jsdelivr": "./build/huetiful.min.js", - "types": "./build/index.d.ts", - "description": "TypeScript utility library for simple 🧮, fast ⏱️ and accessible ♿ color manipulation.", - "dependencies": { - "culori": "^4.0.1" - }, - "devDependencies": { - "@types/bun": "^1.1.9", - "@types/culori": "^2.1.0", - "bun": "^1.1.34", - "tsup": "^8.2.4", - "typescript": "^5.0.2" - }, - "files": [ - "build", - "CHANGELOG.md", - "readme.md", - "contributing.md", - "license" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/prjctimg/huetiful.git" - }, - "keywords": [ - "color", - "stats", - "culori", - "palettes", - "generator", - "utilities", - "manipulation", - "typescript" - ], - "author": "ディーン・タリサイ", - "email": "prjctimg@outlook.com", - "homepage": "https://huetiful-js.com", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/prjctimg/huetiful/issues" - }, - "directories": { - "lib": "lib", - "test": "tests" - } + "name": "@prjctimg/huetiful", + "version": "3.0.6", + "type": "module", + "main": "./build/huetiful.esm.js", + "browser": "./build/huetiful.min.js", + "jsdelivr": "./build/huetiful.min.js", + "types": "./build/index.d.ts", + "description": "TypeScript utility library for simple 🧮, fast ⏱️ and accessible ♿ color manipulation.", + "dependencies": { + "culori": "^4.0.1" + }, + "devDependencies": { + "@types/bun": "^1.1.9", + "@types/culori": "^2.1.0", + "bun": "^1.1.34", + "tsup": "^8.2.4", + "typescript": "^5.0.2" + }, + "files": [ + "build", + "CHANGELOG.md", + "readme.md", + "contributing.md", + "license" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/prjctimg/huetiful.git" + }, + "keywords": [ + "color", + "stats", + "culori", + "palettes", + "generator", + "utilities", + "manipulation", + "typescript" + ], + "author": "ディーン・タリサイ", + "email": "prjctimg@outlook.com", + "homepage": "https://huetiful-js.com", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/prjctimg/huetiful/issues" + }, + "directories": { + "lib": "lib", + "test": "tests" + }, + "trustedDependencies": [ + "@biomejs/biome" + ] } diff --git a/readme.md b/readme.md index f0ca0583..ad9aa50e 100644 --- a/readme.md +++ b/readme.md @@ -54,21 +54,19 @@ You can use this library when you want to do cool stuff with color and code 😎 > The library uses ES modules and has no default export. If you wish to have a UMD variant (for some reason known to self), you can build from source. See [BUILD.md]() for more info -```bash +```sh npm i huetiful-js -# Or if you have different taste... +# Or if you have different package manager... # pnpm add huetiful-js # yarn add huetiful-js # bun add huetiful-js ``` -### Deno +For Deno users, the library is available on JSR under a different alias: -For Deno users, the library is available on JSR under a different alias - -``` +```sh deno add jsr:@algorist/color ``` @@ -83,11 +81,7 @@ These examples are only compatible with version 3.x. Versions prior to that are A color can be defined using different types(arrays, strings, numbers, plain objects etc). Below are examples listing all the valid color tokens: -:::tip - -For more information about color tokens [see this page](/docs/guides/color) - -::: +[See more about the expected types of color tokens](https://huetiful-js/docs/guides/color) ```ts import { token } from "huetiful-js"; @@ -99,7 +93,7 @@ let colorObjectWithAlpha = { l: 50, c: 20, h: 40, alpha: 0.5, mode: "lch" }; let arrColor = ["rgb", 120, 80, 50]; let arrColorWithAlpha = ["rgb", 120, 80, 50, 0.1]; -var allColors = [ +let allColors = [ cssNamedColor, colorNumber, colorObject, @@ -109,9 +103,9 @@ var allColors = [ ]; let res = []; -for (const color of allColors) { +for (const color of allColors) res.push(token(color)); -} + console.log(res); diff --git a/test.ts b/test.ts index f7c961fc..68c683be 100644 --- a/test.ts +++ b/test.ts @@ -1,3 +1,29 @@ -import { token } from './lib/utils.ts' +// import { token } from './lib/utils.ts' -console.log(token({ l: 5, c: 45, h: 87, mode: 'lch' }, { kind: 'arr', omitAlpha: false })) \ No newline at end of file +import { diverging, sortBy, filterBy, stats, colors } from "./lib/index.ts"; +import { achromatic, family, mc, token } from "./lib/utils.ts"; + +// console.log(stats(colors('all', '600'), { +// factor: ['chroma', 'hue'], colorspace: 'lch'zzzzzzz +// })) + + +// console.info(filterBy(colors('all', '500'), { +// factor: ['chroma', 'hue'], ranges: { +// hue: ['<100'], chroma: [20] +// }, colorspace: 'lch' +// })) + + +//console.info(sortBy(['purple', 'blue'], { relative: true, factor: ['chroma', 'hue'] })) +// * fix this +// ! fix this +// ? fix this +// todo + +//console.log(mc('rgb.b')(['rgb', 0.4, 0.3, 0.1])) +// console.log(mc('lch.h')({ r: 0.2, g: 0.4, b: 0.5, mode: 'rgb' })) +// console.log(token({ r: 0.2, g: 0.4, b: 0.5, mode: 'rgb' }, { kind: 'obj', targetMode: 'lch' })) + + +console.log(family('#97dfd7ff')) \ No newline at end of file diff --git a/tests/collection.test.ts b/tests/collection.test.ts index ad222664..471047e2 100644 --- a/tests/collection.test.ts +++ b/tests/collection.test.ts @@ -1,31 +1,34 @@ import { sortBy, filterBy, stats } from "../lib/collection.ts"; -import runner, { type Spec } from "./runner"; +import { colors } from "../lib/palettes.ts"; +import runner, { type Spec } from "./runner.ts"; + +const samples = colors('all', '500') const tests: Spec[] = [ { description: "Filters collections of color.", callback: filterBy, - matcher: "arrayContaining", - params: [], - result: "", + + params: [samples, { ranges: { hue: ['>=190'], factor: ['hue'] } }], + }, { description: "Sorts collections of color.", callback: sortBy, - matcher: "arrayContaining", - params: [], - result: "", + + params: [samples, { factor: ['hue'], order: 'asc' }], + }, { description: "Gets the stats for a collection of color.", callback: stats, - matcher: "arrayContaining", - params: [], - result: "", + + params: [samples], + }, // { // description: "Distributes factors of a collection of color.", // callback: distribute, - // matcher: "arrayContaining", + // matcher: "toEqual", // params: [], // result: "", // }, diff --git a/tests/palettes.test.ts b/tests/palettes.test.ts new file mode 100644 index 00000000..824f626b --- /dev/null +++ b/tests/palettes.test.ts @@ -0,0 +1,23 @@ +import { colors, nearest, diverging } from "../lib/palettes.ts"; + +import runner, { type Spec } from "./runner.ts"; + + +const specs: Spec[] = [{ + description: 'Returns an array of diverging colors', + callback: diverging, + params: [['Spectral']], + +}, { + description: 'Returns an array of colors from Tailwind', + callback: colors, + params: ['red'], + +}, { + description: 'Gets the nearest colors in a collection as compared against a comparison color', + callback: nearest, + params: [colors('all', '600'), { num: 6, against: 'yellow' }], + +}] + +runner(specs) \ No newline at end of file diff --git a/tests/runner.ts b/tests/runner.ts index 22eae86b..23d98ab1 100644 --- a/tests/runner.ts +++ b/tests/runner.ts @@ -1,21 +1,19 @@ // @ts-nocheck: -import { expect, test } from "bun:test"; +import { expect } from "jsr:@std/expect"; export type Spec = { description?: string; - matcher?: string; callback: unknown; params: unknown[]; - result: unknown; }; export default function (specs: Spec[]) { for (const spec of specs) { - test(spec?.description || "Running test...", () => { - expect(spec.callback(...spec.params))[ - spec?.matcher || "toEqual" - ](spec.result); + console.info(`${spec?.description} \n`, spec?.callback(...spec?.params), `\n`) + Deno.test(spec?.description || "Running test...", () => { + expect(spec?.callback(...spec?.params)).toBeTruthy() }); } + } diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 00000000..84c63262 --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,145 @@ + +import { lightness, luminance, achromatic, alpha, family, mc, temp, token } from "../lib/utils.ts"; +import runner, { type Spec } from "./runner.ts"; + + + + +const str = '#ffc3b03b', arr = ['rgb', 0.4, 0.3, 0.1, 0.7], obj = { r: 0.2, g: 0.4, b: 0.5, mode: 'rgb' }, + fn_mc = (a: string, b: string, c: number | string) => mc(a)(b, c) +const specs: Spec[] = [{ + + // ? token() + description: "converts an object to a number", + callback: token, + params: [obj, { kind: 'num', }] +}, +{ + description: "converts a hex string to an array of channel values", + callback: token, + params: [str, { kind: 'arr', targetMode: 'lab' }] +}, +{ + description: "converts an array to a 6 character hex string (without the alpha channel). ", + callback: token, + params: [arr, { kind: 'str', omitAlpha: true }] +}, +{ + description: "converts an array to an object without alpha and mode properties", + callback: token, + params: [arr, { kind: 'obj', omitMode: true, omitAlpha: true }] +}, +{ + description: "converts an object to an array of channel values with no mode string.", + callback: token, + params: [obj, { kind: 'arr', omitMode: true }] +}, +{ + description: "converts an array to a number", + params: [arr, { kind: 'num' }], + callback: token +}, + + +// ? mc() +// * getting +{ + description: 'gets the channel value of a color object', + callback: fn_mc, + params: ['lch.c', obj] +}, { + description: 'gets the channel value of color array', + callback: fn_mc, params: ['lch.c', arr] +}, +{ + description: 'gets the channel value of color string', + callback: fn_mc, params: ['lch.c', str] +}, +// * setting + +{ + description: 'sets the channel value of a color object', + callback: fn_mc, + params: ['lch.h', obj, '*0.2'] +}, + + + +{ + description: 'sets the channel value of a color array', + callback: fn_mc, + params: ['lch.h', arr, 20] +}, + +// ? alpha() +// * setting +{ + description: 'sets the alpha channel value on a color object', callback: alpha, params: [obj, 0.05] +}, { + description: 'sets the alpha channel value on a color string', callback: alpha, params: [str, 0.05] +}, { + description: 'sets the alpha channel value on a color array', callback: alpha, params: [arr, 0.05] +}, + +// * getting +{ + description: 'gets the alpha channel value from a color object', callback: alpha, params: [obj] +}, { + description: 'gets the alpha channel value from a color array', + callback: alpha, params: [arr] +}, { + description: 'gets the alpha channel value from a color string', + callback: alpha, params: [str] +}, + + +// ? family() +{ + description: 'gets the hue family of a color token', + params: [str, true], + callback: family +}, + +// ? temp + +{ + description: 'gets the estimated color temperature of a color token', + params: [str], + callback: temp +}, + +// ? achromatic +{ + description: 'checks if a color is grayscale or not', + params: ['gray'], + callback: achromatic +}, + +// ? lightness +{ + description: 'brightens or darkens the color by scaling the lightness channel', + params: [obj, { amount: 0.3, darken: true }], + callback: lightness +}, +// ? luminance + +// * setting +{ + description: 'gets the luminance value of the passed in color token', + params: [obj], + callback: luminance +}, + +// * setting +// ! passing string values treturns black #000000 + +{ + description: 'sets the luminance value of the passed in color token', + params: [str, 0.5], + callback: luminance +} + +]; + + +runner(specs) \ No newline at end of file diff --git a/www/bun.lockb b/www/bun.lockb deleted file mode 100755 index 97fc622b..00000000 Binary files a/www/bun.lockb and /dev/null differ