├── .browserslistrc ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .postcssrc ├── .prettierrc ├── .size-limit.json ├── .stylelintrc ├── LICENSE ├── README.md ├── config.js ├── eslint.config.js ├── firebase.json ├── index.css ├── index.html ├── index.ts ├── lch.sh ├── lib ├── cameras.ts ├── canvas.ts ├── colors.ts ├── dom.ts ├── hotkeys.ts ├── loader.ts ├── math.ts ├── model.ts ├── paint.ts ├── time.ts └── workers.ts ├── nano-staged.json ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon-lch.ico ├── favicon.ico ├── lch-192.png ├── lch-512.png ├── lch.webmanifest ├── og-lch.png ├── og-oklch.png ├── oklch-192.png ├── oklch-512.png └── oklch.webmanifest ├── simple-git-hooks.json ├── stores ├── accent.ts ├── benchmark.ts ├── current.ts ├── formats.ts ├── history.ts ├── loading.ts ├── settings.ts ├── support.ts ├── url.ts └── visible.ts ├── test ├── colors.test.ts ├── inputs.test.ts └── set-globals.js ├── tsconfig.json ├── types.d.ts ├── view ├── analytics │ └── index.ts ├── base │ ├── MartianMono-1.0.0.woff2 │ ├── apple-touch-icon-lch.png │ ├── apple-touch-icon-oklch.png │ ├── colors.css │ ├── index.css │ ├── index.ts │ ├── lch.svg │ ├── meta.pug │ ├── mixins.pcss │ ├── oklch.svg │ └── reset.css ├── benchmark │ ├── index.css │ ├── index.pug │ └── index.ts ├── button │ ├── cross.svg │ ├── expand.svg │ ├── index.css │ ├── index.ts │ └── mixin.pug ├── card │ ├── index.css │ ├── index.ts │ └── mixin.pug ├── chart │ ├── index.css │ ├── index.ts │ ├── mixin.pug │ ├── paint.ts │ └── worker.ts ├── checkbox │ ├── index.css │ ├── index.ts │ └── mixin.pug ├── code │ ├── index.css │ ├── index.pug │ └── index.ts ├── field │ ├── decrease.svg │ ├── down.svg │ ├── index.css │ ├── index.ts │ └── mixin.pug ├── fullmodel │ ├── index.css │ ├── index.pug │ └── index.ts ├── layout │ ├── index.css │ ├── index.pug │ └── index.ts ├── link │ ├── external.svg │ ├── github.svg │ ├── help.svg │ ├── index.css │ ├── index.ts │ └── mixin.pug ├── main │ ├── index.css │ ├── index.pug │ ├── index.ts │ └── logo.svg ├── minimodel │ ├── index.css │ ├── index.pug │ └── index.ts ├── mode │ ├── index.css │ ├── index.pug │ └── index.ts ├── range │ ├── index.css │ ├── index.ts │ └── mixin.pug ├── sample │ ├── index.css │ ├── index.pug │ └── index.ts ├── settings │ ├── index.css │ ├── index.pug │ └── index.ts └── title │ ├── index.css │ └── index.pug └── vite.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults and supports es6-module 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.svg] 12 | insert_final_newline = false 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v4 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | with: 21 | version: 10 22 | - name: Install Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 23 26 | cache: pnpm 27 | - name: Install dependencies 28 | run: pnpm install --ignore-scripts 29 | - name: Run tests 30 | run: node --run test 31 | deploy: 32 | name: Deploy 33 | runs-on: ubuntu-latest 34 | if: github.ref == 'refs/heads/main' 35 | needs: 36 | - test 37 | steps: 38 | - name: Checkout the repository 39 | uses: actions/checkout@v4 40 | - name: Install pnpm 41 | uses: pnpm/action-setup@v4 42 | with: 43 | version: 10 44 | - name: Install Node.js 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: 23 48 | - name: Install dependencies 49 | run: pnpm install --production --ignore-scripts 50 | - name: Build OKLCH 51 | run: node --run build 52 | - name: Deploy OKLCH 53 | uses: FirebaseExtended/action-hosting-deploy@v0 54 | with: 55 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 56 | firebaseServiceAccount: '${{ secrets.FIREBASE_OKLCH_ACCOUNT }}' 57 | projectId: ai-oklch 58 | channelId: live 59 | - name: Clean build dir 60 | run: node --run clean 61 | - name: Build LCH 62 | run: node --run build:lch 63 | - name: Deploy LCH 64 | uses: FirebaseExtended/action-hosting-deploy@v0 65 | with: 66 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 67 | firebaseServiceAccount: '${{ secrets.FIREBASE_LCH_ACCOUNT }}' 68 | projectId: ai-lch 69 | channelId: live 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@csstools/postcss-oklab-function": { 4 | "subFeatures": { 5 | "displayP3": false 6 | } 7 | }, 8 | "postcss-opacity-percentage": {}, 9 | "postcss-mixins": { 10 | "mixinsFiles": "view/**/mixins.pcss" 11 | }, 12 | "postcss-nesting": {}, 13 | "postcss-media-minmax": {}, 14 | "autoprefixer": {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "jsxSingleQuote": false, 4 | "quoteProps": "consistent", 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Files to download", 4 | "path": [ 5 | "dist/**/*", 6 | "!dist/model-*.js", 7 | "!dist/*.ico", 8 | "!dist/*.webmanifest", 9 | "!dist/*.png" 10 | ], 11 | "limit": "60 KB" 12 | }, 13 | { 14 | "name": "Base scripts to execute", 15 | "path": ["dist/*.js", "!dist/model-*.js"], 16 | "brotli": false, 17 | "limit": "90 KB" 18 | }, 19 | { 20 | "name": "Scripts for 3D model", 21 | "path": "dist/model-*.js", 22 | "brotli": false, 23 | "limit": "495 KB" 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@logux/stylelint-config", 3 | "ignoreFiles": ["dist/**/*"], 4 | "rules": { 5 | "function-disallowed-list": ["rgb", "rgba", "hsl", "hsla"], 6 | "alpha-value-notation": "percentage", 7 | "color-named": "never" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2022 Andrey Sitnik , 4 | Roman Shamin . 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OKLCH & LCH Color Picker 2 | 3 | Color picker and converter for OKLCH and LCH color space. 4 | 5 | - [`oklch.com`](https://oklch.com) 6 | - [`lch.oklch.com`](https://lch.oklch.com) 7 | 8 | OKLCH is a new way to encode colors (like hex, RGBA, or HSL): 9 | 10 | - OKLCH has native browser support. 11 | - It can encode more colors for modern screens (P3, Rec. 2020, and beyond). 12 | - [Unlike HSL], OKLCH always has predictable contrast 13 | after color transformation. 14 | - In contrast [with LCH and Lab], no [hue shift] on chroma changes. 15 | - Provides great accessibility on palette generation. 16 | 17 | Additional links about Oklab and OKLCH: 18 | 19 | - [OKLCH in CSS: why we moved from RGB and HSL](https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl) 20 | - [The article by Oklab creator](https://bottosson.github.io/posts/oklab/) 21 | 22 | [Unlike HSL]: https://wildbit.com/blog/accessible-palette-stop-using-hsl-for-color-systems 23 | [with LCH and Lab]: https://bottosson.github.io/posts/oklab/#blending-colors 24 | [hue shift]: https://lch.oklch.com/#35,55,297,100 25 | 26 | ## Development 27 | 28 | To run a local copy for development: 29 | 30 | 1. Install Node.js and `pnpm`. 31 | 2. Install dependencies: 32 | 33 | ```sh 34 | pnpm install 35 | ``` 36 | 37 | 3. Run local server: 38 | 39 | ```sh 40 | pnpm start 41 | ``` 42 | 43 | We recommend installing Prettier and EditorConfig plugins to your text editor. 44 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | let config = { 2 | ALPHA_MAX: 100, 3 | ALPHA_STEP: 1, 4 | 5 | C_MAX: 0.37, 6 | C_MAX_REC2020: 0.47, 7 | C_RANDOM: 0.1, 8 | C_STEP: 0.01, 9 | 10 | COLOR_FN: '"oklch"', 11 | 12 | GAMUT_EPSILON: 1e-6, 13 | 14 | H_MAX: 360, 15 | H_STEP: 1, 16 | 17 | L_MAX: 1, 18 | L_MAX_COLOR: 1, 19 | L_STEP: 0.01 20 | } 21 | 22 | if (process.env.LCH) { 23 | config = { 24 | ...config, 25 | 26 | C_MAX: 145, 27 | C_MAX_REC2020: 195, 28 | C_RANDOM: 39, 29 | C_STEP: 1, 30 | 31 | COLOR_FN: '"lch"', 32 | 33 | L_MAX_COLOR: 100 34 | } 35 | } 36 | 37 | config.LCH = config.COLOR_FN !== '"oklch"' 38 | 39 | export default config 40 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import loguxTsConfig from '@logux/eslint-config/ts' 2 | 3 | /** @type {import('eslint').Linter.Config[]} */ 4 | export default [ 5 | { ignores: ['dist/'] }, 6 | ...loguxTsConfig, 7 | { 8 | rules: { 9 | '@typescript-eslint/no-unsafe-type-assertion': 'off', 10 | 'no-control-regex': 'off' 11 | } 12 | }, 13 | { 14 | files: ['**/worker.ts'], 15 | rules: { 16 | 'import/extensions': 'off' 17 | } 18 | }, 19 | { 20 | files: ['lib/**', 'view/**', 'stores/**'], 21 | rules: { 22 | 'n/no-unsupported-features/node-builtins': 'off' 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "headers": [ 5 | { 6 | "source": "**/*.@(js|css|woff2|svg)", 7 | "headers": [ 8 | { 9 | "key": "Cache-Control", 10 | "value": "public, max-age=31536000, immutable" 11 | } 12 | ] 13 | }, 14 | { 15 | "source": "**", 16 | "headers": [ 17 | { 18 | "key": "X-Frame-Options", 19 | "value": "DENY" 20 | }, 21 | { 22 | "key": "X-Content-Type-Options", 23 | "value": "nosniff" 24 | }, 25 | { 26 | "key": "Content-Security-Policy", 27 | "value": "default-src 'self'; frame-ancestors 'none'; img-src 'self' data:; connect-src 'self' https://plausible.io;" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | @import url("./view/base/index.css"); 2 | @import url("./view/range/index.css"); 3 | @import url("./view/field/index.css"); 4 | @import url("./view/card/index.css"); 5 | @import url("./view/chart/index.css"); 6 | @import url("./view/main/index.css"); 7 | @import url("./view/layout/index.css"); 8 | @import url("./view/sample/index.css"); 9 | @import url("./view/title/index.css"); 10 | @import url("./view/link/index.css"); 11 | @import url("./view/button/index.css"); 12 | @import url("./view/mode/index.css"); 13 | @import url("./view/code/index.css"); 14 | @import url("./view/checkbox/index.css"); 15 | @import url("./view/settings/index.css"); 16 | @import url("./view/minimodel/index.css"); 17 | @import url("./view/fullmodel/index.css"); 18 | @import url("./view/benchmark/index.css"); 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |