├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── app.css
├── app.d.ts
├── app.html
├── assets
│ └── segg-bear.png
├── components
│ ├── bear.svelte
│ ├── button.svelte
│ ├── configuration-field.svelte
│ ├── curve.svelte
│ ├── edit-curve-input.svelte
│ ├── edit-curve-panel.svelte
│ ├── icons
│ │ ├── check.svelte
│ │ ├── chevron.svelte
│ │ ├── copy.svelte
│ │ ├── corner.svelte
│ │ ├── cross.svelte
│ │ ├── crosshair.svelte
│ │ ├── download.svelte
│ │ ├── eye-closed.svelte
│ │ ├── eye.svelte
│ │ ├── palette.svelte
│ │ ├── pencil.svelte
│ │ ├── plus.svelte
│ │ ├── reset.svelte
│ │ ├── sliders-horizontal.svelte
│ │ └── trash.svelte
│ ├── input.svelte
│ ├── minimal-button.svelte
│ ├── palette.svelte
│ ├── repeatable-row.svelte
│ ├── select.svelte
│ ├── small-button.svelte
│ └── toast.svelte
├── index.test.ts
├── lib
│ ├── clipboard.ts
│ ├── color.ts
│ ├── constants.ts
│ ├── params.ts
│ ├── svg.ts
│ ├── toaster.ts
│ ├── types.ts
│ └── utils.ts
└── routes
│ ├── +layout.svelte
│ └── +page.svelte
├── static
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── og.png
└── site.webmanifest
├── svelte.config.js
├── tailwind.config.js
├── tsconfig.json
├── vercel.json
└── vite.config.ts
/.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 | module.exports = {
2 | root: true,
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:svelte/recommended',
7 | 'prettier'
8 | ],
9 | parser: '@typescript-eslint/parser',
10 | plugins: ['@typescript-eslint'],
11 | parserOptions: {
12 | sourceType: 'module',
13 | ecmaVersion: 2020,
14 | extraFileExtensions: ['.svelte']
15 | },
16 | env: {
17 | browser: true,
18 | es2017: true,
19 | node: true
20 | },
21 | overrides: [
22 | {
23 | files: ['*.svelte'],
24 | parser: 'svelte-eslint-parser',
25 | parserOptions: {
26 | parser: '@typescript-eslint/parser'
27 | }
28 | }
29 | ]
30 | };
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 | vite.config.js.timestamp-*
10 | vite.config.ts.timestamp-*
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | resolution-mode=highest
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "pluginSearchDirs": ["."],
8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # create-svelte
2 |
3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
4 |
5 | ## Creating a project
6 |
7 | If you're seeing this, you've probably already done this step. Congrats!
8 |
9 | ```bash
10 | # create a new project in the current directory
11 | npm create svelte@latest
12 |
13 | # create a new project in my-app
14 | npm create svelte@latest my-app
15 | ```
16 |
17 | ## Developing
18 |
19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20 |
21 | ```bash
22 | npm run dev
23 |
24 | # or start the server and open the app in a new browser tab
25 | npm run dev -- --open
26 | ```
27 |
28 | ## Building
29 |
30 | To create a production version of your app:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | You can preview the production build with `npm run preview`.
37 |
38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "color-poc",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11 | "test": "vitest",
12 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
13 | "format": "prettier --plugin-search-dir . --write ."
14 | },
15 | "devDependencies": {
16 | "@melt-ui/svelte": "^0.50.0",
17 | "@significa/svelte-toaster": "^0.0.2",
18 | "@sveltejs/adapter-auto": "^2.0.0",
19 | "@sveltejs/kit": "^1.20.4",
20 | "@types/chroma-js": "^2.4.1",
21 | "@typescript-eslint/eslint-plugin": "^6.0.0",
22 | "@typescript-eslint/parser": "^6.0.0",
23 | "autoprefixer": "^10.4.15",
24 | "chroma-js": "^2.4.2",
25 | "clsx": "^2.0.0",
26 | "eslint": "^8.28.0",
27 | "eslint-config-prettier": "^8.5.0",
28 | "eslint-plugin-svelte": "^2.30.0",
29 | "phosphor-svelte": "^1.3.0",
30 | "postcss": "^8.4.30",
31 | "prettier": "^2.8.0",
32 | "prettier-plugin-svelte": "^2.10.1",
33 | "svelte": "^4.0.5",
34 | "svelte-check": "^3.4.3",
35 | "tailwind-merge": "^1.14.0",
36 | "tailwindcss": "^3.3.3",
37 | "tslib": "^2.4.1",
38 | "typescript": "^5.0.0",
39 | "vite": "^4.4.2",
40 | "vitest": "^0.34.0"
41 | },
42 | "type": "module"
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 | }
11 |
12 | export {};
13 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 | %sveltekit.head%
19 |
20 |
21 | %sveltekit.body%
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/assets/segg-bear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/src/assets/segg-bear.png
--------------------------------------------------------------------------------
/src/components/bear.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/button.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/configuration-field.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | {label}{#if value !== undefined}: {value} {/if}
20 |
21 | dispatch('reset')}>
22 |
23 |
24 |
25 |
26 |
27 |
28 | {#if description}
29 |
{description}
30 | {/if}
31 |
32 |
--------------------------------------------------------------------------------
/src/components/curve.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
34 |
37 | {#each labels as point}
38 | {point.toFixed(2)}
39 | {/each}
40 |
41 |
42 |
43 |
44 | {#each curve as point, index}
45 |
55 | {/each}
56 |
57 |
58 | {
64 | if (index === 0) return '';
65 | return `L ${index * (height / (curve.length - 1))} ${width - point * width}`;
66 | })
67 | .join(' ')}
68 | `}
69 | fill="none"
70 | stroke="black"
71 | stroke-width="2"
72 | />
73 |
74 |
75 |
83 |
84 |
85 |
86 | {#if $open}
87 |
94 |
95 |
96 | {/if}
97 |
--------------------------------------------------------------------------------
/src/components/edit-curve-input.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/edit-curve-panel.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 | Edit Curve
19 |
20 |
21 |
{
23 | dispatch('create');
24 | }}
25 | >
26 |
27 |
28 |
29 |
30 |
31 | {#each curve as _, i}
32 |
33 |
37 |
38 | {
41 | dispatch('delete', i);
42 | }}
43 | >
44 |
45 |
46 |
47 | {/each}
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/icons/check.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/chevron.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/copy.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/corner.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/cross.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/crosshair.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/download.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/eye-closed.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/eye.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/palette.svelte:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/pencil.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/plus.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/reset.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/sliders-horizontal.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/trash.svelte:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/components/input.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
34 |
--------------------------------------------------------------------------------
/src/components/minimal-button.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/palette.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
32 |
{
35 | copyToClipboard(color, { id: color });
36 | }}
37 | >
38 |
39 | {color}
40 |
41 |
42 |
{
45 | copyToClipboard(getOklch(...oklch), { id: `${color}-oklch` });
46 | }}
47 | >
48 |
49 | {getOklch(...oklch)}
50 |
51 |
52 |
53 | {
56 | if (palette) {
57 | downloadFile(
58 | JSON.stringify(paletteToJSON(`${color} Palette`, palette), null, 2),
59 | `${color}.json`
60 | );
61 | }
62 | }}
63 | >
64 |
65 | Download JSON
66 |
67 | {
70 | if (palette) {
71 | copyToClipboard(paletteToSvg(palette), { id: `${color}-svg` });
72 | }
73 | }}
74 | >
75 |
76 | Copy SVG
77 |
78 | {
80 | dispatch('delete', color);
81 | }}
82 | >
83 |
84 | Delete
85 |
86 |
87 |
88 |
89 | {#each palette as [l, c, h], i}
90 |
{
92 | copyToClipboard(chroma.oklch(l, c, h).hex(), { id: `${color}-hex-${i}` });
93 | }}
94 | class={cn(
95 | 'group flex-1 bg-[--square-color] transition-all outline-none border border-r-0 last:border-r lg:border-0',
96 | i === 0 && 'rounded-l-md',
97 | i === palette.length - 1 && 'rounded-r-md',
98 | colorIndex === i && 'scale-105 rounded-md shadow-lg border lg:border',
99 | l >= 0.6
100 | ? 'border-black/10 text-black/50 hover:text-black focus-visible:text-black'
101 | : 'border-white/10 text-white/50 hover:text-white focus-visible:text-white',
102 | 'h-24 lg:h-32'
103 | )}
104 | style="--square-color: {getOklch(l, c, h)}"
105 | >
106 |
109 |
110 | {i + 1}
111 |
112 |
113 |
116 | L{getL(l)}
117 | C{getC(c)}
118 | H{getH(h)}
119 |
120 |
121 |
122 | {/each}
123 |
124 |
125 |
--------------------------------------------------------------------------------
/src/components/repeatable-row.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
{label}
15 | {#if isDeletable}
16 |
dispatch('delete')}>
17 |
18 |
19 | {/if}
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/select.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
35 |
36 |
37 |
38 |
45 |
--------------------------------------------------------------------------------
/src/components/small-button.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
22 |
23 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/toast.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
22 | {#if toast.data.type === 'success'}
23 |
24 | {:else}
25 |
26 | {/if}
27 |
28 | {toast.data.message}
29 |
30 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 |
3 | describe('sum test', () => {
4 | it('adds 1 + 2 to equal 3', () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/lib/clipboard.ts:
--------------------------------------------------------------------------------
1 | import { toaster } from "./toaster";
2 |
3 | export const copyToClipboard = async (content: string, {
4 | id,
5 | success = 'Copied to clipboard!',
6 | error = 'Failed to copy to clipboard!',
7 | }: {
8 | id?: string
9 | success?: string
10 | error?: string
11 | } = {}) => {
12 | try {
13 | await navigator.clipboard.writeText(content);
14 |
15 | toaster({ type: 'success', message: success }, { id })
16 | } catch (err) {
17 | toaster({ type: 'error', message: error }, { id })
18 | }
19 | }
--------------------------------------------------------------------------------
/src/lib/color.ts:
--------------------------------------------------------------------------------
1 | import chroma from "chroma-js";
2 | import { findClosestNumber, resizeNumberArray } from "./utils";
3 | import type { Configuration, OKLCH } from "./types";
4 |
5 | export const generatePalette = (color: string, config: Configuration): { base: { oklch: OKLCH; index: number }; palette: OKLCH[] } => {
6 | if (!chroma.valid(color)) throw new Error('Invalid color');
7 |
8 | const { scales, chromaStepType, chromaStep, chromaMinimum, curve, overrides } = config
9 |
10 | const [l, c, h] = chroma(color).oklch();
11 |
12 | const curveArr = resizeNumberArray(curve, scales);
13 |
14 | const closest = findClosestNumber(curveArr, l); // find the closest number in the curve to the base color's lightness
15 | const shift = closest - l; // how much to shift the curve to match the base color's lightness
16 | const index = curveArr.indexOf(closest); // index of the closest number in the curve
17 |
18 | const palette: OKLCH[] = Array.from(Array(scales)).map((_, i) => {
19 | // shift the curve to match the base color's lightness
20 | // don't go below 0 or above 100
21 | let newL = Math.min(Math.max(curveArr[i] - shift, 0), 1);
22 |
23 | // reduce chroma as we get further from the base color
24 | // don't go below the minimum (the lowest between minChroma or the base color's chroma)
25 | const cStep = chromaStepType === 'value' ? chromaStep : chromaStep * c;
26 | let newC = Math.max(Math.min(c, chromaMinimum), c - cStep * Math.abs(i - index));
27 |
28 | // overrides
29 | const override = overrides?.find((o) => Number(o.scale) === i + 1);
30 |
31 | // don't override if the step is the base color
32 | if (override && i !== index) {
33 | const c = override.chroma || override.chroma === 0 ? Number(override.chroma) : undefined
34 | const l = override.lightness || override.lightness === 0 ? Number(override.lightness) : undefined
35 |
36 | // chroma overrides can't be higher than what it already was
37 | if (c !== undefined && !isNaN(c)) newC = Math.min(newC, c)
38 |
39 | if (l !== undefined && !isNaN(l)) newL = l
40 | }
41 |
42 | return [newL, newC, h];
43 | });
44 |
45 | return { base: { oklch: [l, c, h], index: index }, palette }
46 | }
47 |
48 | export const getL = (l: number) => +(l * 100).toFixed(2) + '%';
49 | export const getC = (c: number) => +c.toFixed(3);
50 | export const getH = (h: number) => +(h || 0).toFixed(2);
51 | export const getOklch = (l: number, c: number, h: number) => {
52 | return `oklch(${getL(l)} ${getC(c)} ${getH(h)})`;
53 | };
--------------------------------------------------------------------------------
/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import type { Configuration } from "./types";
2 |
3 | export const DEFAULT: Configuration = Object.freeze({
4 | colors: [],
5 | scales: 18,
6 | chromaStepType: 'percentage',
7 | chromaStep: 0.02,
8 | chromaMinimum: 0.05,
9 | curve: [0,0.2,0.4,0.6,0.8,1],
10 | overrides: [],
11 | })
--------------------------------------------------------------------------------
/src/lib/params.ts:
--------------------------------------------------------------------------------
1 | import chroma from "chroma-js";
2 | import { DEFAULT } from "./constants";
3 | import type { Configuration } from "./types";
4 |
5 | export const PARAMS: Record = {
6 | colors: 'c',
7 | scales: 's',
8 | chromaStepType: 'cst',
9 | chromaStep: 'cs',
10 | chromaMinimum: 'cm',
11 | overrides: 'o',
12 | curve: 'cv',
13 | }
14 |
15 | const isValidNumber = (v: number | undefined) => typeof v === 'number' && !isNaN(v);
16 |
17 | const hasValue = (v: T | undefined): v is T => !!v || v === 0;
18 |
19 | export function serializeOverrides(overrides: Configuration['overrides']): string {
20 | return overrides.map((o) => {
21 | const s = Number(o.scale || o.scale === 0 ? o.scale : undefined);
22 | const c = Number(o.chroma || o.chroma === 0 ? o.chroma : undefined);
23 | const l = Number(o.lightness || o.lightness === 0 ? o.lightness : undefined);
24 |
25 | const scale = isValidNumber(s) ? `s${s}` : '';
26 | const chroma = isValidNumber(c) ? `c${c}` : '';
27 | const lightness = isValidNumber(l) ? `l${l}` : '';
28 |
29 | return `${scale}${chroma}${lightness}`;
30 | }).join(',');
31 | }
32 |
33 | export function serializeCurve(curve: Configuration['curve']): string {
34 | return curve.map((n) => Number(n)).filter((n) => !isNaN(n)).join(',');
35 | }
36 |
37 | export function serializer({
38 | colors,
39 | scales,
40 | chromaStepType,
41 | chromaStep,
42 | chromaMinimum,
43 | curve,
44 | overrides,
45 | }: Partial): URLSearchParams {
46 | const params = new URLSearchParams();
47 |
48 | if (colors && colors?.length) {
49 | params.set(PARAMS.colors, colors.join(','))
50 | };
51 |
52 | if (hasValue(scales) && scales !== DEFAULT.scales) {
53 | params.set(PARAMS.scales, scales.toString());
54 | }
55 |
56 | if (chromaStepType && chromaStepType !== DEFAULT.chromaStepType) {
57 | params.set(PARAMS.chromaStepType, chromaStepType);
58 | }
59 |
60 | if (hasValue(chromaStep) && chromaStep !== DEFAULT.chromaStep) {
61 | params.set(PARAMS.chromaStep, chromaStep.toString());
62 | }
63 |
64 | if (hasValue(chromaMinimum) && chromaMinimum !== DEFAULT.chromaMinimum) {
65 | params.set(PARAMS.chromaMinimum, chromaMinimum.toString());
66 | }
67 |
68 | if (curve && curve.length && (curve.length !== DEFAULT.curve.length || curve.some((n, i) => Number(n) !== DEFAULT.curve[i]))) {
69 | params.set(PARAMS.curve, serializeCurve(curve))
70 | }
71 |
72 | if (overrides && overrides.length) {
73 | params.set(PARAMS.overrides, serializeOverrides(overrides));
74 | }
75 |
76 | return params;
77 | }
78 |
79 |
80 | export function parseColor(params: URLSearchParams): Configuration['colors'] {
81 | return params.get(PARAMS.colors)?.split(',').filter((v) => chroma.valid(v)) ?? [...DEFAULT.colors];
82 | }
83 |
84 | export function parseOverrides(params: URLSearchParams): Configuration['overrides'] {
85 | return params.get(PARAMS.overrides)?.split(',').map((v) => {
86 | const scale = Number(v.match(/s(\d+\.?\d*)/)?.[1]);
87 | const chroma = Number(v.match(/c(\d+\.?\d*)/)?.[1]);
88 | const lightness = Number(v.match(/l(\d+\.?\d*)/)?.[1]);
89 |
90 | return {
91 | scale: isValidNumber(scale) ? scale : undefined,
92 | chroma: isValidNumber(chroma) ? chroma : undefined,
93 | lightness: isValidNumber(lightness) ? lightness : undefined,
94 | }
95 | }) ?? [...DEFAULT.overrides]
96 | }
97 |
98 | export function parseCurve(params: URLSearchParams): Configuration['curve'] {
99 | return params.get(PARAMS.curve)?.split(',').map((v) => Number(v)).filter((n) => !isNaN(n)) ?? [...DEFAULT.curve]
100 | }
101 |
102 | export function parseChromaStepType(params: URLSearchParams): Configuration['chromaStepType'] {
103 | return params.get(PARAMS.chromaStepType) === 'value' ? 'value' : 'percentage';
104 | }
105 |
106 | export function parser(params: URLSearchParams): Configuration {
107 | const colors = parseColor(params);
108 | const scales = Number(params.get(PARAMS.scales)) || DEFAULT.scales;
109 | const chromaStepType = parseChromaStepType(params);
110 | const chromaStep = Number(params.get(PARAMS.chromaStep)) ?? DEFAULT.chromaStep;
111 | const chromaMinimum = Number(params.get(PARAMS.chromaMinimum)) ?? DEFAULT.chromaMinimum;
112 | const overrides = parseOverrides(params);
113 | const curve = parseCurve(params);
114 |
115 | return { colors, scales, chromaStepType, chromaStep, chromaMinimum, overrides, curve };
116 | }
--------------------------------------------------------------------------------
/src/lib/svg.ts:
--------------------------------------------------------------------------------
1 | import chroma from "chroma-js";
2 | import type { OKLCH } from "./color";
3 |
4 | // receives a palette and returns an SVG string with a row of squares (200x200 each) with the correct colors (in rgb for better compatibility)
5 | export const paletteToSvg = (palette: OKLCH[]) => {
6 | const squares = palette.map(([l, c, h], i) => {
7 | const [r, g, b] = chroma.oklch(l, c, h).rgb();
8 | return ` `;
9 | }).join('');
10 |
11 | return `${squares} `;
12 | }
--------------------------------------------------------------------------------
/src/lib/toaster.ts:
--------------------------------------------------------------------------------
1 | import { createToaster } from '@significa/svelte-toaster'
2 |
3 | export type ToastType = {
4 | type: 'success' | 'error'
5 | message: string
6 | }
7 |
8 | export const toaster = createToaster({
9 | duration: (toast) => toast.data.type === 'success' ? 2000 : 5000,
10 | })
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type OKLCH = [number, number, number];
2 |
3 | export type Override = { scale?: number; chroma?: number; lightness?: number; };
4 |
5 | export type Configuration = {
6 | colors: string[],
7 | scales: number,
8 | chromaStepType: 'value' | 'percentage',
9 | chromaStep: number,
10 | chromaMinimum: number,
11 | overrides: Override[],
12 | curve: number[],
13 | }
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx';
2 | import { clsx } from 'clsx';
3 | import { twMerge } from 'tailwind-merge';
4 | import type { OKLCH } from './types';
5 | import chroma from 'chroma-js';
6 |
7 | export function cn(...inputs: ClassValue[]) {
8 | return twMerge(clsx(inputs));
9 | }
10 |
11 | // given an array of numbers, generate a new array with a given length.
12 | // the new numbers will be interpolated from the original array.
13 | // so the curve is linear between any points in the original array.
14 | export function resizeNumberArray(arr: number[], newSize: number) {
15 | const originalSize = arr.length;
16 | const step = (originalSize - 1) / (newSize - 1);
17 |
18 | return Array.from({ length: newSize }, (_, index) => {
19 | const leftIndex = Math.floor(index * step);
20 | const rightIndex = Math.ceil(index * step);
21 | const weight = index * step - leftIndex;
22 |
23 | if (leftIndex === rightIndex) {
24 | return arr[leftIndex];
25 | } else {
26 | return (1 - weight) * arr[leftIndex] + weight * arr[rightIndex];
27 | }
28 | });
29 | }
30 |
31 | // function that finds the closest number in an array of numbers
32 | export function findClosestNumber(arr: number[], target: number) {
33 | return arr.reduce((prev, curr) => {
34 | return Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev;
35 | });
36 | }
37 |
38 | /**
39 | *
40 | * {
41 | "{color} Palette": {
42 | "1": {
43 | "name": "1",
44 | "description": "",
45 | "value": "HEX_COLOR",
46 | "type": "color"
47 | },
48 | "2": {
49 | "name": "2",
50 | "description": "",
51 | "value": "HEX_COLOR",
52 | "type": "color"
53 | },
54 | "name": "{color} Palette"
55 | }
56 | }
57 | */
58 |
59 | type PaletteJSON = Record>
65 |
66 | const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
67 |
68 | export function paletteToJSON(name: string, palette: OKLCH[]): { [key: string]: PaletteJSON } {
69 | return {
70 | [capitalize(name)]: palette.reduce((obj, [l, c, h], i) => {
71 | const name = i + 1;
72 | const value = chroma.oklch(l, c, h).hex();
73 | const description = '';
74 | const type = 'color';
75 |
76 | return {
77 | ...obj,
78 | [name]: {
79 | name,
80 | description,
81 | value,
82 | type,
83 | },
84 | };
85 | }, {})
86 | }
87 | }
88 |
89 | export function downloadFile(data: string, fileName = 'palette.json') {
90 | const blob = new Blob([data], { type: 'application/json' });
91 | const url = URL.createObjectURL(blob);
92 | const a = document.createElement('a');
93 | a.href = url;
94 | a.download = fileName;
95 | a.click();
96 | URL.revokeObjectURL(url);
97 | }
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 | Palette generator by Significa
14 |
15 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |
73 |
74 |
75 |
76 | Palette generator
77 |
78 |
79 |
80 |
119 |
120 |
121 |
122 |
123 | Configuration
124 |
125 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 | Curve
191 |
192 |
{
195 | curve = [...DEFAULT.curve];
196 | }}
197 | >
198 |
199 |
200 |
201 |
202 |
203 |
204 | {
207 | curve = [...curve, curve[curve.length - 1] ?? 1];
208 | }}
209 | on:delete={(event) => {
210 | curve = curve.filter((_, index) => index !== event.detail);
211 | }}
212 | />
213 |
214 |
215 |
216 |
You can create your own lightness curve. The numbers between steps will be interpolated
218 | linearly. A shift will be applied to make sure your base color remains unchanged.
220 |
221 |
222 |
223 |
271 |
272 |
273 |
282 |
{
284 | mobileColorsPanel = !mobileColorsPanel;
285 | }}
286 | class={cn(
287 | 'block w-full h-10 px-4 py-2 lg:hidden rounded-t-lg border bg-white transition-all',
288 | mobileColorsPanel ? 'mt-4' : 'mt-0',
289 | 'shadow-[0_-1px_2px_rgba(0,0,0,0.04),0_-2px_4px_rgba(0,0,0,0.02)]'
290 | )}
291 | >
292 |
293 | Palettes
294 | {#if mobileColorsPanel}
295 |
296 | {:else}
297 |
298 | {/if}
299 |
300 |
301 |
308 | {#each colors as c}
309 | {@const { base, palette } = generatePalette(
310 | c,
311 | parser(
312 | serializer({
313 | scales,
314 | chromaStepType,
315 | chromaStep,
316 | chromaMinimum,
317 | overrides,
318 | curve
319 | })
320 | )
321 | )}
322 |
{
327 | colors = colors.filter((c) => c !== event.detail);
328 | }}
329 | />
330 | {:else}
331 |
332 |
333 |
334 |
335 |
336 |
It's so white
337 |
Here's a polar bear
338 |
339 |
340 | {/each}
341 |
356 |
357 |
358 |
359 |
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/favicon.ico
--------------------------------------------------------------------------------
/static/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/significa/palette-generator/370e9723084b378302487aef89f81c89677d0cef/static/og.png
--------------------------------------------------------------------------------
/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/kit/vite';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter(),
15 | alias: {
16 | $components: './src/components',
17 | }
18 | }
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./src/**/*.{html,js,svelte,ts}'],
4 | theme: {
5 | extend: {}
6 | },
7 | plugins: []
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/js/script.js",
5 | "destination": "https://plausible.io/js/script.js"
6 | },
7 | {
8 | "source": "/api/event",
9 | "destination": "https://plausible.io/api/event"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()],
6 | test: {
7 | include: ['src/**/*.{test,spec}.{js,ts}']
8 | }
9 | });
10 |
--------------------------------------------------------------------------------