├── .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 | [](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 |
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 |
277 |
278 |
295 |
315 |
316 |
--------------------------------------------------------------------------------
/src/DownloadAsSvg.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
24 |
25 | {#if base64EncodedData}
26 |
31 | {/if}
32 |
--------------------------------------------------------------------------------
/src/EaseControl.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
24 |
25 |
26 |
27 |
28 | {#if showCurve}
29 |
30 |
31 |
32 | {/if}
33 | (showCurve = !showCurve)}"
37 | >{#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 |
16 | {/each}
17 | {#if !alias}
18 |
19 | {/if}
20 |
--------------------------------------------------------------------------------
/src/Icon.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
24 |
25 |
66 |
--------------------------------------------------------------------------------
/src/OverlayKnobs.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/Palette.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
70 |
71 |
72 |
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 |
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 |
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 |
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 |
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 = `
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 |
--------------------------------------------------------------------------------