Skip to content

Commit

Permalink
feat: replace @ctrl/tinycolor with @ant-design/fast-color (#94)
Browse files Browse the repository at this point in the history
* fast-color

* chore: update @ant-design/fast-color dependency to version 2.0.6

---------

Co-authored-by: 鹤仙 <[email protected]>
  • Loading branch information
aojunhao123 and guoyunhe authored Dec 31, 2024
1 parent 5eb4583 commit 690cffd
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 81 deletions.
164 changes: 164 additions & 0 deletions bench/generate-old.ts
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 23 additions & 0 deletions bench/generate.bench.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
}
115 changes: 37 additions & 78 deletions src/generate.ts
Original file line number Diff line number Diff line change
@@ -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; // 饱和度阶梯,浅色部分
Expand All @@ -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 {
Expand All @@ -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;
// 根据色相不同,色相转向不同
Expand Down Expand Up @@ -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 {
Expand All @@ -112,53 +82,42 @@ 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 {
theme?: 'dark' | 'default';
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());
}

0 comments on commit 690cffd

Please sign in to comment.