Repository: lokesh/color-thief Branch: master Commit: 01dc0f3a8759 Files: 59 Total size: 258.3 KB Directory structure: gitextract_gvnyy151/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .mocharc.yml ├── .nvmrc ├── LICENSE ├── PLAN.md ├── README.md ├── V3.md ├── async.html ├── build/ │ └── build.js ├── cypress/ │ ├── e2e/ │ │ ├── api-direct.cy.js │ │ ├── api.cy.js │ │ ├── cors.cy.js │ │ └── module.cy.js │ ├── fixtures/ │ │ └── example.json │ ├── plugins/ │ │ └── index.cjs │ ├── support/ │ │ ├── commands.js │ │ └── e2e.js │ └── test-pages/ │ ├── api-direct.html │ ├── cors.html │ ├── es6-module.html │ ├── index.html │ ├── index.js │ └── screen.css ├── cypress.config.cjs ├── examples/ │ ├── css/ │ │ └── screen.css │ └── js/ │ └── demo.js ├── index.html ├── package.json ├── src/ │ ├── api.ts │ ├── cli.ts │ ├── color-space.ts │ ├── color.ts │ ├── index.ts │ ├── internals.browser.ts │ ├── internals.ts │ ├── loaders/ │ │ ├── browser.ts │ │ └── node.ts │ ├── observe.ts │ ├── pipeline.ts │ ├── progressive.ts │ ├── quantizers/ │ │ ├── mmcq.ts │ │ └── wasm.ts │ ├── resolve-loader.browser.ts │ ├── resolve-loader.ts │ ├── swatches.ts │ ├── sync.ts │ ├── types.ts │ ├── umd.ts │ ├── wasm/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── worker/ │ ├── manager.ts │ └── worker-script.ts ├── test/ │ ├── cli-test.js │ ├── node-cjs-test.cjs │ └── node-test.js ├── tsconfig.json └── tsup.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig helps developers define and maintain consistent # coding styles between different editors and IDEs # editorconfig.org root = true [*] # Change these settings to your own preference indent_style = space indent_size = 4 # We recommend you to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [master, dev] pull_request: branches: [master] jobs: node-tests: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build - run: npm run test:node browser-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run build - name: Start http-server run: npx http-server -p 8080 & - name: Wait for server run: npx wait-on http://localhost:8080 - run: npx cypress run --config video=false ================================================ FILE: .gitignore ================================================ *.log *.sql *.sqlite .htaccess .ftppass .host_config *.DS_Store ehthumbs.db Icon? Thumbs.db .sass-cache Rakefile rsync-exclude node_modules dist .idea CLAUDE.md *.tsbuildinfo ================================================ FILE: .mocharc.yml ================================================ spec: test/**/*.{js,cjs} timeout: 10000 ================================================ FILE: .nvmrc ================================================ 18.20.4 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Lokesh Dhakar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PLAN.md ================================================ # PLAN.md ## Phase 1: v2 — Non-breaking improvements Improvements that ship under the current API contract. No breaking changes. Existing consumers upgrade without code changes. ### 1A: Fix critical bugs - **Handle white/single-color images.** When the pixel filter strips all pixels (all white, all transparent, single color), return a sensible fallback instead of `null`. `getColor()` should return the dominant remaining color (or the image's actual color if filtering removed everything). `getPalette()` should return a shorter array rather than crash. - **Fix variable scope leak.** `src/color-thief.js:120` — `i = uInt8Array.length` is missing `let`, creating an implicit global. - **Add input validation with clear error messages.** Throw descriptive errors for: missing/unloaded image elements, tainted canvases (CORS), invalid image sources. Currently these fail silently or throw cryptic browser errors. ### 1B: TypeScript type definitions - Ship a `dist/color-thief.d.ts` and `dist/color-thief-node.d.ts` alongside the existing JS output. - Add `types` field to `package.json`. - No source rewrite — just hand-authored `.d.ts` files that match the current API. ### 1C: Accept more input types (browser) Expand what `getColor()` and `getPalette()` accept beyond `HTMLImageElement`: - `HTMLCanvasElement` - `ImageData` - `ImageBitmap` These are additive — existing code passing `` elements still works. ### 1D: Configurable pixel filtering Expose the hardcoded thresholds as optional config: - `ignoreWhite` (default `true`, current behavior) — with configurable RGB threshold (default 250) - `alphaThreshold` (default 125) — pixels below this alpha are skipped - `minSaturation` (default 0) — optional minimum saturation filter Pass as an options object: `getColor(image, { quality: 10, ignoreWhite: false })`. The current positional args (`quality`, `colorCount`) continue to work for backward compat. ### 1E: Update dev tooling - Upgrade ESLint v5 → v9 with flat config. - Update `ecmaVersion` from 2018 to current. - Add a CI workflow (GitHub Actions) for automated test runs on PRs. - Drop the misleading `color-thief.min.js` copy — or actually minify it. --- ## Phase 2: v3 — Breaking changes and new architecture A new major version. Different API surface, new output format, modern JS throughout. Published as a new major version with a migration guide. ### 2A: TypeScript rewrite and unified codebase - Rewrite all source files in TypeScript. - Merge the browser and Node implementations into a single codebase with platform-specific adapters for pixel loading. The core algorithm, pixel filtering, and output formatting are shared. Only the "get pixels from image" step differs. - Single API surface for both platforms. No more class on browser / bare functions on Node. ### 2B: Modern async API - Promise-based everywhere. `getColor()` and `getPalette()` return Promises on both browser and Node. - Drop `getColorFromUrl()`, `getColorAsync()`, and `getImageData()`. Image loading is the consumer's responsibility. - Replace `XMLHttpRequest` with `fetch()` if any internal HTTP calls remain. - Support `AbortController` / `AbortSignal` for cancellation. ### 2C: Rich output format Replace bare `[r, g, b]` arrays with color objects: ``` const color = await colorThief.getColor(image); color.rgb() // { r, g, b } color.hex() // '#e84d3d' color.hsl() // { h, s, l } color.oklch() // { l, c, h } color.array() // [r, g, b] (backward-compat escape hatch) color.isDark // boolean ``` `getPalette()` returns an array of these objects. ### 2D: Semantic swatches Add a `getSwatches()` method that classifies palette colors into UI roles: - Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted - Each swatch includes a suggested text color (title and body) for accessibility. - Inspired by Android's Palette API / node-vibrant, but with OKLCH-based classification for better perceptual accuracy. ### 2E: Web Worker support - Use `OffscreenCanvas` + `createImageBitmap` to move pixel reading and quantization off the main thread. - Opt-in via config: `getColor(image, { worker: true })`. - Fallback to synchronous main-thread processing when Workers or OffscreenCanvas are unavailable. ### 2F: Lighter Node.js dependencies - Remove `sharp` as a hard dependency. It's heavy (native bindings, Docker/CI build issues). - Make image decoding pluggable — consumers bring their own decoder, or use a built-in lightweight default. - Consider `sharp` as an optional peer dependency for users who already have it. ### 2G: Optional WASM backend - Ship a `@colorthief/wasm` package with the quantization algorithm compiled from Rust. - Same API as the pure-JS version — drop-in replacement for the core. - ~6x performance improvement for the compute-heavy pixel clustering step. - The main `colorthief` package stays pure JS with zero native dependencies. ### 2H: OKLCH-native pipeline - Option to perform quantization in OKLCH color space instead of RGB. - Produces more perceptually distinct palettes — colors that look different to humans, not just mathematically distant in RGB. - Default remains RGB quantization for performance and backward compat. OKLCH mode is opt-in. ### 2I: Accessibility built in - For each color in a palette, include: - WCAG contrast ratios against white and black - `isDark` / `isLight` boolean - Suggested foreground text color (white or black) for AA compliance - Make this zero-config — always included in the output, no extra method calls. ### 2J: Progressive extraction - For large images, return an approximate palette immediately from a downsampled pass, then refine progressively. - API: `getColor(image, { progressive: true })` returns an async iterator or observable that emits improving results. - Useful for large images, batch processing, and perceived performance. - No competitor currently offers this. ================================================ FILE: README.md ================================================ # Color Thief > Extract dominant colors and palettes from images in the browser and Node.js. [![npm version](https://img.shields.io/npm/v/colorthief)](https://www.npmjs.com/package/colorthief) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/colorthief)](https://bundlephobia.com/package/colorthief) [![types](https://img.shields.io/npm/types/colorthief)](https://www.npmjs.com/package/colorthief) ## Install ```bash npm install colorthief ``` Or load directly from a CDN: ```html ``` ## Quick Start ```js import { getColorSync, getPaletteSync, getSwatches } from 'colorthief'; // Dominant color const color = getColorSync(img); color.hex(); // '#e84393' color.css(); // 'rgb(232, 67, 147)' color.isDark; // false color.textColor; // '#000000' // Palette const palette = getPaletteSync(img, { colorCount: 6 }); palette.forEach(c => console.log(c.hex())); // Semantic swatches (Vibrant, Muted, DarkVibrant, etc.) const swatches = await getSwatches(img); swatches.Vibrant?.color.hex(); ``` ## Features - **TypeScript** — full type definitions included - **Browser + Node.js** — same API, both platforms - **Sync & async** — synchronous browser API, async for Node.js and Web Workers - **Live extraction** — `observe()` watches video, canvas, or img elements and emits palette updates reactively - **Web Workers** — offload quantization off the main thread with `worker: true` - **Progressive extraction** — 3-pass refinement for instant rough results - **OKLCH quantization** — perceptually uniform palettes via `colorSpace: 'oklch'` - **Semantic swatches** — Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted - **Rich Color objects** — `.hex()`, `.rgb()`, `.hsl()`, `.oklch()`, `.css()`, contrast ratios, text color recommendations - **WCAG contrast** — `color.contrast.white`, `color.contrast.black`, `color.contrast.foreground` - **AbortSignal** — cancel in-flight extractions - **CLI** — `colorthief photo.jpg` with JSON, CSS, and ANSI output - **Zero runtime dependencies** ## API at a Glance | Function | Description | |---|---| | `getColorSync(source, options?)` | Dominant color (sync, browser only) | | `getPaletteSync(source, options?)` | Color palette (sync, browser only) | | `getSwatchesSync(source, options?)` | Semantic swatches (sync, browser only) | | `getColor(source, options?)` | Dominant color (async, browser + Node.js) | | `getPalette(source, options?)` | Color palette (async, browser + Node.js) | | `getSwatches(source, options?)` | Semantic swatches (async, browser + Node.js) | | `getPaletteProgressive(source, options?)` | 3-pass progressive palette (async generator) | | `observe(source, options)` | Watch a source and emit palette updates (browser only) | | `createColor(r, g, b, population)` | Build a Color object from RGB values | ### Options | Option | Default | Description | |---|---|---| | `colorCount` | `10` | Number of palette colors (2–20) | | `quality` | `10` | Sampling rate (1 = every pixel, 10 = every 10th) | | `colorSpace` | `'oklch'` | Quantization space: `'rgb'` or `'oklch'` | | `worker` | `false` | Offload to Web Worker (browser only) | | `signal` | — | `AbortSignal` to cancel extraction | | `ignoreWhite` | `true` | Skip white pixels | ### Color Object | Property / Method | Returns | |---|---| | `.rgb()` | `{ r, g, b }` | | `.hex()` | `'#ff8000'` | | `.hsl()` | `{ h, s, l }` | | `.oklch()` | `{ l, c, h }` | | `.css(format?)` | `'rgb(255, 128, 0)'`, `'hsl(…)'`, or `'oklch(…)'` | | `.array()` | `[r, g, b]` | | `.toString()` | Hex string (works in template literals) | | `.textColor` | `'#ffffff'` or `'#000000'` | | `.isDark` / `.isLight` | Boolean | | `.contrast` | `{ white, black, foreground }` — WCAG ratios | | `.population` | Raw pixel count | | `.proportion` | 0–1 share of total | ## Browser ```js import { getColorSync, getPaletteSync } from 'colorthief'; const img = document.querySelector('img'); const color = getColorSync(img); console.log(color.hex()); const palette = getPaletteSync(img, { colorCount: 5 }); ``` Accepts `HTMLImageElement`, `HTMLCanvasElement`, `HTMLVideoElement`, `ImageData`, `ImageBitmap`, and `OffscreenCanvas`. ### Live extraction with observe() ```js import { observe } from 'colorthief'; // Watch a video and update ambient lighting as it plays const controller = observe(videoElement, { throttle: 200, // ms between updates colorCount: 5, onChange(palette) { updateAmbientBackground(palette); }, }); // Stop when done controller.stop(); ``` Works with `