├── .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 | 35 | -------------------------------------------------------------------------------- /src/components/configuration-field.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | 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 | 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 | 8 | -------------------------------------------------------------------------------- /src/components/palette.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |
27 |
28 |
32 | 41 | 42 | 51 |
52 |
53 | 67 | 78 | { 80 | dispatch('delete', color); 81 | }} 82 | > 83 | 84 | Delete 85 | 86 |
87 |
88 |
89 | {#each palette as [l, c, h], i} 90 | 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 | 37 | 38 | 45 | -------------------------------------------------------------------------------- /src/components/small-button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 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 |
{ 83 | e.preventDefault(); 84 | 85 | if (!color) return; 86 | 87 | if (!chroma.valid(color)) { 88 | invalid = true; 89 | return; 90 | } 91 | 92 | // assign variables 93 | invalid = false; 94 | colors = [color, ...parseColor($page.url.searchParams)]; 95 | color = ''; 96 | mobileColorsPanel = true; 97 | }} 98 | > 99 |
100 | 101 | (invalid = false)} 104 | class={cn(invalid && 'border-pink-300')} 105 | id="color" 106 | bind:value={color} 107 | placeholder="Type your color" 108 | /> 109 | {#if invalid} 110 | Invalid color 111 | {/if} 112 |
113 | 114 | Type a valid color in any format to generate a monochromatic palette. It will range from 116 | dark to light with the saturation decreasing as it moves away from the base color. 118 |
119 | 120 |
121 |

122 | 123 | Configuration 124 |

125 |
126 |
127 | { 133 | scales = DEFAULT.scales; 134 | }} 135 | > 136 | 137 | 138 | 139 | { 145 | chromaStep = DEFAULT.chromaStep; 146 | }} 147 | > 148 | 152 | 160 | 161 | 162 | { 169 | chromaMinimum = DEFAULT.chromaMinimum; 170 | }} 171 | > 172 | 180 | 181 |
182 |
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 |
224 |
225 |

226 | 227 | Overrides 228 |

229 | { 232 | overrides = [...overrides, {}]; 233 | }} 234 | > 235 | 236 | 237 |
238 | 239 | {#each overrides as override, i} 240 | { 243 | overrides = overrides.filter((_, index) => index !== i); 244 | }} 245 | > 246 | 247 | 255 | 263 | 264 | {/each} 265 | 266 | You can add chroma and lightness overrides to specific scales. It will never affect the 268 | base color. If the chroma override is greater than the original, it will not be applied. 270 |
271 |
272 | 273 |
282 | 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 | Illustration of a polar bear 335 |
336 |

It's so white

337 |

Here's a polar bear

338 |
339 |
340 | {/each} 341 |
342 | Built by Significa 348 | / 349 | Source on Github 355 |
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 | --------------------------------------------------------------------------------