From 690cffd4ad6ef6a2a3ee60e833321c75ca260fb8 Mon Sep 17 00:00:00 2001 From: Jony J <1844749591@qq.com> Date: Tue, 31 Dec 2024 16:16:34 +0800 Subject: [PATCH] feat: replace @ctrl/tinycolor with @ant-design/fast-color (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fast-color * chore: update @ant-design/fast-color dependency to version 2.0.6 --------- Co-authored-by: 鹤仙 --- bench/generate-old.ts | 164 ++++++++++++++++++++++++++++++++++++++++ bench/generate.bench.ts | 23 ++++++ package.json | 7 +- src/generate.ts | 115 +++++++++------------------- 4 files changed, 228 insertions(+), 81 deletions(-) create mode 100644 bench/generate-old.ts create mode 100644 bench/generate.bench.ts diff --git a/bench/generate-old.ts b/bench/generate-old.ts new file mode 100644 index 0000000..146242b --- /dev/null +++ b/bench/generate-old.ts @@ -0,0 +1,164 @@ +import { inputToRGB, rgbToHex, rgbToHsv } from '@ctrl/tinycolor'; + +const hueStep = 2; // 色相阶梯 +const saturationStep = 0.16; // 饱和度阶梯,浅色部分 +const saturationStep2 = 0.05; // 饱和度阶梯,深色部分 +const brightnessStep1 = 0.05; // 亮度阶梯,浅色部分 +const brightnessStep2 = 0.15; // 亮度阶梯,深色部分 +const lightColorCount = 5; // 浅色数量,主色上 +const darkColorCount = 4; // 深色数量,主色下 +// 暗色主题颜色映射关系表 +const darkColorMap = [ + { index: 7, opacity: 0.15 }, + { index: 6, opacity: 0.25 }, + { index: 5, opacity: 0.3 }, + { index: 5, opacity: 0.45 }, + { index: 5, opacity: 0.65 }, + { index: 5, opacity: 0.85 }, + { index: 4, opacity: 0.9 }, + { index: 3, opacity: 0.95 }, + { index: 2, opacity: 0.97 }, + { index: 1, opacity: 0.98 }, +]; + +interface HsvObject { + h: number; + s: number; + v: number; +} + +interface RgbObject { + r: number; + g: number; + b: number; +} + +// Wrapper function ported from TinyColor.prototype.toHsv +// Keep it here because of `hsv.h * 360` +function toHsv({ r, g, b }: RgbObject): HsvObject { + const hsv = rgbToHsv(r, g, b); + return { h: hsv.h * 360, s: hsv.s, v: hsv.v }; +} + +// Wrapper function ported from TinyColor.prototype.toHexString +// Keep it here because of the prefix `#` +function toHex({ r, g, b }: RgbObject): string { + return `#${rgbToHex(r, g, b, false)}`; +} + +// Wrapper function ported from TinyColor.prototype.mix, not treeshakable. +// Amount in range [0, 1] +// Assume color1 & color2 has no alpha, since the following src code did so. +function mix(rgb1: RgbObject, rgb2: RgbObject, amount: number): RgbObject { + const p = amount / 100; + const rgb = { + r: (rgb2.r - rgb1.r) * p + rgb1.r, + g: (rgb2.g - rgb1.g) * p + rgb1.g, + b: (rgb2.b - rgb1.b) * p + rgb1.b, + }; + return rgb; +} + +function getHue(hsv: HsvObject, i: number, light?: boolean): number { + let hue: number; + // 根据色相不同,色相转向不同 + if (Math.round(hsv.h) >= 60 && Math.round(hsv.h) <= 240) { + hue = light ? Math.round(hsv.h) - hueStep * i : Math.round(hsv.h) + hueStep * i; + } else { + hue = light ? Math.round(hsv.h) + hueStep * i : Math.round(hsv.h) - hueStep * i; + } + if (hue < 0) { + hue += 360; + } else if (hue >= 360) { + hue -= 360; + } + return hue; +} + +function getSaturation(hsv: HsvObject, i: number, light?: boolean): number { + // grey color don't change saturation + if (hsv.h === 0 && hsv.s === 0) { + return hsv.s; + } + let saturation: number; + if (light) { + saturation = hsv.s - saturationStep * i; + } else if (i === darkColorCount) { + saturation = hsv.s + saturationStep; + } else { + saturation = hsv.s + saturationStep2 * i; + } + // 边界值修正 + if (saturation > 1) { + saturation = 1; + } + // 第一格的 s 限制在 0.06-0.1 之间 + if (light && i === lightColorCount && saturation > 0.1) { + saturation = 0.1; + } + if (saturation < 0.06) { + saturation = 0.06; + } + return Number(saturation.toFixed(2)); +} + +function getValue(hsv: HsvObject, i: number, light?: boolean): number { + let value: number; + if (light) { + value = hsv.v + brightnessStep1 * i; + } else { + value = hsv.v - brightnessStep2 * i; + } + if (value > 1) { + value = 1; + } + return Number(value.toFixed(2)); +} + +interface Opts { + theme?: 'dark' | 'default'; + backgroundColor?: string; +} + +export default function generate(color: any, opts: Opts = {}): string[] { + const patterns: string[] = []; + const pColor = inputToRGB(color); + for (let i = lightColorCount; i > 0; i -= 1) { + const hsv = toHsv(pColor); + const colorString: string = toHex( + inputToRGB({ + h: getHue(hsv, i, true), + s: getSaturation(hsv, i, true), + v: getValue(hsv, i, true), + }), + ); + patterns.push(colorString); + } + patterns.push(toHex(pColor)); + for (let i = 1; i <= darkColorCount; i += 1) { + const hsv = toHsv(pColor); + const colorString: string = toHex( + inputToRGB({ + h: getHue(hsv, i), + s: getSaturation(hsv, i), + v: getValue(hsv, i), + }), + ); + patterns.push(colorString); + } + + // dark theme patterns + if (opts.theme === 'dark') { + return darkColorMap.map(({ index, opacity }) => { + const darkColorString: string = toHex( + mix( + inputToRGB(opts.backgroundColor || '#141414'), + inputToRGB(patterns[index]), + opacity * 100, + ), + ); + return darkColorString; + }); + } + return patterns; +} \ No newline at end of file diff --git a/bench/generate.bench.ts b/bench/generate.bench.ts new file mode 100644 index 0000000..af670a0 --- /dev/null +++ b/bench/generate.bench.ts @@ -0,0 +1,23 @@ +import { bench, describe } from 'vitest'; +import generate from '../src/generate'; +import generateOld from './generate-old'; + +describe('generate from hex string', () => { + bench('@ctrl/tinycolor', () => { + generateOld('#66ccff'); + }); + + bench('@ant-design/fast-color', () => { + generate('#66ccff'); + }); +}); + +describe('generate from rgb object', () => { + bench('@ctrl/tinycolor', () => { + generateOld({ r: 102, g: 204, b: 255 }); + }); + + bench('@ant-design/fast-color', () => { + generate({ r: 102, g: 204, b: 255 }); + }); +}); diff --git a/package.json b/package.json index 14d6122..cf11299 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "test": "jest" }, "dependencies": { - "@ctrl/tinycolor": "^3.6.1" + "@ant-design/fast-color": "^2.0.6" }, "devDependencies": { + "@ctrl/tinycolor": "^3.6.1", "@types/jest": "^26.0.24", "@types/node": "^20.14.9", "@umijs/fabric": "^3.0.0", @@ -42,8 +43,8 @@ "np": "^7.7.0", "prettier": "^2.8.8", "ts-jest": "^26.5.6", - "tsx": "^4.16.0", + "tsx": "^4.16.1", "typescript": "^4.9.5", "vitest": "^1.6.0" } -} +} \ No newline at end of file diff --git a/src/generate.ts b/src/generate.ts index bcb7a7a..0ea3d1c 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,4 +1,5 @@ -import { inputToRGB, rgbToHex, rgbToHsv } from '@ctrl/tinycolor'; +import type { ColorInput} from '@ant-design/fast-color'; +import { FastColor } from '@ant-design/fast-color'; const hueStep = 2; // 色相阶梯 const saturationStep = 0.16; // 饱和度阶梯,浅色部分 @@ -7,18 +8,19 @@ const brightnessStep1 = 0.05; // 亮度阶梯,浅色部分 const brightnessStep2 = 0.15; // 亮度阶梯,深色部分 const lightColorCount = 5; // 浅色数量,主色上 const darkColorCount = 4; // 深色数量,主色下 + // 暗色主题颜色映射关系表 const darkColorMap = [ - { index: 7, opacity: 0.15 }, - { index: 6, opacity: 0.25 }, - { index: 5, opacity: 0.3 }, - { index: 5, opacity: 0.45 }, - { index: 5, opacity: 0.65 }, - { index: 5, opacity: 0.85 }, - { index: 4, opacity: 0.9 }, - { index: 3, opacity: 0.95 }, - { index: 2, opacity: 0.97 }, - { index: 1, opacity: 0.98 }, + { index: 7, amount: 15 }, + { index: 6, amount: 25 }, + { index: 5, amount: 30 }, + { index: 5, amount: 45 }, + { index: 5, amount: 65 }, + { index: 5, amount: 85 }, + { index: 4, amount: 90 }, + { index: 3, amount: 95 }, + { index: 2, amount: 97 }, + { index: 1, amount: 98 }, ]; interface HsvObject { @@ -27,38 +29,6 @@ interface HsvObject { v: number; } -interface RgbObject { - r: number; - g: number; - b: number; -} - -// Wrapper function ported from TinyColor.prototype.toHsv -// Keep it here because of `hsv.h * 360` -function toHsv({ r, g, b }: RgbObject): HsvObject { - const hsv = rgbToHsv(r, g, b); - return { h: hsv.h * 360, s: hsv.s, v: hsv.v }; -} - -// Wrapper function ported from TinyColor.prototype.toHexString -// Keep it here because of the prefix `#` -function toHex({ r, g, b }: RgbObject): string { - return `#${rgbToHex(r, g, b, false)}`; -} - -// Wrapper function ported from TinyColor.prototype.mix, not treeshakable. -// Amount in range [0, 1] -// Assume color1 & color2 has no alpha, since the following src code did so. -function mix(rgb1: RgbObject, rgb2: RgbObject, amount: number): RgbObject { - const p = amount / 100; - const rgb = { - r: (rgb2.r - rgb1.r) * p + rgb1.r, - g: (rgb2.g - rgb1.g) * p + rgb1.g, - b: (rgb2.b - rgb1.b) * p + rgb1.b, - }; - return rgb; -} - function getHue(hsv: HsvObject, i: number, light?: boolean): number { let hue: number; // 根据色相不同,色相转向不同 @@ -99,7 +69,7 @@ function getSaturation(hsv: HsvObject, i: number, light?: boolean): number { if (saturation < 0.06) { saturation = 0.06; } - return Number(saturation.toFixed(2)); + return Math.round(saturation * 100) / 100; } function getValue(hsv: HsvObject, i: number, light?: boolean): number { @@ -112,7 +82,7 @@ function getValue(hsv: HsvObject, i: number, light?: boolean): number { if (value > 1) { value = 1; } - return Number(value.toFixed(2)); + return Math.round(value * 100) / 100; } interface Opts { @@ -120,45 +90,34 @@ interface Opts { backgroundColor?: string; } -export default function generate(color: string, opts: Opts = {}): string[] { - const patterns: string[] = []; - const pColor = inputToRGB(color); +export default function generate(color: ColorInput, opts: Opts = {}): string[] { + const patterns: FastColor[] = []; + const pColor = new FastColor(color); + const hsv = pColor.toHsv(); for (let i = lightColorCount; i > 0; i -= 1) { - const hsv = toHsv(pColor); - const colorString: string = toHex( - inputToRGB({ - h: getHue(hsv, i, true), - s: getSaturation(hsv, i, true), - v: getValue(hsv, i, true), - }), - ); - patterns.push(colorString); + const c = new FastColor({ + h: getHue(hsv, i, true), + s: getSaturation(hsv, i, true), + v: getValue(hsv, i, true), + }); + patterns.push(c); } - patterns.push(toHex(pColor)); + patterns.push(pColor); for (let i = 1; i <= darkColorCount; i += 1) { - const hsv = toHsv(pColor); - const colorString: string = toHex( - inputToRGB({ - h: getHue(hsv, i), - s: getSaturation(hsv, i), - v: getValue(hsv, i), - }), - ); - patterns.push(colorString); + const c = new FastColor({ + h: getHue(hsv, i), + s: getSaturation(hsv, i), + v: getValue(hsv, i), + }); + patterns.push(c); } // dark theme patterns if (opts.theme === 'dark') { - return darkColorMap.map(({ index, opacity }) => { - const darkColorString: string = toHex( - mix( - inputToRGB(opts.backgroundColor || '#141414'), - inputToRGB(patterns[index]), - opacity * 100, - ), - ); - return darkColorString; - }); + return darkColorMap.map(({ index, amount }) => + new FastColor(opts.backgroundColor || '#141414').mix(patterns[index], amount).toHexString(), + ); } - return patterns; + + return patterns.map((c) => c.toHexString()); }