diff --git a/bench/generate-old.ts b/bench/generate-old.ts new file mode 100644 index 0000000..88eb514 --- /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: string, 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..38aca67 --- /dev/null +++ b/bench/generate.bench.ts @@ -0,0 +1,13 @@ +import { bench, describe } from 'vitest' +import generate from '../src/generate'; +import generateOld from './generate-old'; + +describe('generate', () => { + bench('@ctrl/tinycolor', () => { + generateOld('#66ccff'); + }); + + bench('@ant-design/fast-color', () => { + generate('#66ccff'); + }); +}); diff --git a/package.json b/package.json index f8c7b2c..9f89789 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "test": "jest" }, "dependencies": { - "@ctrl/tinycolor": "^3.6.1" + "@ant-design/fast-color": "^1.1.0" }, "devDependencies": { + "@ctrl/tinycolor": "^3.6.1", "@types/jest": "^26.0.24", "@types/node": "^20.14.9", "@umijs/fabric": "^3.0.0", @@ -42,7 +43,7 @@ "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" } diff --git a/src/generate.ts b/src/generate.ts index bcb7a7a..7f682bd 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,4 +1,4 @@ -import { inputToRGB, rgbToHex, rgbToHsv } from '@ctrl/tinycolor'; +import { FastColor } from '@ant-design/fast-color'; const hueStep = 2; // 色相阶梯 const saturationStep = 0.16; // 饱和度阶梯,浅色部分 @@ -27,36 +27,15 @@ 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 mix(rgb1: FastColor, rgb2: FastColor, amount: number) { + return new FastColor({ + r: Math.round((rgb2.r - rgb1.r) * amount + rgb1.r), + g: Math.round((rgb2.g - rgb1.g) * amount + rgb1.g), + b: Math.round((rgb2.b - rgb1.b) * amount + rgb1.b), + }); } function getHue(hsv: HsvObject, i: number, light?: boolean): number { @@ -121,44 +100,37 @@ interface Opts { } export default function generate(color: string, opts: Opts = {}): string[] { - const patterns: string[] = []; - const pColor = inputToRGB(color); + 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, - ), - ); + const darkColorString: string = mix( + new FastColor(opts.backgroundColor || '#141414'), + patterns[index], + opacity, + ).toHexString(); return darkColorString; }); } - return patterns; + return patterns.map((c) => c.toHexString()); }