From 92adfda48fcb3a14057012428d6aabfcdc3cbe52 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 12 Sep 2024 18:46:02 +0530 Subject: [PATCH] feat: blend colors --- README.md | 19 ++- ROADMAP.md | 2 +- index.ts | 3 + lib/blendRgb.ts | 25 ++++ lib/lightenDarkenTw.ts | 2 +- lib/setHslaOpacity.ts | 8 +- package.json | 2 +- tests/basic.test.ts | 310 ++++++++++++++++++++++++++++++++++++++++- utils/blendColors.ts | 49 +++++++ utils/convertColor.ts | 2 +- utils/setOpacity.ts | 9 +- 11 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 lib/blendRgb.ts create mode 100644 utils/blendColors.ts diff --git a/README.md b/README.md index 7fb168f..0cae8c1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ A lightweight JavaScript/TypeScript utility for seamless color manipulation and - **Color Conversion:** Convert colors between HEX, RGB, HSL, HSLA, RGBA and Tailwind CSS formats. - **Color Manipulation:** Lighten or darken a color by a specified percentage. - **Random Color Generation:** Generate random colors in HEX, RGB, HSL, or Tailwind CSS format. -- **Opacity Manipulation:** Set the opacity of color in any format. +- **Opacity Control:** Sets the opacity of color in any format. +- **Blend Colors:** Blends two colors in any format together in a specified ratio. More features coming soon! @@ -143,6 +144,22 @@ setOpacity("#ff5733", 0.5, "rgba"); // 'rgba(255, 88, 51, 0.5)' setOpacity("rgb(200, 100, 150)", 0.3, "hsla"); // hsla(330, 48%, 59%, 0.3) ``` +**blendColors** + +Blends two colors together to create a new color. + +```ts +blendColors(color1: string, color2: string, ratio: number): string +``` + +```ts +blendColors("#ff5733", "#333333", 0.5); // '#994533' +blendColors("rgb(255, 87, 51)", "hsl(101, 100%, 60%)", 0.2); // 'rgb(227, 121, 51)' +``` + +**_Note_**: It returns the color in the format of the first color provided.
+The prefix for Tailwind color will be taken from the first color. + --- ### 🛠️ Development diff --git a/ROADMAP.md b/ROADMAP.md index 8cf06ab..ccaf8bf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ ### v0.2.0 - [x] Add support for rgba and hsla -- [ ] Blend Colors +- [x] Blend Colors - [x] Adjust Opacity ### v0.3.0 diff --git a/index.ts b/index.ts index 4af0f6b..3216766 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import { lightenColor, darkenColor } from "./utils/lightenDarken"; import { randomColor } from "./utils/randomColor"; import { toTailwind } from "./utils/toTailwind"; import { setOpacity } from "./utils/setOpacity"; +import { blendColors } from "./utils/blendColors"; export { convertColor, @@ -11,6 +12,7 @@ export { randomColor, toTailwind, setOpacity, + blendColors, }; const PigmentTS = { @@ -20,6 +22,7 @@ const PigmentTS = { randomColor, toTailwind, setOpacity, + blendColors, }; export default PigmentTS; diff --git a/lib/blendRgb.ts b/lib/blendRgb.ts new file mode 100644 index 0000000..d066503 --- /dev/null +++ b/lib/blendRgb.ts @@ -0,0 +1,25 @@ +/** + * Mixes two rgb colors together + */ +export function blendRgb( + rgbColor: string, + rgbColor2: string, + ratio: number +): string { + if (rgbColor === rgbColor2) return rgbColor; + + const color1 = rgbColor.match(/\d+/g) as string[]; + const color2 = rgbColor2.match(/\d+/g) as string[]; + + const r = Math.round( + parseInt(color1[0]) + (parseInt(color2[0]) - parseInt(color1[0])) * ratio + ); + const g = Math.round( + parseInt(color1[1]) + (parseInt(color2[1]) - parseInt(color1[1])) * ratio + ); + const b = Math.round( + parseInt(color1[2]) + (parseInt(color2[2]) - parseInt(color1[2])) * ratio + ); + + return `rgb(${r}, ${g}, ${b})`; +} diff --git a/lib/lightenDarkenTw.ts b/lib/lightenDarkenTw.ts index abc9859..420fffb 100644 --- a/lib/lightenDarkenTw.ts +++ b/lib/lightenDarkenTw.ts @@ -77,7 +77,7 @@ function calcAmount( /** * Extracts the prefix from a tailwind color */ -function extractPrefix(twColor: string): string { +export function extractPrefix(twColor: string): string { for (const prefix of TAILWIND_PREFIXES) { if (twColor.startsWith(prefix)) { return prefix; diff --git a/lib/setHslaOpacity.ts b/lib/setHslaOpacity.ts index 8deb7bf..a9ea463 100644 --- a/lib/setHslaOpacity.ts +++ b/lib/setHslaOpacity.ts @@ -1,4 +1,10 @@ -export function setHslaOpacity(hslaColor: string, amount: number): string { +/** + * The setHslaOpacity function sets the opacity of an HSLA color + */ +export function setHslaOpacity( + hslaColor: string, + amount: number | string +): string { const alpha = hslaColor.split(", ")[3]; return hslaColor.replace(alpha, amount.toString() + ")"); } diff --git a/package.json b/package.json index afdc35a..469f80a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pigment-ts", - "version": "0.1.0", + "version": "0.2.0", "description": "🎨 A lightweight utility for color manipulation and conversion.", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/tests/basic.test.ts b/tests/basic.test.ts index ef9811c..1b5145a 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -4,7 +4,7 @@ import PigmentTS from "../index"; /** - * Contains test for converting colors. + * Contains tests for converting colors. */ describe("Color conversion", () => { const convertColor = PigmentTS.convertColor; @@ -1179,23 +1179,325 @@ describe("Changing opacity", () => { ]; it.each(setOpacity20)( - `should set opacity to 20% %s`, + `should set opacity to 20%`, ({ color, opacity, to, expected }) => { expect(setOpacity(color, opacity, to)).toBe(expected); } ); it.each(setOpacity50)( - `should set opacity to 50% %s`, + `should set opacity to 50%`, ({ color, opacity, to, expected }) => { expect(setOpacity(color, opacity, to)).toBe(expected); } ); it.each(setOpacity80)( - `should set opacity to 80% %s`, + `should set opacity to 80%`, ({ color, opacity, to, expected }) => { expect(setOpacity(color, opacity, to)).toBe(expected); } ); }); + +/** + * Contains tests for blending colors + */ +describe("Blend colors", () => { + const blendColors = PigmentTS.blendColors; + + const tests: { + color: string; + color2: string; + ratio: number; + expected: string; + }[] = [ + // hex + hex + { + color: "#FF0000", + color2: "#00FF00", + ratio: 0.5, + expected: "#808000", + }, + { + color: "#FF0000", + color2: "#00FF00", + ratio: 0.2, + expected: "#CC3300", + }, + // hex + rgb + { + color: "#FF0000", + color2: "rgb(0, 255, 0)", + ratio: 0.5, + expected: "#808000", + }, + { + color: "#FF0000", + color2: "rgb(0, 255, 0)", + ratio: 0.2, + expected: "#CC3300", + }, + // hex + hsl + { + color: "#FF0000", + color2: "hsl(120, 100%, 50%)", + ratio: 0.5, + expected: "#808000", + }, + { + color: "#FF0000", + color2: "hsl(120, 100%, 50%)", + ratio: 0.2, + expected: "#CC3300", + }, + // hex + rgba + { + color: "#FF0000", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.5, + expected: "#808000", + }, + { + color: "#FF0000", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.2, + expected: "#CC3300", + }, + // hex + hsla + { + color: "#FF0000", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.5, + expected: "#808000", + }, + { + color: "#FF0000", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.2, + expected: "#CC3300", + }, + // hex + tw + { + color: "#FF0000", + color2: "bg-green-500", + ratio: 0.5, + expected: "#91632F", + }, + { + color: "#FF0000", + color2: "bg-green-500", + ratio: 0.2, + expected: "#D32713", + }, + + // rgb + rgb + { + color: "rgb(255, 150, 0)", + color2: "rgb(0, 255, 0)", + ratio: 0.7, + expected: "rgb(77, 224, 0)", + }, + { + color: "rgb(250, 100, 100)", + color2: "rgb(0, 255, 0)", + ratio: 0.3, + expected: "rgb(175, 147, 70)", + }, + // rgb + hsl + { + color: "rgb(255, 150, 0)", + color2: "hsl(120, 100%, 50%)", + ratio: 0.7, + expected: "rgb(77, 224, 0)", + }, + { + color: "rgb(250, 100, 100)", + color2: "hsl(120, 100%, 50%)", + ratio: 0.3, + expected: "rgb(175, 147, 70)", + }, + // rgb + rgba + { + color: "rgb(255, 150, 0)", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.7, + expected: "rgb(77, 224, 0)", + }, + { + color: "rgb(250, 100, 100)", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.3, + expected: "rgb(175, 147, 70)", + }, + // rgb + hsla + { + color: "rgb(255, 150, 0)", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.7, + expected: "rgb(77, 224, 0)", + }, + { + color: "rgb(250, 100, 100)", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.3, + expected: "rgb(175, 147, 70)", + }, + // rgb + tw + { + color: "rgb(255, 150, 0)", + color2: "bg-green-500", + ratio: 0.7, + expected: "rgb(100, 183, 66)", + }, + { + color: "rgb(250, 100, 100)", + color2: "bg-green-500", + ratio: 0.3, + expected: "rgb(185, 129, 98)", + }, + // hsl + hsl + { + color: "hsl(0, 100%, 50%)", + color2: "hsl(120, 100%, 50%)", + ratio: 0.7, + expected: "hsl(94, 100%, 35%)", + }, + { + color: "hsl(120, 100%, 50%)", + color2: "hsl(0, 100%, 50%)", + ratio: 0.3, + expected: "hsl(94, 100%, 35%)", + }, + // hsl + rgba + { + color: "hsl(0, 100%, 50%)", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.7, + expected: "hsl(94, 100%, 35%)", + }, + { + color: "hsl(120, 100%, 50%)", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.3, + expected: "hsl(120, 100%, 50%)", + }, + // hsl + hsla + { + color: "hsl(0, 100%, 50%)", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.7, + expected: "hsl(94, 100%, 35%)", + }, + { + color: "hsl(120, 100%, 50%)", + color2: "hsla(0, 100%, 50%, 0.5)", + ratio: 0.3, + expected: "hsl(94, 100%, 35%)", + }, + // hsl + tw + { + color: "hsl(0, 100%, 50%)", + color2: "bg-green-500", + ratio: 0.7, + expected: "hsl(92, 35%, 40%)", + }, + { + color: "hsl(120, 100%, 50%)", + color2: "bg-green-500", + ratio: 0.3, + expected: "hsl(125, 92%, 49%)", + }, + // rgba + rgba + { + color: "rgba(255, 150, 0, 0.5)", + color2: "rgba(0, 255, 0, 0.5)", + ratio: 0.7, + expected: "rgba(79, 224, 0, 0.5)", + }, + { + color: "rgba(250, 100, 100, 0.2)", + color2: "rgba(0, 255, 0, 0.2)", + ratio: 0.3, + expected: "rgba(175, 147, 70, 0.2)", + }, + // rgba + hsla + { + color: "rgba(255, 150, 0, 0.5)", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.7, + expected: "rgba(79, 224, 0, 0.5)", + }, + { + color: "rgba(250, 100, 100, 0.2)", + color2: "hsla(120, 100%, 50%, 0.2)", + ratio: 0.3, + expected: "rgba(175, 147, 70, 0.2)", + }, + // rgba + tw + { + color: "rgba(255, 150, 0, 0.5)", + color2: "bg-green-500", + ratio: 0.7, + expected: "rgba(100, 184, 66, 0.5)", + }, + { + color: "rgba(250, 100, 100, 0.2)", + color2: "bg-green-500", + ratio: 0.3, + expected: "rgba(184, 127, 97, 0.2)", + }, + { + color: "hsla(120, 100%, 50%, 0.2)", + color2: "rgba(0, 255, 0, 0.2)", + ratio: 0.3, + expected: "hsla(120, 100%, 50%, 0.2)", + }, + // hsla + hsla + { + color: "hsla(0, 100%, 50%, 0.5)", + color2: "hsla(120, 100%, 50%, 0.5)", + ratio: 0.7, + expected: "hsla(94, 100%, 35%, 0.5)", + }, + { + color: "hsla(120, 100%, 50%, 0.2)", + color2: "hsla(120, 100%, 50%, 0.2)", + ratio: 0.3, + expected: "hsla(120, 100%, 50%, 0.2)", + }, + // hsla + tw + { + color: "hsla(0, 100%, 50%, 0.5)", + color2: "bg-green-500", + ratio: 0.7, + expected: "hsla(92, 35%, 40%, 0.5)", + }, + { + color: "hsla(120, 100%, 50%, 0.2)", + color2: "bg-green-500", + ratio: 0.3, + expected: "hsla(125, 92%, 49%, 0.2)", + }, + // tw + tw + { + color: "green-500", + color2: "bg-red-500", + ratio: 0.7, + expected: "[#B26A4B]", + }, + { + color: "bg-green-500", + color2: "bg-red-500", + ratio: 0.3, + expected: "bg-[#609E56]", + }, + ]; + + it.each(tests)( + "should blend two colors", + ({ color, color2, ratio, expected }) => { + expect(blendColors(color, color2, ratio)).toBe(expected); + } + ); +}); diff --git a/utils/blendColors.ts b/utils/blendColors.ts new file mode 100644 index 0000000..0bdc463 --- /dev/null +++ b/utils/blendColors.ts @@ -0,0 +1,49 @@ +import { blendRgb } from "../lib/blendRgb"; +import { detectFormat } from "../lib/detectFormat"; +import { hslaToRgba, rgbaToHsla } from "../lib/hslaAndRgba"; +import { extractPrefix } from "../lib/lightenDarkenTw"; +import { setHslaOpacity } from "../lib/setHslaOpacity"; +import { convertColor } from "./convertColor"; + +/** + * Mixes two colors together in any format + * @param color The first color to blend + * @param color2 The second color to blend + * @returns The blended color in first color's format + */ +export function blendColors( + color: string, + color2: string, + ratio: number +): string { + const format = detectFormat(color); + const format2 = detectFormat(color2); + + if (!format || !format2) throw new Error("Invalid color format"); + if (color === color2) return color; + + const hexColor = convertColor(color, "rgb") as string; + const hexColor2 = convertColor(color2, "rgb") as string; + + const blendedHexColor = blendRgb(hexColor, hexColor2, ratio); + + if (format === "hsla") { + const alpha = color.split(", ")[3]; + const hslaColor = convertColor(blendedHexColor, "hsla") as string; + return `${setHslaOpacity(hslaColor, alpha.replace(")", ""))}`; + } + if (format === "rgba") { + const alpha2 = color.split(", ")[3]; + const rgbaColor = convertColor(blendedHexColor, "rgba") as string; + const hslaColor = setHslaOpacity( + rgbaToHsla(rgbaColor), + alpha2.replace(")", "") + ); + + return hslaToRgba(hslaColor); + } + if (format === "tw") + return `${extractPrefix(color)}${convertColor(blendedHexColor, format)}`; + + return convertColor(blendedHexColor, format) as string; +} diff --git a/utils/convertColor.ts b/utils/convertColor.ts index 765129e..80681e2 100644 --- a/utils/convertColor.ts +++ b/utils/convertColor.ts @@ -30,7 +30,7 @@ export function convertColor( const colorFormat = detectFormat(color); if (!colorFormat) throw new Error("Invalid color format."); - if (colorFormat === to) return color.toUpperCase(); + if (colorFormat === to) return color; if (colorFormat === "hex" && to === "rgb") return hexToRgb(color); // hex to rgb if (colorFormat === "hex" && to === "hsl") return rgbToHsl(hexToRgb(color)); // hex to rgb to hsl diff --git a/utils/setOpacity.ts b/utils/setOpacity.ts index ccc9f8a..fcfbb48 100644 --- a/utils/setOpacity.ts +++ b/utils/setOpacity.ts @@ -5,6 +5,13 @@ import { rgbToHsl } from "../lib/rgbToHsl"; import { setHslaOpacity } from "../lib/setHslaOpacity"; import { convertColor } from "./convertColor"; +/** + * The setOpacity function sets the opacity of a color + * @param color The color to set the opacity of + * @param amount The amount of opacity to set + * @param to The format to convert the color to + * @returns The color with the opacity set + */ export function setOpacity( color: string, amount: number, @@ -12,8 +19,6 @@ export function setOpacity( ): string { const format = detectFormat(color); - console.log(format, color); - if (!format) throw new Error("Invalid color format"); if (amount < 0 || amount > 1) throw new Error("Invalid opacity amount");