From 4369ab68c873250500fcc8f2648d5d715ac5a84e Mon Sep 17 00:00:00 2001 From: Mallik Cheripally Date: Mon, 10 Jun 2024 12:33:18 +0530 Subject: [PATCH] feat: Add function to calculate triadic colors This commit adds a function to calculate the triadic colors of a given color across all supported formats (hex, rgb, hsl, etc.). Accompanying unit tests are also included to validate the correctness of these calculations. The new function has been exported through the project's main entry point for use in creating color schemes. --- src/harmony/triadicColors.ts | 125 ++++++++++++++++++++++++++++ src/index.ts | 1 + tests/harmony/triadicColors.test.ts | 48 +++++++++++ 3 files changed, 174 insertions(+) create mode 100644 src/harmony/triadicColors.ts create mode 100644 tests/harmony/triadicColors.test.ts diff --git a/src/harmony/triadicColors.ts b/src/harmony/triadicColors.ts new file mode 100644 index 0000000..2045085 --- /dev/null +++ b/src/harmony/triadicColors.ts @@ -0,0 +1,125 @@ +import { detectColorFormat } from '@/parser/detectColorFormat'; +import { decomposeColor } from '@/parser/decomposeColor'; +import { rgbToHsl } from '@/conversions/rgbToHsl'; +import { hslToRgb } from '@/conversions/hslToRgb'; +import { hslaToRgba } from '@/conversions/hslaToRgba'; +import { hexToHsl } from '@/conversions/hexToHsl'; +import { hexAlphaToHsla } from '@/conversions/hexAlphaToHsla'; +import { labToRgb } from '@/conversions/labToRgb'; +import { lchToRgb } from '@/conversions/lchToRgb'; +import { parseColorToRgba } from '@/parser/parseColorToRgba'; +import { rebuildColorFromRgba } from '@/parser/rebuildColorFromRgba'; +import { ColorFormat, ColorFormats } from '@/utils/colorFormats'; +import {rgbToLab} from "@/conversions/rgbToLab"; +import {recomposeColor} from "@/parser/recomposeColor"; +import {rgbToLch} from "@/conversions/rgbToLch"; + +/** + * Finds the triadic colors of a given color. + * + * @param {string} color - The input color string in any supported format (hex, rgb, hsl, etc.). + * @returns {string[]} - The triadic colors in the original format. + * @throws {Error} - Throws an error if the input color format is invalid. + */ +export function triadicColors(color: string): string[] { + const format: ColorFormat = detectColorFormat(color); + const decomposed = decomposeColor(color); + + let triadicColors: { r: number; g: number; b: number; a?: number }[] = []; + + switch (format) { + case ColorFormats.HSL: { + const { h, s, l } = decomposed; + const h1 = (h + 120) % 360; + const h2 = (h + 240) % 360; + triadicColors = [hslToRgb(h1, s, l, false), hslToRgb(h2, s, l, false)]; + break; + } + + case ColorFormats.HSLA: { + const { h, s, l, a } = decomposed; + const h1 = (h + 120) % 360; + const h2 = (h + 240) % 360; + triadicColors = [hslaToRgba(h1, s, l, a, false), hslaToRgba(h2, s, l, a, false)]; + break; + } + + case ColorFormats.RGB: { + const hsl = rgbToHsl(decomposed.r, decomposed.g, decomposed.b, false); + const h1 = (hsl.h + 120) % 360; + const h2 = (hsl.h + 240) % 360; + return [ + hslToRgb(h1, hsl.s, hsl.l), + hslToRgb(h2, hsl.s, hsl.l), + ]; + } + + case ColorFormats.RGBA: { + const hsl = rgbToHsl(decomposed.r, decomposed.g, decomposed.b, false); + const h1 = (hsl.h + 120) % 360; + const h2 = (hsl.h + 240) % 360; + triadicColors = [ + // @ts-ignore + hslToRgb(h1, hsl.s, hsl.l, false), + // @ts-ignore + hslToRgb(h2, hsl.s, hsl.l, false), + ]; + triadicColors.forEach((color) => (color.a = decomposed.a)); + break; + } + case ColorFormats.HEX: { + const hsl = hexToHsl(color, false); + const h1 = (hsl.h + 120) % 360; + const h2 = (hsl.h + 240) % 360; + triadicColors = [ + // @ts-ignore + hslToRgb(h1, hsl.s, hsl.l, false), + // @ts-ignore + hslToRgb(h2, hsl.s, hsl.l, false), + ]; + break; + } + case ColorFormats.HEX_ALPHA: { + const hsla = hexAlphaToHsla(color, false); + const h1 = (hsla.h + 120) % 360; + const h2 = (hsla.h + 240) % 360; + triadicColors = [ + hslaToRgba(h1, hsla.s, hsla.l, hsla.a, false), + hslaToRgba(h2, hsla.s, hsla.l, hsla.a, false), + ]; + break; + } + case ColorFormats.LAB: { + const { l, a, b } = decomposed; + const triadicLabColors = [ + { l, a: -a, b: -b }, + { l, a: -a, b: -b + 120 }, + ]; + return triadicLabColors.map(lab => recomposeColor(color, lab)); + } + case ColorFormats.LCH: { + const { l, c, h } = decomposed; + const triadicLchColors = [ + { l, c, h: (h + 120) % 360 }, + { l, c, h: (h + 240) % 360 }, + ]; + return triadicLchColors.map(lch => recomposeColor(color, lch)); + } + case ColorFormats.NAMED: { + const rgba = parseColorToRgba(color); + const hsl = rgbToHsl(rgba.r, rgba.g, rgba.b, false); + const h1 = (hsl.h + 120) % 360; + const h2 = (hsl.h + 240) % 360; + triadicColors = [ + hslToRgb(h1, hsl.s, hsl.l, false), + hslToRgb(h2, hsl.s, hsl.l, false), + ]; + break; + } + default: + throw new Error(`Unsupported color format ${color} for triadic color calculation`); + } + + // @ts-ignore + return triadicColors.map((triadicColor) => rebuildColorFromRgba(color, triadicColor)); +} diff --git a/src/index.ts b/src/index.ts index e5a687b..1ba8a47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ export { xyzToRgb } from '@/conversions/xyzToRgb'; // Harmony export { complementaryColor } from '@/harmony/complementaryColor'; +export { triadicColors } from '@/harmony/triadicColors'; // Validations diff --git a/tests/harmony/triadicColors.test.ts b/tests/harmony/triadicColors.test.ts new file mode 100644 index 0000000..1da9ec9 --- /dev/null +++ b/tests/harmony/triadicColors.test.ts @@ -0,0 +1,48 @@ +import { triadicColors } from '@/harmony/triadicColors'; + +describe('triadicColors', () => { + test('triadic colors of rgb(255, 0, 0) are rgb(0, 255, 0) and rgb(0, 0, 255)', () => { + const result = triadicColors('rgb(255, 0, 0)'); + expect(result).toEqual(['rgb(0, 255, 0)', 'rgb(0, 0, 255)']); + }); + + test('triadic colors of rgba(255, 0, 0, 1) are rgba(0, 255, 0, 1) and rgba(0, 0, 255, 1)', () => { + const result = triadicColors('rgba(255, 0, 0, 1)'); + expect(result).toEqual(['rgba(0, 255, 0, 1)', 'rgba(0, 0, 255, 1)']); + }); + + test('triadic colors of hsl(0, 100%, 50%) are hsl(120, 100%, 50%) and hsl(240, 100%, 50%)', () => { + const result = triadicColors('hsl(0, 100%, 50%)'); + expect(result).toEqual(['hsl(120, 100%, 50%)', 'hsl(240, 100%, 50%)']); + }); + + test('triadic colors of hsla(0, 100%, 50%, 1) are hsla(120, 100%, 50%, 1) and hsla(240, 100%, 50%, 1)', () => { + const result = triadicColors('hsla(0, 100%, 50%, 1)'); + expect(result).toEqual(['hsla(120, 100%, 50%, 1)', 'hsla(240, 100%, 50%, 1)']); + }); + + test('triadic colors of #ff0000 are #00ff00 and #0000ff', () => { + const result = triadicColors('#ff0000'); + expect(result).toEqual(['#00ff00', '#0000ff']); + }); + + test('triadic colors of #ff0000ff are #00ff00ff and #0000ffff', () => { + const result = triadicColors('#ff0000ff'); + expect(result).toEqual(['#00ff00ff', '#0000ffff']); + }); + + test('triadic colors of lab(50 40 30) are lab(50 -40 -30) and lab(50 -40 90)', () => { + const result = triadicColors('lab(50 40 30)'); + expect(result).toEqual(['lab(50 -40 -30)', 'lab(50 -40 90)']); + }); + + test('triadic colors of lch(50 30 100) are lch(50 30 -20) and lch(50 30 220)', () => { + const result = triadicColors('lch(50 30 100)'); + expect(result).toEqual(['lch(50 30 220)', 'lch(50 30 340)']); + }); + + test('triadic colors of red (named color) are lime and blue', () => { + const result = triadicColors('red'); + expect(result).toEqual(['rgba(0, 255, 0, 1)', 'rgba(0, 0, 255, 1)']); + }); +});