├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github └── FUNDING.yml ├── .gitignore ├── .node-version ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── index.html ├── jest.config.cjs ├── jsconfig.json ├── lefthook.yml ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── public ├── _headers ├── apple-touch-icon.png ├── favicon.ico └── screenshot.jpg ├── release-on-sentry.sh ├── site-info.js ├── src ├── App.svelte ├── Checkbox.svelte ├── ColorSpaceSelector.svelte ├── ColorsPlot.svelte ├── ControlGroup.svelte ├── ControlPoint.svelte ├── CopyOnClick.svelte ├── CubicBezierEditor.svelte ├── DownloadAsSvg.svelte ├── EaseControl.svelte ├── EaseSelectOptions.svelte ├── Icon.svelte ├── OverlayKnobs.svelte ├── Palette.svelte ├── PaletteKnobs.svelte ├── PaletteSelector.svelte ├── Palettes.svelte ├── Pitch.svelte ├── Plots.svelte ├── RadioGroup.svelte ├── RangeField.svelte ├── ReferenceColorFieldGroup.svelte ├── Scrim.svelte ├── SelectField.svelte ├── ShareDialog.svelte ├── SiteFooter.svelte ├── SiteHeader.svelte ├── StepsKnob.svelte ├── Swatch.svelte ├── TextField.svelte ├── TinySwatch.svelte ├── VhProvider.svelte ├── XYInputField.svelte ├── base.css ├── global.css ├── lib │ ├── array.js │ ├── clipboard.js │ ├── colors.js │ ├── colors.test.js │ ├── colors.test.js.md │ ├── colors.test.js.snap │ ├── eases.js │ ├── eases.test.js │ ├── math.js │ ├── string.js │ ├── svg.js │ └── url.js ├── machines │ └── draggableMachine.js ├── main.js └── store.js ├── tailwind.config.cjs └── vite.config.js /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: ["eslint:recommended", "plugin:svelte/recommended", "prettier"], 5 | parserOptions: { 6 | sourceType: "module", 7 | ecmaVersion: 2020, 8 | extraFileExtensions: [".svelte"], 9 | }, 10 | env: { 11 | browser: true, 12 | es2017: true, 13 | node: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: saneef 2 | ko_fi: saneef 3 | patreon: saneef 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /CAs 4 | 5 | .eslintcache 6 | .DS_Store 7 | .netlify 8 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.0.0 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | svelteSortOrder: options-styles-scripts-markup 3 | svelteStrictMode: true 4 | plugins: 5 | - prettier-plugin-svelte 6 | overrides: 7 | - files: 8 | - "*.svelte" 9 | options: 10 | parser: svelte 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Saneef Ansari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # color × color 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/42aa179f-b564-4251-85be-4bbfdfd501e5/deploy-status)](https://app.netlify.com/sites/color-color/deploys) 4 | 5 | Color-color is a tool to generate color shades. Supports [Okhsl](https://bottosson.github.io/posts/colorpicker/), [HSLuv](https://www.hsluv.org) and HSL color spaces. You can generate more than one set of shades, and compare side by side. Bring in your reference colors to tune-in the color schemes. 6 | 7 | [Designing accessible color systems](https://stripe.com/au/blog/accessible-color-systems) article by Stripe, and [Colorbox](https://www.colorbox.io) by Lyft Design inspired us. 8 | 9 | ## Get started 10 | 11 | Install the dependencies... 12 | 13 | ```bash 14 | npm install # Or `pnpm install` or `yarn install` 15 | ``` 16 | 17 | ...then start development server: 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | Navigate to [localhost:5173](http://localhost:5173). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 24 | 25 | ## Building and running in production mode 26 | 27 | To create an optimised version of the app: 28 | 29 | ```bash 30 | npm run build 31 | ``` 32 | 33 | ## Credits 34 | 35 | - Entypo pictograms by Daniel Bruce — [www.entypo.com](http://www.entypo.com/) 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%- title %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "\\.js$": "babel-jest", 4 | "\\.svelte$": "svelte-jest", 5 | }, 6 | moduleFileExtensions: ["js", "json", "svelte"], 7 | }; 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module": "esnext", 6 | /** 7 | * svelte-preprocess cannot figure out whether you have 8 | * a value or a type, so tell TypeScript to enforce using 9 | * `import type` instead of `import` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | * To have warnings / errors of the Svelte compiler at the 16 | * correct position, enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | /** 24 | * Typecheck JS in `.svelte` and `.js` files by default. 25 | * Disable this if you'd like to use dynamic types. 26 | */ 27 | "checkJs": true 28 | }, 29 | /** 30 | * Use global.d.ts instead of compilerOptions.types 31 | * to avoid limiting type declarations. 32 | */ 33 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] 34 | } 35 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | prettier: 4 | glob: "*.{md,json,css,html}" 5 | run: pnpm prettier --write {staged_files} 6 | stage_fixed: true 7 | eslint: 8 | glob: "*.{js,mjs,cjs,svelte}" 9 | run: pnpm eslint --fix {staged_files} 10 | stage_fixed: true 11 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | framework = "#custom" 3 | publish = "dist/" 4 | command = "pnpm run release" 5 | 6 | [[redirects]] 7 | from = "/p.js" 8 | to = "https://plausible.io/js/plausible.js" 9 | status = 200 10 | [[redirects]] 11 | from = "/api/event" 12 | to = "https://plausible.io/api/event" 13 | status = 200 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-color-app", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "build:app:": "vite build", 9 | "build": "NODE_ENV=production run-s clean build:*", 10 | "dev": "vite", 11 | "release:sentry": "./release-on-sentry.sh", 12 | "release": "run-s build release:*", 13 | "serve": "vite preview", 14 | "test": "ava" 15 | }, 16 | "devDependencies": { 17 | "@sveltejs/vite-plugin-svelte": "^3.1.0", 18 | "@vitejs/plugin-legacy": "^5.3.2", 19 | "ava": "^6.1.2", 20 | "eslint": "^8.19.0", 21 | "eslint-config-prettier": "^9.1.0", 22 | "eslint-plugin-svelte": "^2.38.0", 23 | "lefthook": "^1.6.10", 24 | "npm-run-all": "^4.1.5", 25 | "prettier": "^3.2.5", 26 | "prettier-plugin-svelte": "^3.2.3", 27 | "svelte": "^4.2.15", 28 | "vite": "5.2.10", 29 | "vite-plugin-html": "^3.2.2", 30 | "workbox-build": "^7.1.0", 31 | "workbox-window": "^7.1.0" 32 | }, 33 | "dependencies": { 34 | "@borderless/base64": "^1.0.1", 35 | "@sentry/browser": "^7.112.2", 36 | "@sentry/cli": "^2.31.0", 37 | "@xstate/fsm": "^2.1.0", 38 | "autoprefixer": "^10.4.19", 39 | "bezier-easing": "^2.1.0", 40 | "colorjs.io": "github:color-js/color.js#4a962dd", 41 | "cssnano": "^7.0.1", 42 | "d3-scale": "^4.0.2", 43 | "d3-shape": "^3.2.0", 44 | "debounce": "^2.0.0", 45 | "pako": "^2.1.0", 46 | "postcss-import": "^16.1.0", 47 | "postcss-nesting": "^12.1.2", 48 | "rimraf": "^5.0.5", 49 | "tailwindcss": "^2.2.19", 50 | "vite-plugin-pwa": "^0.19.8" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // const purgecss = require("@fullhuman/postcss-purgecss")({ 2 | // content: ["./src/**/*.svelte"], 3 | // safelist: [/svelte-/], 4 | // defaultExtractor: (content) => content.match(/[A-Za-z0-9-_:/]+/g) || [], 5 | // }); 6 | 7 | const production = !process.env.ROLLUP_WATCH; 8 | 9 | module.exports = { 10 | plugins: [ 11 | require("postcss-import")(), 12 | require("tailwindcss"), 13 | require("autoprefixer"), 14 | require("postcss-nesting"), 15 | ...(production 16 | ? [ 17 | // purgecss, 18 | require("cssnano")({ 19 | preset: [ 20 | "default", 21 | { 22 | discardComments: { 23 | removeAll: true, 24 | }, 25 | }, 26 | ], 27 | }), 28 | ] 29 | : []), 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | cache-control: max-age=0 3 | cache-control: no-cache 4 | cache-control: no-store 5 | cache-control: must-revalidate 6 | Permissions-Policy: interest-cohort=() 7 | 8 | /*.png 9 | cache-control: public, max-age=86400 10 | 11 | /*.ico 12 | cache-control: public, max-age=86400 13 | 14 | /*.css 15 | cache-control: public, max-age=31536000 16 | 17 | /*.js 18 | cache-control: public, max-age=31536000 19 | 20 | /sw.js 21 | cache-control: public, max-age=0, must-revalidate 22 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saneef/color-color/35887aff5c8279f0a8c0f64af4f387031c2bb009/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saneef/color-color/35887aff5c8279f0a8c0f64af4f387031c2bb009/public/favicon.ico -------------------------------------------------------------------------------- /public/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saneef/color-color/35887aff5c8279f0a8c0f64af4f387031c2bb009/public/screenshot.jpg -------------------------------------------------------------------------------- /release-on-sentry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RELEASE_NAME=$(sentry-cli releases propose-version) 3 | 4 | echo "📦 Creating release: "$RELEASE_NAME 5 | npx sentry-cli releases new $RELEASE_NAME 6 | npx sentry-cli releases files $RELEASE_NAME upload-sourcemaps dist/ --rewrite 7 | # npx sentry-cli releases set-commits --auto $RELEASE_NAME 8 | npx sentry-cli releases finalize $RELEASE_NAME 9 | -------------------------------------------------------------------------------- /site-info.js: -------------------------------------------------------------------------------- 1 | const siteUrl = "https://colorcolor.in"; 2 | 3 | const info = { 4 | title: "color × color", 5 | description: 6 | "Color-color is a tool to create accessible color systems for UIs. You can use Okhsl or HSLuv color spaces to create perceptually uniform colors.", 7 | siteUrl, 8 | imagePath: `${siteUrl}/screenshot.jpg`, 9 | domain: process.env.PLAUSIBLE_DOMAINS || "", 10 | }; 11 | 12 | export default info; 13 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 55 | 56 | 57 |
58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | 69 | 70 |
71 | 72 | {#if $shareDialog.open} 73 | 74 | 75 | {/if} 76 | -------------------------------------------------------------------------------- /src/Checkbox.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | 21 | -------------------------------------------------------------------------------- /src/ColorSpaceSelector.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 20 | 25 |

26 | Learn about difference between 27 | 31 | HSL, HSLuv, and Okhsl. 32 | 33 |

34 |
35 | -------------------------------------------------------------------------------- /src/ColorsPlot.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | 76 | 77 |
78 |

79 | {title} 80 | {#if subtitle}· {subtitle}{/if} 81 |

82 |
83 |
84 | 89 | 90 | 91 | {#each yTicks as tick} 92 | 93 | 94 | 95 | {/each} 96 | 97 | 98 | 99 | {#each data as s, i (i)} 100 | 101 | 102 | 103 | {/each} 104 | 105 | 106 | 107 | {#each data as s, i (i)} 108 | 115 | {s.x} · {title}: {s.y.toFixed(2)} 116 | 117 | {/each} 118 | 119 | 120 |
121 |
122 | {yDomain[1]} 123 | {yDomain[0]} 124 |
125 |
126 |
127 | -------------------------------------------------------------------------------- /src/ControlGroup.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 |
22 | {#if title !== null} 23 |

{title}

24 | {/if} 25 | 26 |
27 | -------------------------------------------------------------------------------- /src/ControlPoint.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {#if variant === "square"} 7 | 8 | 16 | {:else} 17 | 18 | 25 | {/if} 26 | -------------------------------------------------------------------------------- /src/CopyOnClick.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | 62 | 63 | 74 | -------------------------------------------------------------------------------- /src/CubicBezierEditor.svelte: -------------------------------------------------------------------------------- 1 | 70 | 71 | 211 | 212 | 216 |
220 |
225 | 226 | f(t) 227 | 232 | t 233 | 234 | 235 | 236 | {#each ticks as tick} 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | {/each} 245 | 246 | 247 | 253 | 259 | 260 | 264 | 269 | 270 | 274 | 275 | 276 | 277 |
278 |
279 | 286 |
287 | 288 | 289 | 290 | 291 | 292 |
293 |
294 |
295 |
296 | 303 |
304 | 308 | 309 | 310 | 311 | 312 |
313 |
314 |
315 |
316 | -------------------------------------------------------------------------------- /src/DownloadAsSvg.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | {#if base64EncodedData} 26 |
27 | 28 | Download as SVG 29 | 30 |
31 | {/if} 32 | -------------------------------------------------------------------------------- /src/EaseControl.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | 27 | 28 | {#if showCurve} 29 |
30 | 31 |
32 | {/if} 33 | {#if showCurve} 38 | ↑ Hide 39 | {:else}↓ Show{/if} 40 | Curve 42 | -------------------------------------------------------------------------------- /src/EaseSelectOptions.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#each $config.eases as group (group.title)} 11 | 12 | {#each group.eases as ease (ease.value)} 13 | 14 | {/each} 15 | 16 | {/each} 17 | {#if !alias} 18 | 19 | {/if} 20 | -------------------------------------------------------------------------------- /src/Icon.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 26 | {#if title} 27 | {title} 28 | {/if} 29 | {#if icon === "chevron-small-down"} 30 | 32 | {/if} 33 | 34 | {#if icon === "clone"} 35 | 36 | 37 | 38 | 39 | {/if} 40 | 41 | {#if icon === "x"} 42 | 44 | {/if} 45 | {#if icon === "ko-fi"} 46 | 47 | 57 | 63 | 64 | {/if} 65 | 66 | -------------------------------------------------------------------------------- /src/OverlayKnobs.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Palette.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | 70 | 71 |
72 |
73 | 81 | {#if clonable} 82 | 86 | {/if} 87 | {#if removable} 88 | 92 | {/if} 93 |
94 | 95 |
96 | -------------------------------------------------------------------------------- /src/PaletteKnobs.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 |
21 | 22 | 23 | 31 | 39 | 40 | 44 | 45 | 49 | 50 | 51 | 52 | 60 | 61 | 69 | 70 | 74 | 81 | 82 | 83 | 84 | 92 | 93 | 101 | 105 | 106 |
107 | -------------------------------------------------------------------------------- /src/PaletteSelector.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 57 | -------------------------------------------------------------------------------- /src/Palettes.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 | 77 | 78 |
82 | {#each $palettes as palette, j (j)} 83 | 98 | {#each palette as color, i (i)} 99 | 111 | {/each} 112 | 113 | {/each} 114 |
115 |
 
116 | {#each $palettes[0] as color, i (i)} 117 | 129 | {/each} 130 |
131 |
132 | -------------------------------------------------------------------------------- /src/Pitch.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 41 | 42 |
43 |

Support color × color

44 |

45 | All contribution helps with hosting and other operational costs of this 46 | valuable tool. 47 |

48 |

49 | Donate 50 |

51 |
52 | -------------------------------------------------------------------------------- /src/Plots.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 53 | 54 |
55 |
56 |
57 | 58 |
59 | 60 | 61 | 71 | 72 | 73 | 83 | 84 | 85 | 92 | 93 |
94 |
95 |

{currentSwatchId}

96 | 97 | 107 | 108 | 109 | 119 | 120 | 121 | 122 | 133 | 134 |
135 |
136 | -------------------------------------------------------------------------------- /src/RadioGroup.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 |
18 | {#each radios as radio, i (i)} 19 | 20 | 26 | {/each} 27 |
28 | -------------------------------------------------------------------------------- /src/RangeField.svelte: -------------------------------------------------------------------------------- 1 | 74 | 75 | 122 | 123 |
124 | {#if label}{/if} 125 |
126 |
127 | 138 |
139 |
140 | 149 |
150 |
151 |
152 | -------------------------------------------------------------------------------- /src/ReferenceColorFieldGroup.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 25 | 26 | 33 | 34 | {#if $refColors.length} 35 |
    36 | {#each $refColors as c, i (i)} 37 |
  • 38 | 39 | {c.hex} 40 | {c.string} 41 |
  • 42 | {/each} 43 |
44 | {/if} 45 |
46 | -------------------------------------------------------------------------------- /src/Scrim.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 |
18 | -------------------------------------------------------------------------------- /src/SelectField.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 37 | 38 |
39 | 40 |
41 | 44 |
45 | 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /src/ShareDialog.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | 83 | 84 |
85 |
86 |
87 |

Share

88 | 92 |
93 |
94 | 101 | 109 | 110 |
111 |
112 |
113 | -------------------------------------------------------------------------------- /src/SiteFooter.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 30 | 31 | 46 | -------------------------------------------------------------------------------- /src/SiteHeader.svelte: -------------------------------------------------------------------------------- 1 | 66 | 67 | 74 | 75 |
76 |

77 | color color 78 |

79 |
80 | 90 | 97 |
98 |
99 | -------------------------------------------------------------------------------- /src/StepsKnob.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /src/Swatch.svelte: -------------------------------------------------------------------------------- 1 | 52 | 53 | 65 | 66 |
72 | 73 | Select 74 | 75 | {#if $settings.overlayHex} 76 | 77 | {hexCode} 78 | 79 | {/if} 80 | {#if refColor} 81 |
82 | 83 |
84 | {/if} 85 | {#if $settings.overlayContrast} 86 | {blackContrast.toFixed(2)}b 87 | {whiteContrast.toFixed(2)}w 88 | {/if} 89 |
90 | -------------------------------------------------------------------------------- /src/TextField.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | 36 |
37 | {#if label}{/if} 38 | {#if multiline} 39 | 46 | {:else} 47 | 56 | {/if} 57 | {#if legend} 58 |

{legend}

59 | {/if} 60 |
61 | -------------------------------------------------------------------------------- /src/TinySwatch.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/VhProvider.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/XYInputField.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 50 |
51 | 52 |
53 | 54 | 63 |
64 |
65 | 66 | 73 |
74 |
75 | -------------------------------------------------------------------------------- /src/base.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | @apply w-full h-full; 4 | } 5 | 6 | html { 7 | @apply text-gray-900 text-base; 8 | } 9 | 10 | body { 11 | @apply bg-gray-200; 12 | } 13 | 14 | a { 15 | @apply underline; 16 | } 17 | 18 | a:hover, 19 | a:focus, 20 | a:active { 21 | @apply text-black; 22 | } 23 | 24 | /* https://gist.github.com/zachleat/a7393810acf7890e6bef6a34eaa7b78c#gistcomment-3426446 */ 25 | @media (prefers-reduced-motion: no-preference) { 26 | html { 27 | scroll-behavior: smooth; 28 | } 29 | } 30 | 31 | /* Remove all animations and transitions for people that prefer not to see them */ 32 | @media (prefers-reduced-motion: reduce) { 33 | * { 34 | transition-duration: 0.01ms !important; 35 | animation-duration: 0.01ms !important; 36 | animation-iteration-count: 1 !important; 37 | scroll-behavior: auto !important; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @import "tailwindcss/base"; 3 | @import "tailwindcss/components"; 4 | 5 | @import "./base.css"; 6 | /* purgecss end ignore */ 7 | 8 | @import "tailwindcss/utilities"; 9 | -------------------------------------------------------------------------------- /src/lib/array.js: -------------------------------------------------------------------------------- 1 | export const insert = (arr, index, ...newItems) => [ 2 | // part of the array before the specified index 3 | ...arr.slice(0, index), 4 | // inserted items 5 | ...newItems, 6 | // part of the array after the specified index 7 | ...arr.slice(index), 8 | ]; 9 | -------------------------------------------------------------------------------- /src/lib/clipboard.js: -------------------------------------------------------------------------------- 1 | export function copyToClipboard( 2 | text, 3 | { onSuccess = () => {}, onFailure = () => {} } = {} 4 | ) { 5 | navigator.clipboard.writeText(text).then( 6 | function () { 7 | onSuccess(); 8 | }, 9 | function (err) { 10 | onFailure(err); 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/colors.js: -------------------------------------------------------------------------------- 1 | import { 2 | ColorSpace, 3 | HSL, 4 | HSLuv, 5 | LCH, 6 | OKLCH, 7 | Okhsl, 8 | distance as computeDistance, 9 | to as convert, 10 | contrast as getContrast, 11 | getLuminance as luminance, 12 | parse, 13 | sRGB, 14 | serialize, 15 | } from "colorjs.io/fn"; 16 | 17 | ColorSpace.register(sRGB); 18 | ColorSpace.register(LCH); 19 | ColorSpace.register(HSL); 20 | ColorSpace.register(OKLCH); 21 | ColorSpace.register(Okhsl); 22 | ColorSpace.register(HSLuv); 23 | 24 | export function createColor(cssString) { 25 | return parse(cssString); 26 | } 27 | 28 | export function createColorByHSL(h, s, l, colorSpace = "hsl") { 29 | const hsl = colorSpace === "okhsl" ? [h, s / 100, l / 100] : [h, s, l]; 30 | const colorObj = { 31 | space: colorSpace, 32 | coords: /**@type [number, number, number] */ (hsl), 33 | }; 34 | 35 | return convert(colorObj, colorSpace); 36 | } 37 | 38 | export function colorToString(color, format = "hex", colorSpace = "srgb") { 39 | const c = convert(color, colorSpace); 40 | return serialize(c, { format }); 41 | } 42 | 43 | export function getChroma(color) { 44 | const _c = convert(color, "lch"); 45 | const [_, c] = _c.coords; 46 | return c; 47 | } 48 | 49 | export function getLuminance(color) { 50 | return luminance(color); 51 | } 52 | 53 | export function contrast(color1, color2, algorithm = "WCAG21") { 54 | return getContrast(color1, color2, algorithm); 55 | } 56 | 57 | export function distance(color1, color2, colorSpace = "srgb") { 58 | return computeDistance(color1, color2, colorSpace); 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/colors.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | 3 | import { getLuminance } from "colorjs.io/fn"; 4 | import { 5 | colorToString, 6 | contrast, 7 | createColor, 8 | createColorByHSL, 9 | distance, 10 | getChroma, 11 | } from "./colors.js"; 12 | 13 | const isCloseTo = (a, b) => Math.abs(a - b) < 0.005; 14 | 15 | test("createColor from HEX", (t) => { 16 | t.snapshot(createColor("#c0ffee")); 17 | }); 18 | 19 | test("createColor from CSS HSL string", (t) => { 20 | t.snapshot(createColor("HSL(200 53% 79%)")); 21 | }); 22 | 23 | test("createColorByHSL into HSL", (t) => { 24 | const c = createColorByHSL(200, 53, 79); 25 | t.is(c.space.id, "srgb"); 26 | t.is(colorToString(c, "hex"), "#add3e6"); 27 | }); 28 | 29 | test("createColorByHSL into HSLuv", (t) => { 30 | const c = createColorByHSL(200, 53, 79, "hsluv"); 31 | 32 | t.is(c.space.id, "hsluv"); 33 | t.is( 34 | colorToString(c, "hsluv", "hsluv"), 35 | "color(--hsluv 224.52 48.074% 82.453%)" 36 | ); 37 | }); 38 | 39 | test("createColorByHSL into Okhsl", (t) => { 40 | const c = createColorByHSL(200, 50, 50, "okhsl"); 41 | 42 | t.is(c.space.id, "okhsl"); 43 | t.is( 44 | colorToString(c, "okhsl", "okhsl"), 45 | "color(--okhsl 228.69 0.23501% 1.8408%)" 46 | ); 47 | }); 48 | 49 | test("colorToString: get HEX string", (t) => { 50 | const color = createColor("HSL(200 53% 79%)"); 51 | t.is(colorToString(color, "hex"), "#add3e6"); 52 | }); 53 | 54 | test("colorToString: get as HEX string", (t) => { 55 | const color = createColor("HSL(200 53% 79%)"); 56 | t.is(colorToString(color, "hex"), "#add3e6"); 57 | }); 58 | 59 | test("colorToString: get a hsl string", (t) => { 60 | const color = createColor("#add3e6"); 61 | t.is(colorToString(color, "hsl", "hsl"), "hsl(200 53.271% 79.02%)"); 62 | }); 63 | 64 | test("getChroma from color", (t) => { 65 | const color = createColor("#52ac64"); 66 | t.truthy(isCloseTo(getChroma(color), 49.1972)); 67 | }); 68 | 69 | test("getLuminance from color", (t) => { 70 | const color = createColor("#cef4d4"); 71 | t.truthy(isCloseTo(getLuminance(color), 0.82575)); 72 | }); 73 | 74 | test("get contrast with white", (t) => { 75 | const color1 = createColor("#9ae7a9"); 76 | const color2 = createColor("#fff"); 77 | 78 | t.truthy(isCloseTo(contrast(color1, color2), 1.46)); 79 | }); 80 | 81 | test("get contrast with black", (t) => { 82 | const color1 = createColor("#9ae7a9"); 83 | const color2 = createColor("#000"); 84 | 85 | t.truthy(isCloseTo(contrast(color1, color2), 14.38)); 86 | }); 87 | 88 | test("get distance with black", (t) => { 89 | const color1 = createColor("#9ae7a9"); 90 | const color2 = createColor("#000"); 91 | t.truthy(isCloseTo(distance(color1, color2), 1.2745)); 92 | }); 93 | 94 | test("get distance with white", (t) => { 95 | const color1 = createColor("#9ae7a9"); 96 | const color2 = createColor("#fff"); 97 | t.truthy(isCloseTo(distance(color1, color2), 0.528)); 98 | }); 99 | -------------------------------------------------------------------------------- /src/lib/colors.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/lib/colors.test.js` 2 | 3 | The actual snapshot is saved in `colors.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## createColor from HEX 8 | 9 | > Snapshot 1 10 | 11 | { 12 | alpha: 1, 13 | coords: [ 14 | 0.7529411764705882, 15 | 1, 16 | 0.9333333333333333, 17 | ], 18 | spaceId: 'srgb', 19 | } 20 | 21 | ## createColor from CSS HSL string 22 | 23 | > Snapshot 1 24 | 25 | { 26 | alpha: 1, 27 | coords: [ 28 | Number { 29 | 200 30 | --- 31 | raw: '200', 32 | type: '', 33 | }, 34 | 53, 35 | 79, 36 | ], 37 | spaceId: 'hsl', 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/colors.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saneef/color-color/35887aff5c8279f0a8c0f64af4f387031c2bb009/src/lib/colors.test.js.snap -------------------------------------------------------------------------------- /src/lib/eases.js: -------------------------------------------------------------------------------- 1 | // Values are function names from "svelte/easing" 2 | export const eases = [ 3 | { 4 | title: "Linear", 5 | eases: [ 6 | { 7 | title: "Linear", 8 | value: ".25,.25,.75,.75", 9 | alias: "linear", 10 | }, 11 | ], 12 | }, 13 | { 14 | title: "Sine", 15 | eases: [ 16 | { 17 | title: "Ease in sine", 18 | value: ".12,0,.39,0", 19 | alias: "sineIn", 20 | }, 21 | { 22 | title: "Ease in-out sine", 23 | value: ".37,0,.63,1", 24 | alias: "sineInOut", 25 | }, 26 | { 27 | title: "Ease out sine", 28 | value: ".61,1,.88,1", 29 | alias: "sineOut", 30 | }, 31 | ], 32 | }, 33 | { 34 | title: "Quadratic", 35 | eases: [ 36 | { 37 | title: "Ease in quad", 38 | value: ".11,0,.5,0", 39 | alias: "quadIn", 40 | }, 41 | { 42 | title: "Ease in-out quad", 43 | value: ".45,0,.55,1", 44 | alias: "quadInOut", 45 | }, 46 | { 47 | title: "Ease out quad", 48 | value: ".5,1,.89,1", 49 | alias: "quadOut", 50 | }, 51 | ], 52 | }, 53 | { 54 | title: "Cubic", 55 | eases: [ 56 | { 57 | title: "Ease in cubic", 58 | value: ".32,0,.67,0", 59 | alias: "cubicIn", 60 | }, 61 | { 62 | title: "Ease in-out cubic", 63 | value: ".65,0,.35,1", 64 | alias: "cubicInOut", 65 | }, 66 | { 67 | title: "Ease out cubic", 68 | value: ".33,1,.68,1", 69 | alias: "cubicOut", 70 | }, 71 | ], 72 | }, 73 | { 74 | title: "Quartic", 75 | eases: [ 76 | { 77 | title: "Ease in quart", 78 | value: ".5,0,.75,0", 79 | alias: "quartIn", 80 | }, 81 | { 82 | title: "Ease in-out quart", 83 | value: ".76,0,.24,1", 84 | alias: "quartInOut", 85 | }, 86 | { 87 | title: "Ease out quart", 88 | value: ".25,1,.5,1", 89 | alias: "quartOut", 90 | }, 91 | ], 92 | }, 93 | { 94 | title: "Quintic", 95 | eases: [ 96 | { 97 | title: "Ease in quint", 98 | value: ".64,0,.78,0", 99 | alias: "quintIn", 100 | }, 101 | { 102 | title: "Ease in-out quint", 103 | value: ".83,0,.17,1", 104 | alias: "quintInOut", 105 | }, 106 | { 107 | title: "Ease out quint", 108 | value: ".22,1,.36,1", 109 | alias: "quintOut", 110 | }, 111 | ], 112 | }, 113 | { 114 | title: "Exponential", 115 | eases: [ 116 | { 117 | title: "Ease in expo", 118 | value: ".7,0,.84,0", 119 | alias: "expoIn", 120 | }, 121 | { 122 | title: "Ease in-out expo", 123 | value: ".87,0,.13,1", 124 | alias: "expoInOut", 125 | }, 126 | { 127 | title: "Ease out expo", 128 | value: ".16,1,.3,1", 129 | alias: "expoOut", 130 | }, 131 | ], 132 | }, 133 | { 134 | title: "Circular", 135 | eases: [ 136 | { 137 | title: "Ease in circ", 138 | value: ".55,0,1,.45", 139 | alias: "circIn", 140 | }, 141 | { 142 | title: "Ease in-out circ", 143 | value: ".85,0,.15,1", 144 | alias: "circInOut", 145 | }, 146 | { 147 | title: "Ease out circ", 148 | value: "0,.55,.45,1", 149 | alias: "circOut", 150 | }, 151 | ], 152 | }, 153 | ]; 154 | 155 | export const getBezierEasingByAlias = (alias) => { 156 | const ungroupedEases = eases.reduce((acc, cur) => [...acc, ...cur.eases], []); 157 | 158 | const ease = ungroupedEases.find((e) => e.alias === alias); 159 | 160 | return ease && ease.value; 161 | }; 162 | 163 | export const getAliasByBezierEasing = (value) => { 164 | const ungroupedEases = eases.reduce((acc, cur) => [...acc, ...cur.eases], []); 165 | 166 | const ease = ungroupedEases.find((e) => e.value === value); 167 | 168 | return ease && ease.alias; 169 | }; 170 | 171 | /** 172 | * Converts a string Bezier definition to a tuple 173 | * 174 | * @param {string} str 175 | * @return {[number,number,number,number]} 176 | */ 177 | export const stringToCubicBezierParams = (str) => { 178 | const params = str.split(","); 179 | const numericParams = params.map((p) => +p.trim()).filter((n) => !isNaN(n)); 180 | 181 | if (numericParams.length === 4) { 182 | return /** @type {[number,number,number,number]} */ (numericParams); 183 | } 184 | }; 185 | 186 | const numberToString = (n) => { 187 | let str = Number.isInteger(n) ? n.toString() : n.toFixed(2); 188 | if (str.indexOf("0") === 0) { 189 | str = str.slice(1); 190 | } 191 | 192 | if (str[str.length - 1] === "0") { 193 | str = str.slice(0, -1); 194 | } 195 | 196 | return str; 197 | }; 198 | 199 | export const cubicBezierParamsToString = (params) => { 200 | const cleanedParams = params.filter((p) => !isNaN(p)); 201 | 202 | if (cleanedParams.length === 4) { 203 | return cleanedParams.map(numberToString).join(","); 204 | } 205 | }; 206 | -------------------------------------------------------------------------------- /src/lib/eases.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { 3 | getBezierEasingByAlias, 4 | getAliasByBezierEasing, 5 | stringToCubicBezierParams, 6 | cubicBezierParamsToString, 7 | } from "./eases.js"; 8 | 9 | test("getBezierEasingByAlias: should get cubic bezier value for 'circInOut'", (t) => { 10 | t.truthy(getBezierEasingByAlias("circInOut") === ".85,0,.15,1"); 11 | }); 12 | 13 | test("getBezierEasingByAlias: should not get cubic bezier value for 'blah'", (t) => { 14 | t.truthy(getBezierEasingByAlias("blah") === undefined); 15 | }); 16 | 17 | test("getAliasByBezierEasing: should get cubic bezier alias for '.85,0,.15,1'", (t) => { 18 | t.truthy(getAliasByBezierEasing(".85,0,.15,1") === "circInOut"); 19 | }); 20 | 21 | test("getAliasByBezierEasing: should get cubic bezier alias for '.85,0,.15,1.5'", (t) => { 22 | t.truthy(getAliasByBezierEasing(".85,0,.15,1.5") === undefined); 23 | }); 24 | 25 | test("should parse", (t) => { 26 | t.deepEqual(stringToCubicBezierParams(".2,.2,.2,.2"), [0.2, 0.2, 0.2, 0.2]); 27 | }); 28 | 29 | test("stringToCubicBezierParams: should not parse", (t) => { 30 | t.truthy(stringToCubicBezierParams(".2,.2,.2") === undefined); 31 | }); 32 | 33 | test("stringToCubicBezierParams: should not parse this too", (t) => { 34 | t.truthy(stringToCubicBezierParams(".2,.2,.2,a") === undefined); 35 | }); 36 | 37 | test("cubicBezierParamsToString: should encode", (t) => { 38 | t.truthy(cubicBezierParamsToString([1, 0.2, 0.33, 0.2]) === "1,.2,.33,.2"); 39 | }); 40 | 41 | test("cubicBezierParamsToString: should not encode", (t) => { 42 | t.truthy(cubicBezierParamsToString([0.2, 0.2, 0.2]) === undefined); 43 | }); 44 | 45 | test("cubicBezierParamsToString: should not encode this too", (t) => { 46 | t.truthy(cubicBezierParamsToString([0.2, 0.2, 0.2, "a"]) === undefined); 47 | }); 48 | -------------------------------------------------------------------------------- /src/lib/math.js: -------------------------------------------------------------------------------- 1 | export function randomInt(min = 0, max = 1) { 2 | return Math.floor(Math.random() * (max - min + 1) + min); 3 | } 4 | 5 | export const linspace = (length) => 6 | Array.from({ length: length + 1 }).map((_, i) => (1 / length) * i); 7 | 8 | export const clamp = (min, val, max) => Math.max(min, Math.min(val, max)); 9 | 10 | export const lerp = (min, max, t) => { 11 | return min * (1 - t) + max * t; 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/string.js: -------------------------------------------------------------------------------- 1 | export const minify = (str) => 2 | str.replace(/(\r\n|\n|\r)/gm, "").replace(/[ ]{2,}/gm, ""); 3 | -------------------------------------------------------------------------------- /src/lib/svg.js: -------------------------------------------------------------------------------- 1 | import { minify } from "./string.js"; 2 | 3 | const generateSwatches = (parentId, p, swatchWidth, swatchHeight, padding) => 4 | Object.entries(p) 5 | .map( 6 | ([id, hex], i) => 7 | `` 10 | ) 11 | .join(""); 12 | 13 | const generateGroups = (p, swatchWidth, swatchHeight, padding) => 14 | Object.entries(p).reduce( 15 | (acc, [id, colors], i) => 16 | acc + 17 | ` 20 | ${generateSwatches(id, colors, swatchWidth, swatchHeight, padding)} 21 | `, 22 | "" 23 | ); 24 | 25 | export const jsonToSvg = ( 26 | json, 27 | swatchWidth = 80, 28 | swatchHeight = 60, 29 | padding = 8 30 | ) => { 31 | const numOfColors = Object.keys(json).length; 32 | const firstColorId = Object.keys(json)[0]; 33 | const numOfSwatches = Object.keys(json[firstColorId]).length; 34 | 35 | const canvasWidth = numOfColors * swatchWidth + (numOfColors - 1) * padding; 36 | const canvasHeight = 37 | numOfSwatches * swatchHeight + (numOfSwatches - 1) * padding; 38 | 39 | const svg = ` 40 | Colors 41 | Created with color-color 42 | 43 | ${generateGroups(json, swatchWidth, swatchHeight, padding)} 44 | 45 | 46 | `; 47 | 48 | return minify(svg); 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/url.js: -------------------------------------------------------------------------------- 1 | import pako from "pako"; 2 | import { 3 | encodeUrl as base64Encode, 4 | decode as base64Decode, 5 | } from "@borderless/base64"; 6 | 7 | export const getBaseUrl = () => { 8 | const getUrl = window.location; 9 | 10 | return ( 11 | getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.split("/")[1] 12 | ); 13 | }; 14 | 15 | const getQueryParams = () => { 16 | const search = window.location.search; 17 | const queryParams = new URLSearchParams(search); 18 | let obj = {}; 19 | 20 | for (const [key, value] of queryParams) { 21 | obj[key] = value; 22 | } 23 | 24 | return obj; 25 | }; 26 | 27 | const buildUrl = (encodedState) => { 28 | const url = new URL(getBaseUrl()); 29 | 30 | url.searchParams.append("s", encodedState); 31 | 32 | return url.href; 33 | }; 34 | 35 | const serializeState = (state) => 36 | base64Encode(pako.deflate(JSON.stringify(state))); 37 | 38 | const deserializeState = (string) => { 39 | let data = "{}"; 40 | 41 | try { 42 | const binaryData = base64Decode(string); 43 | 44 | data = pako.inflate(binaryData, { to: "string" }); 45 | } catch (e) { 46 | console.log("Unable extract state from URL"); 47 | } 48 | 49 | return JSON.parse(data); 50 | }; 51 | 52 | export const getStateFromUrl = () => { 53 | const { s } = getQueryParams(); 54 | 55 | if (s) { 56 | return deserializeState(s); 57 | } 58 | return {}; 59 | }; 60 | 61 | export const getStatefulUrl = (state) => { 62 | const encodedState = serializeState(state); 63 | 64 | return buildUrl(encodedState); 65 | }; 66 | -------------------------------------------------------------------------------- /src/machines/draggableMachine.js: -------------------------------------------------------------------------------- 1 | import { createMachine } from "@xstate/fsm"; 2 | 3 | const dragDropMachineCreator = (options) => 4 | createMachine( 5 | { 6 | initial: "idle", 7 | context: {}, 8 | states: { 9 | idle: { 10 | on: { 11 | mousedown: { 12 | actions: "assignPoint", 13 | target: "dragging", 14 | }, 15 | }, 16 | }, 17 | dragging: { 18 | // after: { 19 | // TIMEOUT: { 20 | // target: "idle", 21 | // actions: "resetPosition", 22 | // }, 23 | // }, 24 | on: { 25 | mousemove: { 26 | actions: "assignDelta", 27 | internal: false, 28 | }, 29 | mouseup: { 30 | target: "idle", 31 | }, 32 | "keyup.escape": { 33 | target: "idle", 34 | actions: "resetPosition", 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | options 41 | ); 42 | export default dragDropMachineCreator; 43 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/browser"; 2 | import App from "./App.svelte"; 3 | import { registerSW } from "virtual:pwa-register"; 4 | 5 | if (process.env.NODE_ENV === "production" && process.env.SENTRY_DSN) { 6 | Sentry.init({ 7 | dsn: process.env.SENTRY_DSN, 8 | allowUrls: [/https?:\/\/(www\.)?colorcolor\.in/], 9 | }); 10 | } 11 | 12 | const app = new App({ 13 | target: document.body, 14 | props: {}, 15 | }); 16 | 17 | registerSW(); 18 | 19 | export default app; 20 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import BezierEasing from "bezier-easing"; 2 | import { derived, readable, writable } from "svelte/store"; 3 | import { insert } from "./lib/array"; 4 | import { 5 | colorToString, 6 | contrast, 7 | createColor, 8 | createColorByHSL, 9 | distance, 10 | getChroma, 11 | getLuminance, 12 | } from "./lib/colors"; 13 | import { 14 | eases, 15 | getBezierEasingByAlias, 16 | stringToCubicBezierParams, 17 | } from "./lib/eases"; 18 | import { randomInt } from "./lib/math"; 19 | import { jsonToSvg } from "./lib/svg"; 20 | import { getStateFromUrl, getStatefulUrl } from "./lib/url"; 21 | 22 | const defaults = { 23 | steps: 9, 24 | saturationRate: 130, 25 | }; 26 | const maxNumOfPalettes = 6; 27 | const urlState = getStateFromUrl(); 28 | 29 | export const config = readable({ 30 | eases: eases, 31 | resolution: 0.25, 32 | limits: { 33 | hue: [0, 360], 34 | sat: [0, 100], 35 | lig: [0, 100], 36 | rate: [0, 200], 37 | }, 38 | }); 39 | 40 | const shareDialogStoreCreator = (config) => { 41 | const { subscribe, set, update } = writable(config); 42 | 43 | const openWithTriggerRect = (rect) => { 44 | update((state) => { 45 | return { 46 | ...state, 47 | open: true, 48 | rect, 49 | }; 50 | }); 51 | }; 52 | 53 | return { 54 | subscribe, 55 | set, 56 | update, 57 | openWithTriggerRect, 58 | }; 59 | }; 60 | 61 | export const shareDialog = shareDialogStoreCreator({ open: false }); 62 | 63 | export const settings = writable( 64 | Object.assign( 65 | {}, 66 | { 67 | overlayContrast: false, 68 | overlayHex: true, 69 | refColorsRaw: "", 70 | colorSpace: "okhsl", 71 | }, 72 | urlState.settings 73 | ) 74 | ); 75 | 76 | function createPaletteParams() { 77 | const { subscribe, set, update } = writable( 78 | Object.assign( 79 | {}, 80 | { 81 | steps: defaults.steps, 82 | paletteIndex: 0, 83 | swatchIndex: Math.floor(defaults.steps / 2), 84 | maxNumOfPalettes, 85 | params: [ 86 | { 87 | hue: { 88 | start: 16, 89 | end: 27, 90 | ease: getBezierEasingByAlias("quadIn"), 91 | interpolateHueOver360: false, 92 | }, 93 | sat: { 94 | start: 45, 95 | end: 88, 96 | ease: getBezierEasingByAlias("quadOut"), 97 | rate: defaults.saturationRate, 98 | }, 99 | lig: { 100 | start: 98.75, 101 | end: 12, 102 | ease: "0.4,0.64,0.6,0.91", 103 | }, 104 | }, 105 | { 106 | hue: { 107 | start: 150, 108 | end: 139, 109 | ease: getBezierEasingByAlias("quadIn"), 110 | interpolateHueOver360: false, 111 | }, 112 | sat: { 113 | start: 44, 114 | end: 81, 115 | ease: getBezierEasingByAlias("quadOut"), 116 | rate: defaults.saturationRate, 117 | }, 118 | lig: { 119 | start: 99, 120 | end: 12, 121 | ease: "0.51,0.93,0.89,1", 122 | }, 123 | }, 124 | { 125 | hue: { 126 | start: 235, 127 | end: 250, 128 | ease: getBezierEasingByAlias("quadIn"), 129 | interpolateHueOver360: false, 130 | }, 131 | sat: { 132 | start: 44, 133 | end: 81, 134 | ease: getBezierEasingByAlias("quadOut"), 135 | rate: 125, 136 | }, 137 | lig: { 138 | start: 99, 139 | end: 12, 140 | ease: getBezierEasingByAlias("quadOut"), 141 | }, 142 | }, 143 | ], 144 | }, 145 | urlState.paletteParams 146 | ) 147 | ); 148 | 149 | const removeByIndex = (index) => 150 | update((pp) => { 151 | if (pp.params.length > 1) { 152 | pp.params = pp.params.filter((_, i) => i !== index); 153 | if (pp.paletteIndex >= index) { 154 | pp.paletteIndex = Math.max(pp.paletteIndex - 1, 0); 155 | } 156 | } 157 | return pp; 158 | }); 159 | 160 | const add = () => 161 | update((pp) => { 162 | if (pp.params.length < maxNumOfPalettes) { 163 | const hueRange = 20; 164 | const hue = randomInt(0, 360 - hueRange); 165 | 166 | const param = { 167 | hue: { 168 | start: hue, 169 | end: hue + hueRange, 170 | ease: getBezierEasingByAlias("quadIn"), 171 | }, 172 | sat: { 173 | start: 60, 174 | end: 100, 175 | ease: getBezierEasingByAlias("quadOut"), 176 | rate: defaults.saturationRate, 177 | }, 178 | lig: { start: 100, end: 5, ease: getBezierEasingByAlias("quadOut") }, 179 | }; 180 | 181 | pp.paletteIndex = pp.params.length; 182 | pp.params = [...pp.params, param]; 183 | } 184 | 185 | return pp; 186 | }); 187 | 188 | const cloneByIndex = (index) => 189 | update((pp) => { 190 | if (pp.params.length < maxNumOfPalettes) { 191 | const nextParams = structuredClone(pp.params[index]); 192 | pp.params = insert(pp.params, index + 1, nextParams); 193 | } 194 | return pp; 195 | }); 196 | const checkAndSet = (obj) => { 197 | const { swatchIndex, steps } = obj; 198 | 199 | if (swatchIndex >= steps) { 200 | obj.swatchIndex = steps - 1; 201 | } 202 | 203 | set(obj); 204 | }; 205 | 206 | return { 207 | subscribe, 208 | set: checkAndSet, 209 | update, 210 | removeByIndex, 211 | add, 212 | cloneByIndex, 213 | }; 214 | } 215 | export const paletteParams = createPaletteParams(); 216 | 217 | const easeSteps = (easeFn, currentStep, totalStep) => 218 | easeFn(currentStep / totalStep) * currentStep; 219 | 220 | const staticColors = { 221 | white: createColorByHSL(0, 1, 1), 222 | black: createColorByHSL(0, 0, 0), 223 | }; 224 | 225 | export const palettes = derived( 226 | [paletteParams, settings], 227 | ([$paletteParams, $settings]) => { 228 | const steps = $paletteParams.steps; 229 | 230 | return $paletteParams.params.map((pal) => { 231 | const { hue, sat, lig } = pal; 232 | const interpolateHueOver360 = 233 | hue.interpolateHueOver360 && hue.start > hue.end; 234 | const hueEnd = interpolateHueOver360 ? 360 + hue.end : hue.end; 235 | 236 | const hUnit = (hueEnd - hue.start) / steps; 237 | const sUnit = (sat.end - sat.start) / steps; 238 | const lUnit = (lig.end - lig.start) / steps; 239 | 240 | const hueEaseFn = BezierEasing(...stringToCubicBezierParams(hue.ease)); 241 | const satEaseFn = BezierEasing(...stringToCubicBezierParams(sat.ease)); 242 | const ligEaseFn = BezierEasing(...stringToCubicBezierParams(lig.ease)); 243 | 244 | const swatches = Array.from({ length: steps }).map((_, i) => { 245 | let h = hue.start + easeSteps(hueEaseFn, i + 1, steps) * hUnit; 246 | if (interpolateHueOver360) { 247 | h = h % 360; 248 | } 249 | 250 | let s = sat.start + easeSteps(satEaseFn, i + 1, steps) * sUnit; 251 | s = Math.min(100, s * (sat.rate / 100)); 252 | 253 | const l = lig.start + easeSteps(ligEaseFn, i + 1, steps) * lUnit; 254 | 255 | const id = (i + 1) * (steps > 9 ? 10 : 100); 256 | const _color = createColorByHSL(h, s, l, $settings.colorSpace); 257 | const hex = colorToString(_color, "hex", "srgb"); 258 | const chroma = getChroma(_color); 259 | const luminance = getLuminance(_color); 260 | const whiteContrast = contrast(_color, staticColors.white); 261 | const blackContrast = contrast(_color, staticColors.black); 262 | const string = colorToString(_color, undefined, $settings.colorSpace); 263 | 264 | return { 265 | _color, 266 | id: id.toString(), 267 | h, 268 | s, 269 | l, 270 | hex, 271 | chroma, 272 | luminance, 273 | whiteContrast, 274 | blackContrast, 275 | string, 276 | }; 277 | }); 278 | 279 | return swatches; 280 | }); 281 | } 282 | ); 283 | 284 | export const swatchesGroupedById = derived([palettes], ([$palettes]) => { 285 | const groupedBySwatchId = $palettes 286 | .map((palette, i) => { 287 | return palette.map((swatch) => { 288 | const { id: swatchId, ...rest } = swatch; 289 | return { 290 | ...rest, 291 | paletteIndex: i, 292 | swatchId, 293 | }; 294 | }); 295 | }) 296 | .flat() 297 | .reduce( 298 | (acc, swatch) => ({ 299 | ...acc, 300 | [swatch.swatchId]: [ 301 | ...(acc[swatch.swatchId] ? acc[swatch.swatchId] : []), 302 | swatch, 303 | ], 304 | }), 305 | {} 306 | ); 307 | 308 | return Object.keys(groupedBySwatchId).reduce((acc, swatchId, i) => { 309 | acc[i] = groupedBySwatchId[swatchId]; 310 | return acc; 311 | }, []); 312 | }); 313 | 314 | const hexRe = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/; 315 | 316 | export const refColors = derived(settings, ($settings) => { 317 | return $settings.refColorsRaw 318 | .split(",") 319 | .map((s) => s.trim()) 320 | .filter((s) => s.match(hexRe) !== null) 321 | .map((hex) => { 322 | const _color = createColor(hex); 323 | return { 324 | _color, 325 | hex: colorToString(_color, "hex"), 326 | string: colorToString(_color, undefined, $settings.colorSpace), 327 | }; 328 | }); 329 | }); 330 | 331 | export const nearestRefColors = derived( 332 | [refColors, palettes], 333 | ([$refColors, $palettes]) => { 334 | const refs = $refColors.reduce((acc, { hex }) => { 335 | return { ...acc, [hex]: {} }; 336 | }, {}); 337 | 338 | $refColors.forEach(({ _color: _rcColor, hex: rcHex }) => { 339 | $palettes.forEach((p) => 340 | p.forEach((swatch) => { 341 | const { _color, hex } = swatch; 342 | const dist = distance(_rcColor, _color); 343 | if (refs[rcHex].hex === undefined || refs[rcHex].dist > dist) { 344 | refs[rcHex].hex = hex; 345 | refs[rcHex].dist = dist; 346 | } 347 | }) 348 | ); 349 | }); 350 | 351 | const matchedSwatches = Object.keys(refs).reduce((acc, key) => { 352 | const { hex } = refs[key]; 353 | 354 | return { 355 | [hex]: key, 356 | ...acc, 357 | }; 358 | }, {}); 359 | 360 | return matchedSwatches; 361 | } 362 | ); 363 | 364 | const groupPalettesByName = (palettes) => 365 | palettes.reduce((pacc, p, i) => { 366 | const palette = p.reduce((acc, s) => { 367 | return { ...acc, [s.id]: s.hex }; 368 | }, {}); 369 | return { ...pacc, [`color-${i + 1}`]: palette }; 370 | }, {}); 371 | 372 | export const shareState = derived( 373 | [settings, paletteParams, palettes], 374 | ([$settings, $paletteParams, $palettes]) => { 375 | const state = { 376 | paletteParams: $paletteParams, 377 | settings: $settings, 378 | }; 379 | 380 | const paletteGroup = groupPalettesByName($palettes); 381 | 382 | return { 383 | url: getStatefulUrl(state), 384 | json: JSON.stringify(paletteGroup, null, 2), 385 | svg: jsonToSvg(paletteGroup), 386 | }; 387 | } 388 | ); 389 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const isProduction = process.env.NODE_ENV === "production"; 2 | module.exports = { 3 | purge: { 4 | enabled: isProduction, 5 | content: ["./src/**/*.svelte"], 6 | safelist: [/svelte-/], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 2 | import legacy from "@vitejs/plugin-legacy"; 3 | import { defineConfig } from "vite"; 4 | import { createHtmlPlugin } from "vite-plugin-html"; 5 | import { VitePWA } from "vite-plugin-pwa"; 6 | 7 | import siteInfo from "./site-info.js"; 8 | 9 | export default defineConfig({ 10 | build: { 11 | sourcemap: true, 12 | }, 13 | plugins: [ 14 | createHtmlPlugin({ 15 | inject: { data: { ...siteInfo } }, 16 | }), 17 | svelte(), 18 | VitePWA({ 19 | registerType: "autoUpdate", 20 | manifest: { 21 | name: "color × color", 22 | short_name: "colorcolor", 23 | background_color: "#e5e7eb", 24 | theme_color: "#111827", 25 | }, 26 | }), 27 | legacy({ 28 | targets: ["defaults", "not IE 11"], 29 | }), 30 | ], 31 | }); 32 | --------------------------------------------------------------------------------