├── bun.lockb ├── src ├── index.ts ├── types.ts ├── presetColors.ts └── FastColor.ts ├── index.js ├── .fatherrc.js ├── .prettierrc ├── .github ├── workflows │ └── main.yml └── dependabot.yml ├── .vscode ├── extensions.json └── settings.json ├── now.json ├── .editorconfig ├── tests ├── index.test.ts ├── ctrl-tinycolor │ ├── rgbToHsv.test.ts │ └── TinyColor.test.ts ├── instance.test.ts ├── hsv.test.ts ├── hsl.test.ts ├── css-hsl-syntax.test.ts ├── css-rgb-syntax.test.ts └── preset.test.ts ├── .eslintrc.js ├── .gitignore ├── tsconfig.json ├── .dumirc.ts ├── bench ├── hexStringToHsvObject.bench.ts ├── hexStringToHslObject.bench.ts ├── hexStringToRgbObject.bench.ts ├── hslObjectToHexString.bench.ts ├── hsvObjectToHexString.bench.ts └── rgbObjectToHexString.bench.ts ├── HISTORY.md ├── LICENSE ├── package.json └── README.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/fast-color/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FastColor'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import Trigger from './src/'; 3 | export default Trigger; 4 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "jsxSingleQuote": false 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ✅ test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | uses: react-component/rc-test/.github/workflows/test.yml@main 6 | secrets: inherit 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "stylelint.vscode-stylelint", 5 | "esbenp.prettier-vscode", 6 | "editorconfig.editorconfig" 7 | ] 8 | } -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "rc-trigger", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": ".doc" } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | describe('index', () => { 4 | it('mix should round', () => { 5 | const source = new FastColor('rgba(255, 255, 255, 0.1128)'); 6 | const target = new FastColor('rgba(0, 0, 0, 0.93)'); 7 | 8 | const mixed = source.mix(target, 50); 9 | 10 | expect(mixed.toRgbString()).toBe('rgba(128,128,128,0.52)'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/ctrl-tinycolor/rgbToHsv.test.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor, rgbToHsv } from '@ctrl/tinycolor'; 2 | import type { HSV } from '../../src'; 3 | import { FastColor } from '../../src'; 4 | 5 | test('rgbToHsv alternative', () => { 6 | const r = 102, 7 | g = 204, 8 | b = 255; 9 | const hsv: HSV = new TinyColor(rgbToHsv(r, g, b)).toHsv(); 10 | hsv.h = Math.round(hsv.h * 360); 11 | hsv.a = 1; 12 | expect(new FastColor({ r, g, b }).toHsv()).toEqual(hsv); 13 | }); 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | rules: { 4 | 'default-case': 0, 5 | 'import/no-extraneous-dependencies': 0, 6 | 'react-hooks/exhaustive-deps': 0, 7 | 'react/no-find-dom-node': 0, 8 | 'react/no-did-update-set-state': 0, 9 | 'react/no-unused-state': 1, 10 | 'react/sort-comp': 0, 11 | 'jsx-a11y/label-has-for': 0, 12 | 'jsx-a11y/label-has-associated-control': 0, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | *.iml 3 | *.log 4 | .idea 5 | .ipr 6 | .iws 7 | *~ 8 | ~* 9 | *.diff 10 | *.patch 11 | *.bak 12 | .DS_Store 13 | Thumbs.db 14 | .project 15 | .*proj 16 | .svn 17 | *.swp 18 | *.swo 19 | *.pyc 20 | *.pyo 21 | node_modules 22 | .cache 23 | *.css 24 | build 25 | lib 26 | es 27 | coverage 28 | yarn.lock 29 | package-lock.json 30 | 31 | # dumi 32 | .umi 33 | .umi-production 34 | .umi-test 35 | .docs 36 | 37 | 38 | # dumi 39 | .dumi/tmp 40 | .dumi/tmp-production -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "declaration": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "paths": { 11 | "@/*": [ 12 | "src/*" 13 | ], 14 | "@@/*": [ 15 | ".dumi/tmp/*" 16 | ] 17 | }, 18 | }, 19 | "exclude": [ 20 | "./bench" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | mfsu: false, 5 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 6 | themeConfig: { 7 | name: 'Trigger', 8 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 9 | }, 10 | styles: [ 11 | ` 12 | .dumi-default-previewer-demo { 13 | position: relative; 14 | min-height: 300px; 15 | } 16 | `, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface RGB { 2 | r: number; 3 | g: number; 4 | b: number; 5 | a: number; 6 | } 7 | 8 | export interface HSL { 9 | h: number; 10 | s: number; 11 | l: number; 12 | a: number; 13 | } 14 | 15 | export interface HSV { 16 | h: number; 17 | s: number; 18 | v: number; 19 | a: number; 20 | } 21 | 22 | export type OptionalA = Omit & { a?: number }; 23 | 24 | export type ColorInput = 25 | | string 26 | | OptionalA 27 | | OptionalA 28 | | OptionalA; 29 | -------------------------------------------------------------------------------- /bench/hexStringToHsvObject.bench.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | import { bench } from 'vitest'; 3 | import { FastColor as FastColorES } from '../es'; 4 | import { FastColor } from '../src'; 5 | 6 | bench('@ctrl/tinycolor', () => { 7 | new TinyColor('#66ccff').toHsv(); 8 | }); 9 | 10 | bench.skip('color2k', () => { 11 | // not supported 12 | }); 13 | 14 | bench('@ant-design/fast-color src', () => { 15 | new FastColor('#66ccff').toHsv(); 16 | }); 17 | 18 | bench('@ant-design/fast-color es', () => { 19 | new FastColorES('#66ccff').toHsv(); 20 | }); 21 | -------------------------------------------------------------------------------- /bench/hexStringToHslObject.bench.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | import { parseToHsla } from 'color2k'; 3 | import { bench } from 'vitest'; 4 | import { FastColor as FastColorES } from '../es'; 5 | import { FastColor } from '../src'; 6 | 7 | bench('@ctrl/tinycolor', () => { 8 | new TinyColor('#66ccff').toHsl(); 9 | }); 10 | 11 | bench('color2k', () => { 12 | parseToHsla('#66ccff'); 13 | }); 14 | 15 | bench('@ant-design/fast-color src', () => { 16 | new FastColor('#66ccff').toHsl(); 17 | }); 18 | 19 | bench('@ant-design/fast-color es', () => { 20 | new FastColorES('#66ccff').toHsl(); 21 | }); 22 | -------------------------------------------------------------------------------- /bench/hexStringToRgbObject.bench.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | import { parseToRgba } from 'color2k'; 3 | import { bench } from 'vitest'; 4 | import { FastColor as FastColorES } from '../es'; 5 | import { FastColor } from '../src'; 6 | 7 | bench('@ctrl/tinycolor', () => { 8 | new TinyColor('#66ccff').toRgb(); 9 | }); 10 | 11 | bench('color2k', () => { 12 | parseToRgba('#66ccff'); 13 | }); 14 | 15 | bench('@ant-design/fast-color src', () => { 16 | new FastColor('#66ccff').toRgb(); 17 | }); 18 | 19 | bench('@ant-design/fast-color es', () => { 20 | new FastColorES('#66ccff').toRgb(); 21 | }); 22 | -------------------------------------------------------------------------------- /bench/hslObjectToHexString.bench.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | import { bench } from 'vitest'; 3 | import { FastColor as FastColorES } from '../es'; 4 | import { FastColor } from '../src'; 5 | 6 | bench('@ctrl/tinycolor', () => { 7 | new TinyColor({ h: 270, s: 0.6, l: 0.5 }).toHexString(); 8 | }); 9 | 10 | bench.skip('color2k', () => { 11 | // not supported 12 | }); 13 | 14 | bench('@ant-design/fast-color src', () => { 15 | new FastColor({ h: 270, s: 0.6, l: 0.5 }).toHexString(); 16 | }); 17 | 18 | bench('@ant-design/fast-color es', () => { 19 | new FastColorES({ h: 270, s: 0.6, l: 0.5 }).toHexString(); 20 | }); 21 | -------------------------------------------------------------------------------- /bench/hsvObjectToHexString.bench.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | import { bench } from 'vitest'; 3 | import { FastColor as FastColorES } from '../es'; 4 | import { FastColor } from '../src'; 5 | 6 | bench('@ctrl/tinycolor', () => { 7 | new TinyColor({ h: 270, s: 0.6, v: 0.5 }).toHexString(); 8 | }); 9 | 10 | bench.skip('color2k', () => { 11 | // not supported 12 | }); 13 | 14 | bench('@ant-design/fast-color src', () => { 15 | new FastColor({ h: 270, s: 0.6, v: 0.5 }).toHexString(); 16 | }); 17 | 18 | bench('@ant-design/fast-color es', () => { 19 | new FastColorES({ h: 270, s: 0.6, v: 0.5 }).toHexString(); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: np 11 | versions: 12 | - 7.2.0 13 | - 7.3.0 14 | - 7.4.0 15 | - dependency-name: "@types/react-dom" 16 | versions: 17 | - 17.0.0 18 | - 17.0.1 19 | - 17.0.2 20 | - dependency-name: "@types/react" 21 | versions: 22 | - 17.0.0 23 | - 17.0.1 24 | - 17.0.2 25 | - 17.0.3 26 | - dependency-name: typescript 27 | versions: 28 | - 4.1.3 29 | - 4.1.4 30 | - 4.1.5 31 | -------------------------------------------------------------------------------- /bench/rgbObjectToHexString.bench.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | import { rgba, toHex } from 'color2k'; 3 | import { bench } from 'vitest'; 4 | import { FastColor as FastColorES } from '../es'; 5 | import { FastColor } from '../src'; 6 | 7 | bench('@ctrl/tinycolor', () => { 8 | new TinyColor({ r: 11, g: 22, b: 33 }).toHexString(); 9 | }); 10 | 11 | bench('color2k', () => { 12 | toHex(rgba(11, 22, 33, 1)); 13 | }); 14 | 15 | bench('@ant-design/fast-color src', () => { 16 | new FastColor({ r: 11, g: 22, b: 33 }).toHexString(); 17 | }); 18 | 19 | bench('@ant-design/fast-color es', () => { 20 | new FastColorES({ r: 11, g: 22, b: 33 }).toHexString(); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/instance.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | describe('instance', () => { 4 | describe('clone', () => { 5 | it('clone should return same construct', () => { 6 | const base = new FastColor('#1677ff'); 7 | const turn = base.clone(); 8 | 9 | expect(turn instanceof FastColor); 10 | }); 11 | 12 | it('extends should be same', () => { 13 | class Little extends FastColor { 14 | constructor() { 15 | super('#1677ff'); 16 | } 17 | 18 | bamboo() { 19 | return 'beauty'; 20 | } 21 | } 22 | 23 | const base = new Little(); 24 | const turn = base.clone(); 25 | 26 | expect(turn instanceof Little); 27 | expect(turn.bamboo()).toBe('beauty'); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 4.1.0 / 2020-05-08 5 | 6 | - upgrade rc-animate to `3.x` 7 | 8 | ## 2.5.0 / 2018-06-05 9 | 10 | - support `alignPoint` 11 | 12 | ## 2.1.0 / 2017-10-16 13 | 14 | - add action `contextMenu` 15 | 16 | ## 2.0.0 / 2017-09-25 17 | 18 | - support React 16 19 | 20 | ## 1.11.0 / 2017-06-07 21 | 22 | - add es 23 | 24 | ## 1.9.0 / 2017-02-27 25 | 26 | - add getDocument prop 27 | 28 | ## 1.8.2 / 2017-02-24 29 | 30 | - change default container to absolute to fix scrollbar change problem 31 | 32 | ## 1.7.0 / 2016-07-18 33 | 34 | - use getContainerRenderMixin from 'rc-util' 35 | 36 | ## 1.6.0 / 2016-05-26 37 | 38 | - support popup as function 39 | 40 | ## 1.5.0 / 2016-05-26 41 | 42 | - add forcePopupAlign method 43 | 44 | ## 1.4.0 / 2016-04-06 45 | 46 | - support onPopupAlign 47 | 48 | ## 1.3.0 / 2016-03-25 49 | 50 | - support mask/maskTransitionName/zIndex 51 | 52 | ## 1.2.0 / 2016-03-01 53 | 54 | - add showAction/hideAction 55 | 56 | ## 1.1.0 / 2016-01-06 57 | 58 | - add root trigger node as parameter of getPopupContainer 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"], 3 | "stylelint.validate": ["acss", "css", "less", "sass", "scss"], 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.fixAll.stylelint": "explicit", 7 | "source.fixAll.markdownlint": "explicit", 8 | "source.organizeImports": "explicit" 9 | }, 10 | "editor.formatOnSave": true, 11 | "[javascript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[javascriptreact]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[typescript]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[typescriptreact]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[vue]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode" 25 | }, 26 | "[css]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | }, 29 | "[less]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | }, 32 | "[scss]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "[html]": { 36 | "editor.defaultFormatter": "esbenp.prettier-vscode" 37 | }, 38 | "[json]": { 39 | "editor.defaultFormatter": "esbenp.prettier-vscode" 40 | }, 41 | "[jsonc]": { 42 | "editor.defaultFormatter": "esbenp.prettier-vscode" 43 | }, 44 | "editor.defaultFormatter": "esbenp.prettier-vscode", 45 | "typescript.tsdk": "node_modules/typescript/lib" 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design/fast-color", 3 | "version": "3.0.0", 4 | "description": "fast and small color class", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-trigger", 9 | "trigger" 10 | ], 11 | "homepage": "https://github.com/ant-design/fast-color", 12 | "bugs": { 13 | "url": "https://github.com/ant-design/fast-color/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/ant-design/fast-color.git" 18 | }, 19 | "license": "MIT", 20 | "author": { 21 | "name": "Guo Yunhe", 22 | "email": "i@guoyunhe.me", 23 | "url": "https://guoyunhe.me/" 24 | }, 25 | "main": "./lib/index", 26 | "module": "./es/index", 27 | "files": [ 28 | "es", 29 | "lib" 30 | ], 31 | "scripts": { 32 | "bench": "vitest bench", 33 | "build": "dumi build", 34 | "compile": "father build", 35 | "coverage": "rc-test --coverage", 36 | "lint": "eslint src/ tests/ --ext .tsx,.ts,.jsx,.js", 37 | "now-build": "npm run build", 38 | "prepublishOnly": "npm run compile && rc-np", 39 | "start": "dumi dev", 40 | "test": "rc-test" 41 | }, 42 | "dependencies": {}, 43 | "devDependencies": { 44 | "@ctrl/tinycolor": "^4.1.0", 45 | "@rc-component/father-plugin": "^2.0.4", 46 | "@rc-component/np": "^1.0.0", 47 | "@types/jest": "^29.5.12", 48 | "@types/node": "^22.1.0", 49 | "@umijs/fabric": "^4.0.1", 50 | "color2k": "^2.0.3", 51 | "cross-env": "^7.0.3", 52 | "dumi": "^2.3.8", 53 | "eslint": "^8.57.0", 54 | "father": "^4.4.4", 55 | "rc-test": "^7.0.15", 56 | "typescript": "^5.4.5", 57 | "vitest": "^1.6.0" 58 | }, 59 | "engines": { 60 | "node": ">=8.x" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/hsv.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | describe('hsv', () => { 4 | it('hsv object to hex', () => { 5 | expect(new FastColor({ h: 270, s: 0.6, v: 0.4 }).toHexString()).toBe( 6 | '#472966', 7 | ); 8 | }); 9 | 10 | it('hsv string to hex', () => { 11 | expect(new FastColor('hsv(270, 60%, 40%)').toHexString()).toBe('#472966'); 12 | }); 13 | 14 | it('hsb string to hex', () => { 15 | expect(new FastColor('hsb(270 60 40)').toHexString()).toBe('#472966'); 16 | }); 17 | 18 | it('hex to hsv object', () => { 19 | expect(new FastColor('#472966').toHsv()).toEqual({ 20 | h: 270, 21 | s: 0.5980392156862745, 22 | v: 0.4, 23 | a: 1, 24 | }); 25 | }); 26 | 27 | it('should be same RGB', () => { 28 | const base = new FastColor({ 29 | h: 180, 30 | s: 0, 31 | v: 0, 32 | }); 33 | 34 | const turn = base.setHue(0); 35 | 36 | expect(base.toHexString()).toEqual(turn.toHexString()); 37 | }); 38 | 39 | it('clone should be same hsv', () => { 40 | const base = new FastColor({ 41 | h: 180, 42 | s: 0, 43 | v: 0, 44 | }); 45 | 46 | const turn = base.clone(); 47 | 48 | expect(base.toHsv()).toEqual(turn.toHsv()); 49 | }); 50 | 51 | it('setHue should be stable', () => { 52 | const base = new FastColor('#1677ff'); 53 | expect(base.getValue()).toBe(1); 54 | 55 | const turn = base.setHue(233); 56 | expect(turn.getValue()).toBe(1); 57 | }); 58 | 59 | it('normalizes H outside range for HSV (object input)', () => { 60 | // -60 -> 300 (magenta) 61 | expect(new FastColor({ h: -60, s: 1, v: 1 }).toHexString()).toBe('#ff00ff'); 62 | // 420 -> 60 (yellow) 63 | expect(new FastColor({ h: 420, s: 1, v: 1 }).toHexString()).toBe('#ffff00'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/hsl.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | describe('hsl', () => { 4 | // Unified hex<->hsl fixtures 5 | const hexHslFixtures: { hex: string; hsl: { h: number; s: number; l: number } }[] = [ 6 | // Boundaries 7 | { hex: '#000000', hsl: { h: 0, s: 0, l: 0 } }, 8 | { hex: '#ffffff', hsl: { h: 0, s: 0, l: 1 } }, 9 | 10 | // Primaries 11 | { hex: '#ff0000', hsl: { h: 0, s: 1, l: 0.5 } }, 12 | { hex: '#00ff00', hsl: { h: 120, s: 1, l: 0.5 } }, 13 | { hex: '#0000ff', hsl: { h: 240, s: 1, l: 0.5 } }, 14 | 15 | // Secondaries 16 | { hex: '#ffff00', hsl: { h: 60, s: 1, l: 0.5 } }, 17 | { hex: '#00ffff', hsl: { h: 180, s: 1, l: 0.5 } }, 18 | { hex: '#ff00ff', hsl: { h: 300, s: 1, l: 0.5 } }, 19 | 20 | // Specific samples 21 | { hex: '#2400c2', hsl: { h: 251, s: 1, l: 0.3804 } }, 22 | { hex: '#3d5dff', hsl: { h: 230, s: 1, l: 0.6196 } }, 23 | ]; 24 | 25 | // hex -> hsl object values and roundtrip 26 | hexHslFixtures.forEach(({ hex, hsl: expected }) => { 27 | it(`hex to hsl object values: ${hex}`, () => { 28 | const hsl = new FastColor(hex).toHsl(); 29 | expect(hsl.h).toBe(expected.h); 30 | expect(hsl.s).toBe(expected.s); 31 | expect(hsl.l).toBeCloseTo(expected.l, 4); 32 | expect(hsl.a).toBe(1); 33 | 34 | const back = new FastColor(hsl).toHexString(); 35 | expect(back).toBe(hex); 36 | }); 37 | }); 38 | 39 | it('setHue should not change lightness', () => { 40 | const base = new FastColor('#1677ff'); 41 | expect(base.getLightness()).toBeCloseTo(new FastColor('#1677ff').getLightness(), 4); 42 | 43 | const turn = base.setHue(233); 44 | expect(turn.getLightness()).toBeCloseTo(base.getLightness(), 4); 45 | }); 46 | 47 | const hslaAlphaCases: [string, string, number][] = [ 48 | ['hsla(251, 100%, 38%, 0.5)', 'hsla(251,100%,38%,0.5)', 0.5], 49 | ['hsla(120, 25%, 33%, 0.7)', 'hsla(120,25%,33%,0.7)', 0.7], 50 | ]; 51 | 52 | hslaAlphaCases.forEach(([str, normalized, alpha]) => { 53 | it(`supports hsla alpha: ${str}`, () => { 54 | expect(new FastColor(str).toHslString()).toBe(normalized); 55 | expect(new FastColor(str).toRgb().a).toBe(alpha); 56 | }); 57 | }); 58 | 59 | it('hue 0 and 360 are equivalent', () => { 60 | const c0 = new FastColor('hsl(0, 100%, 50%)'); 61 | const c360 = new FastColor('hsl(360, 100%, 50%)'); 62 | expect(c0.toHexString()).toBe(c360.toHexString()); 63 | }); 64 | 65 | it('s=0 yields grayscale regardless of hue', () => { 66 | const a = new FastColor('hsl(0, 0%, 40%)'); 67 | const b = new FastColor('hsl(200, 0%, 40%)'); 68 | expect(a.toHexString()).toBe(b.toHexString()); 69 | }); 70 | 71 | it('roundtrip toHslString keeps values', () => { 72 | const c = new FastColor('hsla(120, 25%, 33%, 0.7)'); 73 | expect(c.toHslString()).toBe('hsla(120,25%,33%,0.7)'); 74 | const parsed = new FastColor(c.toHslString()); 75 | expect(parsed.toHslString()).toBe('hsla(120,25%,33%,0.7)'); 76 | }); 77 | 78 | it('darken and lighten adjust lightness bounds', () => { 79 | const c = new FastColor('hsl(200, 50%, 50%)'); 80 | const darker = c.darken(20); 81 | const lighter = c.lighten(20); 82 | expect(darker.getLightness()).toBeCloseTo(c.getLightness() - 0.2, 4); 83 | expect(lighter.getLightness()).toBeCloseTo(c.getLightness() + 0.2, 4); 84 | 85 | const minCap = c.darken(100); 86 | const maxCap = c.lighten(100); 87 | expect(minCap.getLightness()).toBe(0); 88 | expect(maxCap.getLightness()).toBe(1); 89 | }); 90 | 91 | it('normalizes H outside range for HSL (object input)', () => { 92 | // -60 -> 300 (magenta) 93 | expect(new FastColor({ h: -60, s: 1, l: 0.5 }).toHexString()).toBe('#ff00ff'); 94 | // 420 -> 60 (yellow) 95 | expect(new FastColor({ h: 420, s: 1, l: 0.5 }).toHexString()).toBe('#ffff00'); 96 | }); 97 | }); 98 | 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ant-design/fast-color 2 | 3 | Fast color class. 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![npm download][download-image]][download-url] 7 | [![build status][github-actions-image]][github-actions-url] 8 | [![Test coverage][codecov-image]][codecov-url] 9 | [![bundle size][bundlephobia-image]][bundlephobia-url] 10 | [![dumi][dumi-image]][dumi-url] 11 | 12 | [npm-image]: http://img.shields.io/npm/v/@ant-design/fast-color.svg?style=flat-square 13 | [npm-url]: http://npmjs.org/package/@ant-design/fast-color 14 | [github-actions-image]: https://github.com/ant-design/fast-color/workflows/CI/badge.svg 15 | [github-actions-url]: https://github.com/ant-design/fast-color/actions 16 | [codecov-image]: https://img.shields.io/codecov/c/github/ant-design/fast-color/main.svg?style=flat-square 17 | [codecov-url]: https://codecov.io/gh/ant-design/fast-color/branch/main 18 | [david-url]: https://david-dm.org/ant-design/fast-color 19 | [david-image]: https://david-dm.org/ant-design/fast-color/status.svg?style=flat-square 20 | [david-dev-url]: https://david-dm.org/ant-design/fast-color?type=dev 21 | [david-dev-image]: https://david-dm.org/ant-design/fast-color/dev-status.svg?style=flat-square 22 | [download-image]: https://img.shields.io/npm/dm/@ant-design/fast-color.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/@ant-design/fast-color 24 | [bundlephobia-url]: https://bundlephobia.com/result?p=@ant-design/fast-color 25 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@ant-design/fast-color 26 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square 27 | [dumi-url]: https://github.com/umijs/dumi 28 | 29 | ## Install 30 | 31 | [![@ant-design/fast-color](https://nodei.co/npm/@ant-design/fast-color.png)](https://npmjs.org/package/@ant-design/fast-color) 32 | 33 | ## Usage 34 | 35 | ```js 36 | import { FastColor } from '@ant-design/fast-color'; 37 | 38 | // input 39 | new FastColor('#666'); // short hex 40 | new FastColor('#66ccff'); // hex 41 | new FastColor('#66ccffaa'); // hex with alpha 42 | new FastColor('rgba(102, 204, 255, .5)'); // old css rgb syntax 43 | new FastColor('rgb(102 204 255 / .5)'); // new css rgb syntax 44 | new FastColor('hsl(270, 60, 40, .5)'); // old css hsl syntax, with or without unit 45 | new FastColor('hsl(270deg 60% 40% / 50%)'); // new css hsl syntax, with or without unit 46 | new FastColor({ r: 102, g: 204, b: 255, a: 0.5 }); // rgb object 47 | new FastColor({ h: 270, s: 0.6, l: 0.4, a: 0.5 }); // hsl object 48 | new FastColor({ h: 270, s: 0.6, v: 0.4, a: 0.5 }); // hsv object 49 | 50 | // clone 51 | const color = new FastColor('#66ccff'); 52 | const color2 = new FastColor(color); // clone via constructor 53 | const color3 = color2.clone(); // call clone method 54 | 55 | // output 56 | color.toHexString(); // #66ccff 57 | color.toRgb(); // Object { r: 102, g: 204, b: 255, a: 1 } 58 | color.toRgbString(); // rgb(102,204,255) 59 | color.toHsl(); // Object { h: 200, s: 0.6, l: 0.7, a: 1 } 60 | color.toHslString(); // hsl(200,60%,70%) 61 | color.toHsv(); // Object { h: 200, s: 0.6, v: 1, a: 1 } 62 | ``` 63 | 64 | ## Compatibility 65 | 66 | | 浏览器 | IE / Edge | Firefox | Chrome | Safari | Electron | 67 | | --- | :---: | :---: | :---: | :---: | :---: | 68 | | 支持版本 | ![](https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png)
IE11, Edge | ![](https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)
最近2个版本 | ![](https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)
最近2个版本 | ![](https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png)
最近2个版本 | ![](https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png)
最近2个版本 | 69 | 70 | ## API 71 | 72 | TODO 73 | 74 | ## License 75 | 76 | @ant-design/fast-color is released under the MIT license. 77 | -------------------------------------------------------------------------------- /tests/css-hsl-syntax.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | // Support CSS hsl() and hsla() syntax with absolute values 4 | // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl 5 | describe('css hsl() syntax', () => { 6 | describe('new space-separated syntax', () => { 7 | it('without units', () => { 8 | expect(new FastColor('hsl(270 60 40)').toRgb()).toEqual({ 9 | r: 102, 10 | g: 41, 11 | b: 163, 12 | a: 1, 13 | }); 14 | 15 | expect(new FastColor('hsl(270 60 40 / 0.2)').toRgb()).toEqual({ 16 | r: 102, 17 | g: 41, 18 | b: 163, 19 | a: 0.2, 20 | }); 21 | 22 | expect(new FastColor('hsl(270 60 40 / .2)').toRgb()).toEqual({ 23 | r: 102, 24 | g: 41, 25 | b: 163, 26 | a: 0.2, 27 | }); 28 | 29 | expect(new FastColor('hsl(270 60 40 / 0.233)').toRgb()).toEqual({ 30 | r: 102, 31 | g: 41, 32 | b: 163, 33 | a: 0.233, 34 | }); 35 | 36 | expect(new FastColor('hsl(270 60 40 / .233)').toRgb()).toEqual({ 37 | r: 102, 38 | g: 41, 39 | b: 163, 40 | a: 0.233, 41 | }); 42 | }); 43 | 44 | it('with units', () => { 45 | expect(new FastColor('hsl(270 60 40)').toRgb()).toEqual({ 46 | r: 102, 47 | g: 41, 48 | b: 163, 49 | a: 1, 50 | }); 51 | 52 | expect(new FastColor('hsl(270 60 40)').toRgb()).toEqual({ 53 | r: 102, 54 | g: 41, 55 | b: 163, 56 | a: 1, 57 | }); 58 | 59 | expect(new FastColor('hsl(270 60 40 / 20%)').toRgb()).toEqual({ 60 | r: 102, 61 | g: 41, 62 | b: 163, 63 | a: 0.2, 64 | }); 65 | 66 | expect(new FastColor('hsl(270 60 40 / 23.3%)').toRgb()).toEqual({ 67 | r: 102, 68 | g: 41, 69 | b: 163, 70 | a: 0.233, 71 | }); 72 | }); 73 | }); 74 | 75 | describe('old comma-separated syntax', () => { 76 | it('without units', () => { 77 | expect(new FastColor('hsl(270, 60, 40)').toRgb()).toEqual({ 78 | r: 102, 79 | g: 41, 80 | b: 163, 81 | a: 1, 82 | }); 83 | 84 | expect(new FastColor('hsla(270, 60, 40, 0.2)').toRgb()).toEqual({ 85 | r: 102, 86 | g: 41, 87 | b: 163, 88 | a: 0.2, 89 | }); 90 | 91 | expect(new FastColor('hsla(270, 60, 40, .2)').toRgb()).toEqual({ 92 | r: 102, 93 | g: 41, 94 | b: 163, 95 | a: 0.2, 96 | }); 97 | 98 | expect(new FastColor('hsla(270, 60, 40, 0.233)').toRgb()).toEqual({ 99 | r: 102, 100 | g: 41, 101 | b: 163, 102 | a: 0.233, 103 | }); 104 | 105 | expect(new FastColor('hsla(270, 60, 40, .233)').toRgb()).toEqual({ 106 | r: 102, 107 | g: 41, 108 | b: 163, 109 | a: 0.233, 110 | }); 111 | }); 112 | 113 | it('with units', () => { 114 | expect(new FastColor('hsl(270deg, 60%, 40%)').toRgb()).toEqual({ 115 | r: 102, 116 | g: 41, 117 | b: 163, 118 | a: 1, 119 | }); 120 | 121 | expect(new FastColor('hsla(270deg, 60%, 40%, 20%)').toRgb()).toEqual({ 122 | r: 102, 123 | g: 41, 124 | b: 163, 125 | a: 0.2, 126 | }); 127 | 128 | expect(new FastColor('hsla(270deg, 60%, 40%, 23.3%)').toRgb()).toEqual({ 129 | r: 102, 130 | g: 41, 131 | b: 163, 132 | a: 0.233, 133 | }); 134 | 135 | // String back 136 | expect(new FastColor('hsla(270, 60, 40, 0.2)').toHslString()).toEqual( 137 | 'hsla(270,60%,40%,0.2)', 138 | ); 139 | expect( 140 | new FastColor('hsla(270deg, 60%, 40%, 23.3%)').toHslString(), 141 | ).toEqual('hsla(270,60%,40%,0.233)'); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/presetColors.ts: -------------------------------------------------------------------------------- 1 | // 36 Hex to reduce the size of the file 2 | export default { 3 | aliceblue: '9ehhb', 4 | antiquewhite: '9sgk7', 5 | aqua: '1ekf', 6 | aquamarine: '4zsno', 7 | azure: '9eiv3', 8 | beige: '9lhp8', 9 | bisque: '9zg04', 10 | black: '0', 11 | blanchedalmond: '9zhe5', 12 | blue: '73', 13 | blueviolet: '5e31e', 14 | brown: '6g016', 15 | burlywood: '8ouiv', 16 | cadetblue: '3qba8', 17 | chartreuse: '4zshs', 18 | chocolate: '87k0u', 19 | coral: '9yvyo', 20 | cornflowerblue: '3xael', 21 | cornsilk: '9zjz0', 22 | crimson: '8l4xo', 23 | cyan: '1ekf', 24 | darkblue: '3v', 25 | darkcyan: 'rkb', 26 | darkgoldenrod: '776yz', 27 | darkgray: '6mbhl', 28 | darkgreen: 'jr4', 29 | darkgrey: '6mbhl', 30 | darkkhaki: '7ehkb', 31 | darkmagenta: '5f91n', 32 | darkolivegreen: '3bzfz', 33 | darkorange: '9yygw', 34 | darkorchid: '5z6x8', 35 | darkred: '5f8xs', 36 | darksalmon: '9441m', 37 | darkseagreen: '5lwgf', 38 | darkslateblue: '2th1n', 39 | darkslategray: '1ugcv', 40 | darkslategrey: '1ugcv', 41 | darkturquoise: '14up', 42 | darkviolet: '5rw7n', 43 | deeppink: '9yavn', 44 | deepskyblue: '11xb', 45 | dimgray: '442g9', 46 | dimgrey: '442g9', 47 | dodgerblue: '16xof', 48 | firebrick: '6y7tu', 49 | floralwhite: '9zkds', 50 | forestgreen: '1cisi', 51 | fuchsia: '9y70f', 52 | gainsboro: '8m8kc', 53 | ghostwhite: '9pq0v', 54 | goldenrod: '8j4f4', 55 | gold: '9zda8', 56 | gray: '50i2o', 57 | green: 'pa8', 58 | greenyellow: '6senj', 59 | grey: '50i2o', 60 | honeydew: '9eiuo', 61 | hotpink: '9yrp0', 62 | indianred: '80gnw', 63 | indigo: '2xcoy', 64 | ivory: '9zldc', 65 | khaki: '9edu4', 66 | lavenderblush: '9ziet', 67 | lavender: '90c8q', 68 | lawngreen: '4vk74', 69 | lemonchiffon: '9zkct', 70 | lightblue: '6s73a', 71 | lightcoral: '9dtog', 72 | lightcyan: '8s1rz', 73 | lightgoldenrodyellow: '9sjiq', 74 | lightgray: '89jo3', 75 | lightgreen: '5nkwg', 76 | lightgrey: '89jo3', 77 | lightpink: '9z6wx', 78 | lightsalmon: '9z2ii', 79 | lightseagreen: '19xgq', 80 | lightskyblue: '5arju', 81 | lightslategray: '4nwk9', 82 | lightslategrey: '4nwk9', 83 | lightsteelblue: '6wau6', 84 | lightyellow: '9zlcw', 85 | lime: '1edc', 86 | limegreen: '1zcxe', 87 | linen: '9shk6', 88 | magenta: '9y70f', 89 | maroon: '4zsow', 90 | mediumaquamarine: '40eju', 91 | mediumblue: '5p', 92 | mediumorchid: '79qkz', 93 | mediumpurple: '5r3rv', 94 | mediumseagreen: '2d9ip', 95 | mediumslateblue: '4tcku', 96 | mediumspringgreen: '1di2', 97 | mediumturquoise: '2uabw', 98 | mediumvioletred: '7rn9h', 99 | midnightblue: 'z980', 100 | mintcream: '9ljp6', 101 | mistyrose: '9zg0x', 102 | moccasin: '9zfzp', 103 | navajowhite: '9zest', 104 | navy: '3k', 105 | oldlace: '9wq92', 106 | olive: '50hz4', 107 | olivedrab: '472ub', 108 | orange: '9z3eo', 109 | orangered: '9ykg0', 110 | orchid: '8iu3a', 111 | palegoldenrod: '9bl4a', 112 | palegreen: '5yw0o', 113 | paleturquoise: '6v4ku', 114 | palevioletred: '8k8lv', 115 | papayawhip: '9zi6t', 116 | peachpuff: '9ze0p', 117 | peru: '80oqn', 118 | pink: '9z8wb', 119 | plum: '8nba5', 120 | powderblue: '6wgdi', 121 | purple: '4zssg', 122 | rebeccapurple: '3zk49', 123 | red: '9y6tc', 124 | rosybrown: '7cv4f', 125 | royalblue: '2jvtt', 126 | saddlebrown: '5fmkz', 127 | salmon: '9rvci', 128 | sandybrown: '9jn1c', 129 | seagreen: '1tdnb', 130 | seashell: '9zje6', 131 | sienna: '6973h', 132 | silver: '7ir40', 133 | skyblue: '5arjf', 134 | slateblue: '45e4t', 135 | slategray: '4e100', 136 | slategrey: '4e100', 137 | snow: '9zke2', 138 | springgreen: '1egv', 139 | steelblue: '2r1kk', 140 | tan: '87yx8', 141 | teal: 'pds', 142 | thistle: '8ggk8', 143 | tomato: '9yqfb', 144 | turquoise: '2j4r4', 145 | violet: '9b10u', 146 | wheat: '9ld4j', 147 | white: '9zldr', 148 | whitesmoke: '9lhpx', 149 | yellow: '9zl6o', 150 | yellowgreen: '61fzm', 151 | }; 152 | -------------------------------------------------------------------------------- /tests/css-rgb-syntax.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | // Support CSS rgb() and rgba() syntax with absolute values 4 | // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb 5 | describe('css rgb() syntax', () => { 6 | describe('new space-separated syntax', () => { 7 | it('parse number (0-255)', () => { 8 | expect(new FastColor('rgb(255 255 255)').toRgb()).toEqual({ 9 | r: 255, 10 | g: 255, 11 | b: 255, 12 | a: 1, 13 | }); 14 | }); 15 | 16 | it('parse number alpha (0-1)', () => { 17 | expect(new FastColor('rgb(255 255 255 / 0.2)').toRgb()).toEqual({ 18 | r: 255, 19 | g: 255, 20 | b: 255, 21 | a: 0.2, 22 | }); 23 | 24 | expect(new FastColor('rgb(255 255 255 / .2)').toRgb()).toEqual({ 25 | r: 255, 26 | g: 255, 27 | b: 255, 28 | a: 0.2, 29 | }); 30 | 31 | expect(new FastColor('rgb(255 255 255 / 0.233)').toRgb()).toEqual({ 32 | r: 255, 33 | g: 255, 34 | b: 255, 35 | a: 0.233, 36 | }); 37 | 38 | expect(new FastColor('rgb(255 255 255 / .233)').toRgb()).toEqual({ 39 | r: 255, 40 | g: 255, 41 | b: 255, 42 | a: 0.233, 43 | }); 44 | }); 45 | 46 | it('parse percentage (0-100%)', () => { 47 | expect(new FastColor('rgb(100% 100% 100%)').toRgb()).toEqual({ 48 | r: 255, 49 | g: 255, 50 | b: 255, 51 | a: 1, 52 | }); 53 | 54 | expect(new FastColor('rgb(100% 100% 100%)').toRgb()).toEqual({ 55 | r: 255, 56 | g: 255, 57 | b: 255, 58 | a: 1, 59 | }); 60 | }); 61 | 62 | it('parse percentage alpha (0-1)', () => { 63 | expect(new FastColor('rgb(100% 100% 100% / 20%)').toRgb()).toEqual({ 64 | r: 255, 65 | g: 255, 66 | b: 255, 67 | a: 0.2, 68 | }); 69 | 70 | expect(new FastColor('rgb(100% 100% 100% / 23.3%)').toRgb()).toEqual({ 71 | r: 255, 72 | g: 255, 73 | b: 255, 74 | a: 0.233, 75 | }); 76 | }); 77 | }); 78 | 79 | describe('old comma-separated syntax', () => { 80 | it('parse number (0-255)', () => { 81 | expect(new FastColor('rgb(255, 255, 255)').toRgb()).toEqual({ 82 | r: 255, 83 | g: 255, 84 | b: 255, 85 | a: 1, 86 | }); 87 | }); 88 | 89 | it('parse number alpha (0-1)', () => { 90 | expect(new FastColor('rgba(255, 255, 255, 0.2)').toRgb()).toEqual({ 91 | r: 255, 92 | g: 255, 93 | b: 255, 94 | a: 0.2, 95 | }); 96 | 97 | expect(new FastColor('rgba(255, 255, 255, .2)').toRgb()).toEqual({ 98 | r: 255, 99 | g: 255, 100 | b: 255, 101 | a: 0.2, 102 | }); 103 | 104 | expect(new FastColor('rgba(255, 255, 255, 0.233)').toRgb()).toEqual({ 105 | r: 255, 106 | g: 255, 107 | b: 255, 108 | a: 0.233, 109 | }); 110 | 111 | expect(new FastColor('rgba(255, 255, 255, .233)').toRgb()).toEqual({ 112 | r: 255, 113 | g: 255, 114 | b: 255, 115 | a: 0.233, 116 | }); 117 | }); 118 | 119 | it('parse percentage (0-100%)', () => { 120 | expect(new FastColor('rgb(100%, 100%, 100%)').toRgb()).toEqual({ 121 | r: 255, 122 | g: 255, 123 | b: 255, 124 | a: 1, 125 | }); 126 | }); 127 | 128 | it('parse percentage alpha (0-1)', () => { 129 | expect(new FastColor('rgba(100%, 100%, 100%, 20%)').toRgb()).toEqual({ 130 | r: 255, 131 | g: 255, 132 | b: 255, 133 | a: 0.2, 134 | }); 135 | 136 | expect(new FastColor('rgba(100%, 100%, 100%, 23.3%)').toRgb()).toEqual({ 137 | r: 255, 138 | g: 255, 139 | b: 255, 140 | a: 0.233, 141 | }); 142 | }); 143 | }); 144 | 145 | it('invalid rgb', () => { 146 | expect(new FastColor('rgb').toRgb()).toEqual({ 147 | r: 0, 148 | g: 0, 149 | b: 0, 150 | a: 1, 151 | }); 152 | }); 153 | 154 | it('rgb with extra stop', () => { 155 | expect(new FastColor('rgb(255, 90, 30) 0%').toRgb()).toEqual({ 156 | r: 255, 157 | g: 90, 158 | b: 30, 159 | a: 1, 160 | }); 161 | }); 162 | 163 | it('pure rbg', () => { 164 | expect(new FastColor('FF00FF').toRgb()).toEqual({ 165 | r: 255, 166 | g: 0, 167 | b: 255, 168 | a: 1, 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /tests/preset.test.ts: -------------------------------------------------------------------------------- 1 | import { FastColor } from '../src'; 2 | 3 | describe('index', () => { 4 | const originPresetColors = { 5 | aliceblue: '#f0f8ff', 6 | antiquewhite: '#faebd7', 7 | aqua: '#00ffff', 8 | aquamarine: '#7fffd4', 9 | azure: '#f0ffff', 10 | beige: '#f5f5dc', 11 | bisque: '#ffe4c4', 12 | black: '#000000', 13 | blanchedalmond: '#ffebcd', 14 | blue: '#0000ff', 15 | blueviolet: '#8a2be2', 16 | brown: '#a52a2a', 17 | burlywood: '#deb887', 18 | cadetblue: '#5f9ea0', 19 | chartreuse: '#7fff00', 20 | chocolate: '#d2691e', 21 | coral: '#ff7f50', 22 | cornflowerblue: '#6495ed', 23 | cornsilk: '#fff8dc', 24 | crimson: '#dc143c', 25 | cyan: '#00ffff', 26 | darkblue: '#00008b', 27 | darkcyan: '#008b8b', 28 | darkgoldenrod: '#b8860b', 29 | darkgray: '#a9a9a9', 30 | darkgreen: '#006400', 31 | darkgrey: '#a9a9a9', 32 | darkkhaki: '#bdb76b', 33 | darkmagenta: '#8b008b', 34 | darkolivegreen: '#556b2f', 35 | darkorange: '#ff8c00', 36 | darkorchid: '#9932cc', 37 | darkred: '#8b0000', 38 | darksalmon: '#e9967a', 39 | darkseagreen: '#8fbc8f', 40 | darkslateblue: '#483d8b', 41 | darkslategray: '#2f4f4f', 42 | darkslategrey: '#2f4f4f', 43 | darkturquoise: '#00ced1', 44 | darkviolet: '#9400d3', 45 | deeppink: '#ff1493', 46 | deepskyblue: '#00bfff', 47 | dimgray: '#696969', 48 | dimgrey: '#696969', 49 | dodgerblue: '#1e90ff', 50 | firebrick: '#b22222', 51 | floralwhite: '#fffaf0', 52 | forestgreen: '#228b22', 53 | fuchsia: '#ff00ff', 54 | gainsboro: '#dcdcdc', 55 | ghostwhite: '#f8f8ff', 56 | goldenrod: '#daa520', 57 | gold: '#ffd700', 58 | gray: '#808080', 59 | green: '#008000', 60 | greenyellow: '#adff2f', 61 | grey: '#808080', 62 | honeydew: '#f0fff0', 63 | hotpink: '#ff69b4', 64 | indianred: '#cd5c5c', 65 | indigo: '#4b0082', 66 | ivory: '#fffff0', 67 | khaki: '#f0e68c', 68 | lavenderblush: '#fff0f5', 69 | lavender: '#e6e6fa', 70 | lawngreen: '#7cfc00', 71 | lemonchiffon: '#fffacd', 72 | lightblue: '#add8e6', 73 | lightcoral: '#f08080', 74 | lightcyan: '#e0ffff', 75 | lightgoldenrodyellow: '#fafad2', 76 | lightgray: '#d3d3d3', 77 | lightgreen: '#90ee90', 78 | lightgrey: '#d3d3d3', 79 | lightpink: '#ffb6c1', 80 | lightsalmon: '#ffa07a', 81 | lightseagreen: '#20b2aa', 82 | lightskyblue: '#87cefa', 83 | lightslategray: '#778899', 84 | lightslategrey: '#778899', 85 | lightsteelblue: '#b0c4de', 86 | lightyellow: '#ffffe0', 87 | lime: '#00ff00', 88 | limegreen: '#32cd32', 89 | linen: '#faf0e6', 90 | magenta: '#ff00ff', 91 | maroon: '#800000', 92 | mediumaquamarine: '#66cdaa', 93 | mediumblue: '#0000cd', 94 | mediumorchid: '#ba55d3', 95 | mediumpurple: '#9370db', 96 | mediumseagreen: '#3cb371', 97 | mediumslateblue: '#7b68ee', 98 | mediumspringgreen: '#00fa9a', 99 | mediumturquoise: '#48d1cc', 100 | mediumvioletred: '#c71585', 101 | midnightblue: '#191970', 102 | mintcream: '#f5fffa', 103 | mistyrose: '#ffe4e1', 104 | moccasin: '#ffe4b5', 105 | navajowhite: '#ffdead', 106 | navy: '#000080', 107 | oldlace: '#fdf5e6', 108 | olive: '#808000', 109 | olivedrab: '#6b8e23', 110 | orange: '#ffa500', 111 | orangered: '#ff4500', 112 | orchid: '#da70d6', 113 | palegoldenrod: '#eee8aa', 114 | palegreen: '#98fb98', 115 | paleturquoise: '#afeeee', 116 | palevioletred: '#db7093', 117 | papayawhip: '#ffefd5', 118 | peachpuff: '#ffdab9', 119 | peru: '#cd853f', 120 | pink: '#ffc0cb', 121 | plum: '#dda0dd', 122 | powderblue: '#b0e0e6', 123 | purple: '#800080', 124 | rebeccapurple: '#663399', 125 | red: '#ff0000', 126 | rosybrown: '#bc8f8f', 127 | royalblue: '#4169e1', 128 | saddlebrown: '#8b4513', 129 | salmon: '#fa8072', 130 | sandybrown: '#f4a460', 131 | seagreen: '#2e8b57', 132 | seashell: '#fff5ee', 133 | sienna: '#a0522d', 134 | silver: '#c0c0c0', 135 | skyblue: '#87ceeb', 136 | slateblue: '#6a5acd', 137 | slategray: '#708090', 138 | slategrey: '#708090', 139 | snow: '#fffafa', 140 | springgreen: '#00ff7f', 141 | steelblue: '#4682b4', 142 | tan: '#d2b48c', 143 | teal: '#008080', 144 | thistle: '#d8bfd8', 145 | tomato: '#ff6347', 146 | turquoise: '#40e0d0', 147 | violet: '#ee82ee', 148 | wheat: '#f5deb3', 149 | white: '#ffffff', 150 | whitesmoke: '#f5f5f5', 151 | yellow: '#ffff00', 152 | yellowgreen: '#9acd32', 153 | }; 154 | 155 | for (const colorName in originPresetColors) { 156 | it(`preset color: ${colorName}`, () => { 157 | const color = new FastColor(colorName); 158 | expect(color.toHexString()).toBe(originPresetColors[colorName]); 159 | }); 160 | } 161 | }); 162 | -------------------------------------------------------------------------------- /tests/ctrl-tinycolor/TinyColor.test.ts: -------------------------------------------------------------------------------- 1 | // Modified from https://github.com/scttcper/tinycolor/blob/master/test/index.spec.ts 2 | 3 | import { FastColor } from '../../src'; 4 | 5 | describe('@ctrl/tinycolor compatibility', () => { 6 | it('should init', () => { 7 | const r = new FastColor('#66ccff'); 8 | expect(r.toHexString()).toBe('#66ccff'); 9 | expect(r).toBeTruthy(); 10 | }); 11 | 12 | it('should clone', () => { 13 | const color1 = new FastColor('#66ccff'); 14 | expect(color1.toString()).toBe('rgb(102,204,255)'); 15 | expect(color1.toRgb()).toEqual({ 16 | a: 1, 17 | b: 255, 18 | g: 204, 19 | r: 102, 20 | }); 21 | let color2 = color1.clone(); 22 | expect(color2.toRgb()).toEqual({ 23 | a: 1, 24 | b: 255, 25 | g: 204, 26 | r: 102, 27 | }); 28 | color2 = color2.setA(0.5); 29 | expect(color2.toString()).toBe('rgba(102,204,255,0.5)'); 30 | }); 31 | 32 | it('should parse hex', () => { 33 | expect(new FastColor('#000').toHexString()).toBe('#000000'); 34 | expect(new FastColor('#0000').toHexString()).toBe('#00000000'); 35 | expect(new FastColor('#000').a).toBe(1); 36 | // Not sure this is expected behavior 37 | expect(new FastColor('#0000').a).toBe(0); 38 | }); 39 | 40 | it('should parse rgb', () => { 41 | // parenthesized input 42 | expect(new FastColor('rgb(255,0,0)').toHexString()).toBe('#ff0000'); 43 | // parenthesized spaced input 44 | expect(new FastColor('rgb (255,0,0)').toHexString()).toBe('#ff0000'); 45 | // object input 46 | expect(new FastColor({ r: 255, g: 0, b: 0 }).toHexString()).toBe('#ff0000'); 47 | // object input and compare 48 | expect(new FastColor({ r: 255, g: 0, b: 0 }).toRgb()).toEqual({ 49 | r: 255, 50 | g: 0, 51 | b: 0, 52 | a: 1, 53 | }); 54 | 55 | expect( 56 | new FastColor({ r: 200, g: 100, b: 0 }).equals( 57 | new FastColor('rgb(200, 100, 0)'), 58 | ), 59 | ).toBe(true); 60 | expect( 61 | new FastColor({ r: 200, g: 100, b: 0, a: 0.4 }).equals( 62 | new FastColor('rgba(200 100 0 .4)'), 63 | ), 64 | ).toBe(true); 65 | 66 | expect( 67 | new FastColor({ r: 199, g: 100, b: 0 }).equals( 68 | new FastColor({ r: 200, g: 100, b: 0 }), 69 | ), 70 | ).toBe(false); 71 | 72 | expect( 73 | new FastColor({ r: 199, g: 100, b: 0 }).equals( 74 | new FastColor({ r: 200, g: 100, b: 0 }), 75 | ), 76 | ).toBe(false); 77 | }); 78 | 79 | it('should parse percentage rgb text', () => { 80 | // parenthesized input 81 | expect(new FastColor('rgb(100%, 0%, 0%)').toHexString()).toBe('#ff0000'); 82 | // parenthesized spaced input 83 | expect(new FastColor('rgb (100%, 0%, 0%)').toHexString()).toBe('#ff0000'); 84 | }); 85 | 86 | it('should parse HSL', () => { 87 | // to hex 88 | expect(new FastColor({ h: 251, s: 1, l: 0.38 }).toHexString()).toBe( 89 | '#2400c2', 90 | ); 91 | // to rgb 92 | expect(new FastColor({ h: 251, s: 1, l: 0.38 }).toRgbString()).toBe( 93 | 'rgb(36,0,194)', 94 | ); 95 | // to hsl 96 | expect(new FastColor({ h: 251, s: 1, l: 0.38 }).toHslString()).toBe( 97 | 'hsl(251,100%,38%)', 98 | ); 99 | expect( 100 | new FastColor({ h: 251, s: 1, l: 0.38, a: 0.38 }).toHslString(), 101 | ).toBe('hsla(251,100%,38%,0.38)'); 102 | // to hex 103 | expect(new FastColor('hsl(251,100,38)').toHexString()).toBe('#2400c2'); 104 | // to rgb 105 | expect(new FastColor('hsl(251,100%,38%)').toRgbString()).toBe( 106 | 'rgb(36,0,194)', 107 | ); 108 | // to hsl 109 | expect(new FastColor('hsl(251,100%,38%)').toHslString()).toBe( 110 | 'hsl(251,100%,38%)', 111 | ); 112 | }); 113 | 114 | it('should get alpha', () => { 115 | let hexSetter = new FastColor('rgba(255,0,0,1)'); 116 | // Alpha should start as 1 117 | expect(hexSetter.a).toBe(1); 118 | hexSetter = hexSetter.setA(0.9); 119 | // setAlpha should change alpha value 120 | expect(hexSetter.a).toBe(0.9); 121 | hexSetter = hexSetter.setA(0.5); 122 | // setAlpha should change alpha value 123 | expect(hexSetter.a).toBe(0.5); 124 | }); 125 | 126 | it('should set alpha', () => { 127 | let hexSetter = new FastColor('rgba(255,0,0,1)'); 128 | // Alpha should start as 1 129 | expect(hexSetter.a).toBe(1); 130 | hexSetter = hexSetter.setA(0.5); 131 | // setAlpha should change alpha value 132 | expect(hexSetter.a).toBe(0.5); 133 | hexSetter = hexSetter.setA(0); 134 | // setAlpha should change alpha value 135 | expect(hexSetter.a).toBe(0); 136 | hexSetter = hexSetter.setA(-1); 137 | // setAlpha with value < 0 is corrected to 0 138 | expect(hexSetter.a).toBe(0); 139 | hexSetter = hexSetter.setA(2); 140 | // setAlpha with value > 1 is corrected to 1 141 | expect(hexSetter.a).toBe(1); 142 | }); 143 | 144 | it('should getBrightness', () => { 145 | expect(new FastColor('#000').getBrightness()).toBe(0); 146 | expect(new FastColor('#fff').getBrightness()).toBe(255); 147 | }); 148 | 149 | it('should getLuminance', () => { 150 | expect(new FastColor('#000').getLuminance()).toBe(0); 151 | expect(new FastColor('#fff').getLuminance()).toBe(1); 152 | }); 153 | 154 | it('isDark returns true/false for dark/light colors', () => { 155 | expect(new FastColor('#000').isDark()).toBe(true); 156 | expect(new FastColor('#111').isDark()).toBe(true); 157 | expect(new FastColor('#222').isDark()).toBe(true); 158 | expect(new FastColor('#333').isDark()).toBe(true); 159 | expect(new FastColor('#444').isDark()).toBe(true); 160 | expect(new FastColor('#555').isDark()).toBe(true); 161 | expect(new FastColor('#666').isDark()).toBe(true); 162 | expect(new FastColor('#777').isDark()).toBe(true); 163 | expect(new FastColor('#888').isDark()).toBe(false); 164 | expect(new FastColor('#999').isDark()).toBe(false); 165 | expect(new FastColor('#aaa').isDark()).toBe(false); 166 | expect(new FastColor('#bbb').isDark()).toBe(false); 167 | expect(new FastColor('#ccc').isDark()).toBe(false); 168 | expect(new FastColor('#ddd').isDark()).toBe(false); 169 | expect(new FastColor('#eee').isDark()).toBe(false); 170 | expect(new FastColor('#fff').isDark()).toBe(false); 171 | }); 172 | it('isLight returns true/false for light/dark colors', () => { 173 | expect(new FastColor('#000').isLight()).toBe(false); 174 | expect(new FastColor('#111').isLight()).toBe(false); 175 | expect(new FastColor('#222').isLight()).toBe(false); 176 | expect(new FastColor('#333').isLight()).toBe(false); 177 | expect(new FastColor('#444').isLight()).toBe(false); 178 | expect(new FastColor('#555').isLight()).toBe(false); 179 | expect(new FastColor('#666').isLight()).toBe(false); 180 | expect(new FastColor('#777').isLight()).toBe(false); 181 | expect(new FastColor('#888').isLight()).toBe(true); 182 | expect(new FastColor('#999').isLight()).toBe(true); 183 | expect(new FastColor('#aaa').isLight()).toBe(true); 184 | expect(new FastColor('#bbb').isLight()).toBe(true); 185 | expect(new FastColor('#ccc').isLight()).toBe(true); 186 | expect(new FastColor('#ddd').isLight()).toBe(true); 187 | expect(new FastColor('#eee').isLight()).toBe(true); 188 | expect(new FastColor('#fff').isLight()).toBe(true); 189 | }); 190 | 191 | it('Color equality', () => { 192 | expect(new FastColor('#ff0000').equals(new FastColor('#ff0000'))).toBe( 193 | true, 194 | ); 195 | expect(new FastColor('#ff0000').equals(new FastColor('rgb(255,0,0)'))).toBe( 196 | true, 197 | ); 198 | expect( 199 | new FastColor('#ff0000').equals(new FastColor('rgba(255,0,0,.1)')), 200 | ).toBe(false); 201 | expect( 202 | new FastColor('#ff000066').equals(new FastColor('rgba(255,0,0,.4)')), 203 | ).toBe(true); 204 | expect( 205 | new FastColor('#f009').equals(new FastColor('rgba(255,0,0,.6)')), 206 | ).toBe(true); 207 | expect(new FastColor('#336699CC').equals(new FastColor('#369C'))).toBe( 208 | true, 209 | ); 210 | expect(new FastColor('#f00').equals(new FastColor('#ff0000'))).toBe(true); 211 | expect(new FastColor('#f00').equals(new FastColor('#ff0000'))).toBe(true); 212 | expect(new FastColor('#ff0000').equals(new FastColor('#00ff00'))).toBe( 213 | false, 214 | ); 215 | expect( 216 | new FastColor('#ff8000').equals(new FastColor('rgb(100%, 50%, 0%)')), 217 | ).toBe(true); 218 | }); 219 | 220 | it('onBackground', () => { 221 | expect( 222 | new FastColor('#ffffff') 223 | .onBackground(new FastColor('#000')) 224 | .toHexString(), 225 | ).toBe('#ffffff'); 226 | expect( 227 | new FastColor('#ffffff00') 228 | .onBackground(new FastColor('#000')) 229 | .toHexString(), 230 | ).toBe('#000000'); 231 | expect( 232 | new FastColor('#ffffff77') 233 | .onBackground(new FastColor('#000')) 234 | .toHexString(), 235 | ).toBe('#777777'); 236 | expect( 237 | new FastColor('#262a6d82') 238 | .onBackground(new FastColor('#644242')) 239 | .toHexString(), 240 | ).toBe('#443658'); 241 | expect( 242 | new FastColor('rgba(255,0,0,0.5)') 243 | .onBackground(new FastColor('rgba(0,255,0,0.5)')) 244 | .toRgbString(), 245 | ).toBe('rgba(170,85,0,0.75)'); 246 | expect( 247 | new FastColor('rgba(255,0,0,0.5)') 248 | .onBackground(new FastColor('rgba(0,0,255,1)')) 249 | .toRgbString(), 250 | ).toBe('rgb(128,0,128)'); 251 | expect( 252 | new FastColor('rgba(0,0,255,1)') 253 | .onBackground(new FastColor('rgba(0,0,0,0.5)')) 254 | .toRgbString(), 255 | ).toBe('rgb(0,0,255)'); 256 | }); 257 | 258 | it('default of empty', () => { 259 | expect(new FastColor('').toRgb()).toEqual({ 260 | r: 0, 261 | g: 0, 262 | b: 0, 263 | a: 1, 264 | }); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /src/FastColor.ts: -------------------------------------------------------------------------------- 1 | import presetColors from './presetColors'; 2 | import type { ColorInput, HSL, HSV, OptionalA, RGB } from './types'; 3 | 4 | type Constructor = new (...args: any[]) => T; 5 | 6 | type ParseNumber = (num: number, txt: string, index: number) => number; 7 | 8 | const round = Math.round; 9 | 10 | /** 11 | * Support format, alpha unit will check the % mark: 12 | * - rgba(102, 204, 255, .5) -> [102, 204, 255, 0.5] 13 | * - rgb(102 204 255 / .5) -> [102, 204, 255, 0.5] 14 | * - rgb(100%, 50%, 0% / 50%) -> [255, 128, 0, 0.5] 15 | * - hsl(270, 60, 40, .5) -> [270, 60, 40, 0.5] 16 | * - hsl(270deg 60% 40% / 50%) -> [270, 60, 40, 0.5] 17 | * 18 | * When `base` is provided, the percentage value will be divided by `base`. 19 | */ 20 | function splitColorStr(str: string, parseNum: ParseNumber): number[] { 21 | const match: string[] = 22 | str 23 | // Remove str before `(` 24 | .replace(/^[^(]*\((.*)/, '$1') 25 | // Remove str after `)` 26 | .replace(/\).*/, '') 27 | .match(/\d*\.?\d+%?/g) || []; 28 | const numList = match.map((item) => parseFloat(item)); 29 | 30 | for (let i = 0; i < 3; i += 1) { 31 | numList[i] = parseNum(numList[i] || 0, match[i] || '', i); 32 | } 33 | 34 | // For alpha. 50% should be 0.5 35 | if (match[3]) { 36 | numList[3] = match[3].includes('%') ? numList[3] / 100 : numList[3]; 37 | } else { 38 | // By default, alpha is 1 39 | numList[3] = 1; 40 | } 41 | 42 | return numList; 43 | } 44 | 45 | const parseHSVorHSL: ParseNumber = (num, _, index) => 46 | index === 0 ? num : num / 100; 47 | 48 | /** round and limit number to integer between 0-255 */ 49 | function limitRange(value: number, max?: number) { 50 | const mergedMax = max || 255; 51 | 52 | if (value > mergedMax) { 53 | return mergedMax; 54 | } 55 | if (value < 0) { 56 | return 0; 57 | } 58 | return value; 59 | } 60 | 61 | export class FastColor { 62 | /** 63 | * All FastColor objects are valid. So isValid is always true. This property is kept to be compatible with TinyColor. 64 | */ 65 | isValid: boolean = true; 66 | 67 | /** 68 | * Red, R in RGB 69 | */ 70 | r: number = 0; 71 | 72 | /** 73 | * Green, G in RGB 74 | */ 75 | g: number = 0; 76 | 77 | /** 78 | * Blue, B in RGB 79 | */ 80 | b: number = 0; 81 | 82 | /** 83 | * Alpha/Opacity, A in RGBA/HSLA 84 | */ 85 | a: number = 1; 86 | 87 | // HSV privates 88 | private _h?: number; 89 | private _hsl_s?: number; 90 | private _hsv_s?: number; 91 | private _l?: number; 92 | private _v?: number; 93 | 94 | // intermediate variables to calculate HSL/HSV 95 | private _max?: number; 96 | private _min?: number; 97 | 98 | private _brightness?: number; 99 | 100 | constructor(input: ColorInput) { 101 | /** 102 | * Always check 3 char in the object to determine the format. 103 | * We not use function in check to save bundle size. 104 | * e.g. 'rgb' -> { r: 0, g: 0, b: 0 }. 105 | */ 106 | function matchFormat(str: string) { 107 | return ( 108 | str[0] in (input as object) && 109 | str[1] in (input as object) && 110 | str[2] in (input as object) 111 | ); 112 | } 113 | 114 | if (!input) { 115 | // Do nothing since already initialized 116 | } else if (typeof input === 'string') { 117 | const trimStr = input.trim(); 118 | 119 | function matchPrefix(prefix: string) { 120 | return trimStr.startsWith(prefix); 121 | } 122 | 123 | if (/^#?[A-F\d]{3,8}$/i.test(trimStr)) { 124 | this.fromHexString(trimStr); 125 | } else if (matchPrefix('rgb')) { 126 | this.fromRgbString(trimStr); 127 | } else if (matchPrefix('hsl')) { 128 | this.fromHslString(trimStr); 129 | } else if (matchPrefix('hsv') || matchPrefix('hsb')) { 130 | this.fromHsvString(trimStr); 131 | } else { 132 | // From preset color 133 | const presetColor = presetColors[trimStr.toLowerCase()]; 134 | if (presetColor) { 135 | this.fromHexString( 136 | // Convert 36 hex to 16 hex 137 | parseInt(presetColor, 36).toString(16).padStart(6, '0'), 138 | ); 139 | } 140 | } 141 | } else if (input instanceof FastColor) { 142 | this.r = input.r; 143 | this.g = input.g; 144 | this.b = input.b; 145 | this.a = input.a; 146 | this._h = input._h; 147 | this._hsl_s = input._hsl_s; 148 | this._hsv_s = input._hsv_s; 149 | this._l = input._l; 150 | this._v = input._v; 151 | } else if (matchFormat('rgb')) { 152 | this.r = limitRange((input as RGB).r); 153 | this.g = limitRange((input as RGB).g); 154 | this.b = limitRange((input as RGB).b); 155 | this.a = 156 | typeof input.a === 'number' ? limitRange((input as RGB).a, 1) : 1; 157 | } else if (matchFormat('hsl')) { 158 | this.fromHsl(input as HSL); 159 | } else if (matchFormat('hsv')) { 160 | this.fromHsv(input as HSV); 161 | } else { 162 | throw new Error( 163 | '@ant-design/fast-color: unsupported input ' + JSON.stringify(input), 164 | ); 165 | } 166 | } 167 | 168 | // ======================= Setter ======================= 169 | 170 | setR(value: number) { 171 | return this._sc('r', value); 172 | } 173 | 174 | setG(value: number) { 175 | return this._sc('g', value); 176 | } 177 | 178 | setB(value: number) { 179 | return this._sc('b', value); 180 | } 181 | 182 | setA(value: number) { 183 | return this._sc('a', value, 1); 184 | } 185 | 186 | setHue(value: number) { 187 | const hsv = this.toHsv(); 188 | hsv.h = value; 189 | return this._c(hsv); 190 | } 191 | 192 | // ======================= Getter ======================= 193 | /** 194 | * Returns the perceived luminance of a color, from 0-1. 195 | * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 196 | */ 197 | getLuminance(): number { 198 | function adjustGamma(raw: number) { 199 | const val = raw / 255; 200 | 201 | return val <= 0.03928 202 | ? val / 12.92 203 | : Math.pow((val + 0.055) / 1.055, 2.4); 204 | } 205 | 206 | const R = adjustGamma(this.r); 207 | const G = adjustGamma(this.g); 208 | const B = adjustGamma(this.b); 209 | 210 | return 0.2126 * R + 0.7152 * G + 0.0722 * B; 211 | } 212 | 213 | getHue(): number { 214 | if (typeof this._h === 'undefined') { 215 | const delta = this.getMax() - this.getMin(); 216 | if (delta === 0) { 217 | this._h = 0; 218 | } else { 219 | this._h = round( 220 | 60 * 221 | (this.r === this.getMax() 222 | ? (this.g - this.b) / delta + (this.g < this.b ? 6 : 0) 223 | : this.g === this.getMax() 224 | ? (this.b - this.r) / delta + 2 225 | : (this.r - this.g) / delta + 4), 226 | ); 227 | } 228 | } 229 | return this._h; 230 | } 231 | 232 | /** 233 | * @deprecated should use getHSVSaturation or getHSLSaturation instead 234 | */ 235 | getSaturation(): number { 236 | return this.getHSVSaturation(); 237 | } 238 | 239 | getHSVSaturation(): number { 240 | if (typeof this._hsv_s === 'undefined') { 241 | const delta = this.getMax() - this.getMin(); 242 | if (delta === 0) { 243 | this._hsv_s = 0; 244 | } else { 245 | this._hsv_s = delta / this.getMax(); 246 | } 247 | } 248 | return this._hsv_s; 249 | } 250 | 251 | getHSLSaturation(): number { 252 | if (typeof this._hsl_s === 'undefined') { 253 | const delta = this.getMax() - this.getMin(); 254 | if (delta === 0) { 255 | this._hsl_s = 0; 256 | } else { 257 | const l = this.getLightness(); 258 | this._hsl_s = (delta/255) / (1 - Math.abs(2 * l - 1)); 259 | } 260 | } 261 | return this._hsl_s; 262 | } 263 | 264 | getLightness(): number { 265 | if (typeof this._l === 'undefined') { 266 | this._l = (this.getMax() + this.getMin()) / 510; 267 | } 268 | return this._l; 269 | } 270 | 271 | getValue(): number { 272 | if (typeof this._v === 'undefined') { 273 | this._v = this.getMax() / 255; 274 | } 275 | return this._v; 276 | } 277 | 278 | /** 279 | * Returns the perceived brightness of the color, from 0-255. 280 | * Note: this is not the b of HSB 281 | * @see http://www.w3.org/TR/AERT#color-contrast 282 | */ 283 | getBrightness(): number { 284 | if (typeof this._brightness === 'undefined') { 285 | this._brightness = (this.r * 299 + this.g * 587 + this.b * 114) / 1000; 286 | } 287 | return this._brightness; 288 | } 289 | 290 | // ======================== Func ======================== 291 | 292 | darken(amount = 10) { 293 | const h = this.getHue(); 294 | const s = this.getSaturation(); 295 | let l = this.getLightness() - amount / 100; 296 | if (l < 0) { 297 | l = 0; 298 | } 299 | return this._c({ h, s, l, a: this.a }); 300 | } 301 | 302 | lighten(amount = 10) { 303 | const h = this.getHue(); 304 | const s = this.getSaturation(); 305 | let l = this.getLightness() + amount / 100; 306 | if (l > 1) { 307 | l = 1; 308 | } 309 | return this._c({ h, s, l, a: this.a }); 310 | } 311 | 312 | /** 313 | * Mix the current color a given amount with another color, from 0 to 100. 314 | * 0 means no mixing (return current color). 315 | */ 316 | mix(input: ColorInput, amount = 50) { 317 | const color = this._c(input); 318 | 319 | const p = amount / 100; 320 | const calc = (key: string) => (color[key] - this[key]) * p + this[key]; 321 | 322 | const rgba = { 323 | r: round(calc('r')), 324 | g: round(calc('g')), 325 | b: round(calc('b')), 326 | a: round(calc('a') * 100) / 100, 327 | }; 328 | 329 | return this._c(rgba); 330 | } 331 | 332 | /** 333 | * Mix the color with pure white, from 0 to 100. 334 | * Providing 0 will do nothing, providing 100 will always return white. 335 | */ 336 | tint(amount = 10) { 337 | return this.mix({ r: 255, g: 255, b: 255, a: 1 }, amount); 338 | } 339 | 340 | /** 341 | * Mix the color with pure black, from 0 to 100. 342 | * Providing 0 will do nothing, providing 100 will always return black. 343 | */ 344 | shade(amount = 10) { 345 | return this.mix({ r: 0, g: 0, b: 0, a: 1 }, amount); 346 | } 347 | 348 | onBackground(background: ColorInput) { 349 | const bg = this._c(background); 350 | const alpha = this.a + bg.a * (1 - this.a); 351 | 352 | const calc = (key: string) => { 353 | return round( 354 | (this[key] * this.a + bg[key] * bg.a * (1 - this.a)) / alpha, 355 | ); 356 | }; 357 | 358 | return this._c({ 359 | r: calc('r'), 360 | g: calc('g'), 361 | b: calc('b'), 362 | a: alpha, 363 | }); 364 | } 365 | 366 | // ======================= Status ======================= 367 | isDark(): boolean { 368 | return this.getBrightness() < 128; 369 | } 370 | 371 | isLight(): boolean { 372 | return this.getBrightness() >= 128; 373 | } 374 | 375 | // ======================== MISC ======================== 376 | equals(other: FastColor): boolean { 377 | return ( 378 | this.r === other.r && 379 | this.g === other.g && 380 | this.b === other.b && 381 | this.a === other.a 382 | ); 383 | } 384 | 385 | clone(): this { 386 | return this._c(this); 387 | } 388 | 389 | // ======================= Format ======================= 390 | toHexString(): string { 391 | let hex = '#'; 392 | const rHex = (this.r || 0).toString(16); 393 | hex += rHex.length === 2 ? rHex : '0' + rHex; 394 | const gHex = (this.g || 0).toString(16); 395 | hex += gHex.length === 2 ? gHex : '0' + gHex; 396 | const bHex = (this.b || 0).toString(16); 397 | hex += bHex.length === 2 ? bHex : '0' + bHex; 398 | if (typeof this.a === 'number' && this.a >= 0 && this.a < 1) { 399 | const aHex = round(this.a * 255).toString(16); 400 | hex += aHex.length === 2 ? aHex : '0' + aHex; 401 | } 402 | return hex; 403 | } 404 | 405 | /** CSS support color pattern */ 406 | toHsl(): HSL { 407 | return { 408 | h: this.getHue(), 409 | s: this.getHSLSaturation(), 410 | l: this.getLightness(), 411 | a: this.a, 412 | }; 413 | } 414 | 415 | /** CSS support color pattern */ 416 | toHslString(): string { 417 | const h = this.getHue(); 418 | const s = round(this.getHSLSaturation() * 100); 419 | const l = round(this.getLightness() * 100); 420 | 421 | return this.a !== 1 422 | ? `hsla(${h},${s}%,${l}%,${this.a})` 423 | : `hsl(${h},${s}%,${l}%)`; 424 | } 425 | 426 | /** Same as toHsb */ 427 | toHsv(): HSV { 428 | return { 429 | h: this.getHue(), 430 | s: this.getHSVSaturation(), 431 | v: this.getValue(), 432 | a: this.a, 433 | }; 434 | } 435 | 436 | toRgb(): RGB { 437 | return { 438 | r: this.r, 439 | g: this.g, 440 | b: this.b, 441 | a: this.a, 442 | }; 443 | } 444 | 445 | toRgbString(): string { 446 | return this.a !== 1 447 | ? `rgba(${this.r},${this.g},${this.b},${this.a})` 448 | : `rgb(${this.r},${this.g},${this.b})`; 449 | } 450 | 451 | toString(): string { 452 | return this.toRgbString(); 453 | } 454 | 455 | // ====================== Privates ====================== 456 | /** Return a new FastColor object with one channel changed */ 457 | private _sc(rgb: string, value: number, max?: number) { 458 | const clone = this.clone(); 459 | clone[rgb] = limitRange(value, max); 460 | return clone; 461 | } 462 | 463 | private _c(input: ColorInput): this { 464 | return new (this.constructor as Constructor)(input); 465 | } 466 | 467 | private getMax() { 468 | if (typeof this._max === 'undefined') { 469 | this._max = Math.max(this.r, this.g, this.b); 470 | } 471 | return this._max; 472 | } 473 | 474 | private getMin() { 475 | if (typeof this._min === 'undefined') { 476 | this._min = Math.min(this.r, this.g, this.b); 477 | } 478 | return this._min; 479 | } 480 | 481 | private fromHexString(trimStr: string) { 482 | const withoutPrefix = trimStr.replace('#', ''); 483 | 484 | function connectNum(index1: number, index2?: number) { 485 | return parseInt( 486 | withoutPrefix[index1] + withoutPrefix[index2 || index1], 487 | 16, 488 | ); 489 | } 490 | 491 | if (withoutPrefix.length < 6) { 492 | // #rgb or #rgba 493 | this.r = connectNum(0); 494 | this.g = connectNum(1); 495 | this.b = connectNum(2); 496 | this.a = withoutPrefix[3] ? connectNum(3) / 255 : 1; 497 | } else { 498 | // #rrggbb or #rrggbbaa 499 | this.r = connectNum(0, 1); 500 | this.g = connectNum(2, 3); 501 | this.b = connectNum(4, 5); 502 | this.a = withoutPrefix[6] ? connectNum(6, 7) / 255 : 1; 503 | } 504 | } 505 | 506 | private fromHsl({ h: _h, s, l, a }: OptionalA): void { 507 | const h = ((_h % 360) + 360) % 360; 508 | this._h = h; 509 | this._hsl_s = s; 510 | this._l = l; 511 | this.a = typeof a === 'number' ? a : 1; 512 | 513 | if (s <= 0) { 514 | const rgb = round(l * 255); 515 | this.r = rgb; 516 | this.g = rgb; 517 | this.b = rgb; 518 | return; 519 | } 520 | 521 | let r = 0, 522 | g = 0, 523 | b = 0; 524 | 525 | const huePrime = h / 60; 526 | const chroma = (1 - Math.abs(2 * l - 1)) * s; 527 | const secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1)); 528 | 529 | if (huePrime >= 0 && huePrime < 1) { 530 | r = chroma; 531 | g = secondComponent; 532 | } else if (huePrime >= 1 && huePrime < 2) { 533 | r = secondComponent; 534 | g = chroma; 535 | } else if (huePrime >= 2 && huePrime < 3) { 536 | g = chroma; 537 | b = secondComponent; 538 | } else if (huePrime >= 3 && huePrime < 4) { 539 | g = secondComponent; 540 | b = chroma; 541 | } else if (huePrime >= 4 && huePrime < 5) { 542 | r = secondComponent; 543 | b = chroma; 544 | } else if (huePrime >= 5 && huePrime < 6) { 545 | r = chroma; 546 | b = secondComponent; 547 | } 548 | 549 | const lightnessModification = l - chroma / 2; 550 | this.r = round((r + lightnessModification) * 255); 551 | this.g = round((g + lightnessModification) * 255); 552 | this.b = round((b + lightnessModification) * 255); 553 | } 554 | 555 | private fromHsv({ h: _h, s, v, a }: OptionalA): void { 556 | const h = ((_h % 360) + 360) % 360; 557 | this._h = h; 558 | this._hsv_s = s; 559 | this._v = v; 560 | this.a = typeof a === 'number' ? a : 1; 561 | 562 | const vv = round(v * 255); 563 | this.r = vv; 564 | this.g = vv; 565 | this.b = vv; 566 | 567 | if (s <= 0) { 568 | return; 569 | } 570 | 571 | const hh = h / 60; 572 | const i = Math.floor(hh); 573 | const ff = hh - i; 574 | const p = round(v * (1.0 - s) * 255); 575 | const q = round(v * (1.0 - s * ff) * 255); 576 | const t = round(v * (1.0 - s * (1.0 - ff)) * 255); 577 | 578 | switch (i) { 579 | case 0: 580 | this.g = t; 581 | this.b = p; 582 | break; 583 | case 1: 584 | this.r = q; 585 | this.b = p; 586 | break; 587 | case 2: 588 | this.r = p; 589 | this.b = t; 590 | break; 591 | case 3: 592 | this.r = p; 593 | this.g = q; 594 | break; 595 | case 4: 596 | this.r = t; 597 | this.g = p; 598 | break; 599 | case 5: 600 | default: 601 | this.g = p; 602 | this.b = q; 603 | break; 604 | } 605 | } 606 | 607 | private fromHsvString(trimStr: string) { 608 | const cells = splitColorStr(trimStr, parseHSVorHSL); 609 | 610 | this.fromHsv({ 611 | h: cells[0], 612 | s: cells[1], 613 | v: cells[2], 614 | a: cells[3], 615 | }); 616 | } 617 | 618 | private fromHslString(trimStr: string) { 619 | const cells = splitColorStr(trimStr, parseHSVorHSL); 620 | 621 | this.fromHsl({ 622 | h: cells[0], 623 | s: cells[1], 624 | l: cells[2], 625 | a: cells[3], 626 | }); 627 | } 628 | 629 | private fromRgbString(trimStr: string) { 630 | const cells = splitColorStr(trimStr, (num, txt) => 631 | // Convert percentage to number. e.g. 50% -> 128 632 | txt.includes('%') ? round((num / 100) * 255) : num, 633 | ); 634 | 635 | this.r = cells[0]; 636 | this.g = cells[1]; 637 | this.b = cells[2]; 638 | this.a = cells[3]; 639 | } 640 | } 641 | --------------------------------------------------------------------------------