Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on:
push:
branches:
- main
- master
pull_request:

jobs:
test:
name: Build and test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Lint
run: npm run lint

- name: Test
run: npm test

- name: Pack smoke test
run: npm run test:pack
Binary file modified .gitignore
Binary file not shown.
19 changes: 19 additions & 0 deletions .idea/php.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 40 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ npm i @rtcoder/dominant-color

## Usage

Import the `getDominantColor` function from `@rtcoder/dominant-color` in your JavaScript file:
Import the `getDominantColor` or `getDominantColorAsync` function from `@rtcoder/dominant-color` in your JavaScript file:

```javascript
import { getDominantColor } from "@rtcoder/dominant-color";
import { getDominantColor, getDominantColorAsync } from "@rtcoder/dominant-color";
```

Select an image element from your HTML:
Select an image source:

```javascript
const img = document.querySelector('img');
Expand All @@ -31,33 +31,56 @@ getDominantColor(img, {
downScaleFactor: 1,
skipPixels: 0,
colorsPaletteLength: 5,
colorBucketSize: 24,
colorGroupingThreshold: 0,
colorQuantization: 'exact',
paletteWithCountOfOccurrences: false,
colorFormat: 'rgb',
callback: (color, palette) => {
// Your code here
},
errorCallback: (error) => {
// Handle image loading or canvas errors here
}
});
```

You can also use the Promise-based API:

```javascript
const { dominant, colorsPalette } = await getDominantColorAsync(img, {
colorFormat: 'hex',
colorQuantization: 'median-cut'
});
```

Image sources can be an `HTMLImageElement`, `HTMLCanvasElement`, `ImageBitmap`, image URL string, `Blob`, or `File`.

## Configuration Options

The `getDominantColor` function accepts the following configuration options:
The `getDominantColor` and `getDominantColorAsync` functions accept the following configuration options:

| Name | Type | Default Value | Description |
| --------------------------- | -------- | ------------- | ------------------------------------------------------------ |
| `downScaleFactor` | number | 1 | Factor of scale down for the image. Recommended for large images. |
| `skipPixels` | number | 0 | Skips every `n` pixels while determining the dominant color. Recommended for large images. |
| `colorsPaletteLength` | number | 5 | Length of the returned color palette. |
| `colorBucketSize` | number | 24 | RGB bucket size used when `colorQuantization` is `'bucket'`. Smaller values keep more detail; larger values merge more colors. |
| `colorGroupingThreshold` | number | 0 | Groups similar RGB colors before sorting. Use `0` for exact pixel matching, or a larger value such as `10`-`30` for photos. |
| `colorQuantization` | string | `'exact'` | Defines the color counting algorithm. Use `'exact'` for exact RGB matching, `'bucket'` for fast grouped palettes, or `'median-cut'` for more balanced photo palettes. |
| `paletteWithCountOfOccurrences` | boolean | false | Determines whether to return colors with the number of occurrences. |
| `colorFormat` | string | `'rgb'` | Defines the format of the returned colors. Available values are `'rgb'`, `'hsl'`, and `'hex'`. |
| `colorFormat` | string | `'rgb'` | Defines the format of the returned dominant color and palette colors. Available values are `'rgb'`, `'hsl'`, and `'hex'`. |
| `callback` | function | [empty function] | Callback function that receives the dominant color and the colors palette. |
| `errorCallback` | function | [empty function] | Callback function that receives image loading, canvas, and processing errors. |

## Interfaces

The library provides the following interfaces for type checking:

```typescript
type ColorFormat = 'rgb' | 'hsl' | 'hex';
type ColorQuantization = 'exact' | 'bucket' | 'median-cut';
type DominantColorSource = HTMLImageElement | HTMLCanvasElement | ImageBitmap | string | Blob;

interface PrimaryColor {
color: string;
Expand All @@ -68,14 +91,25 @@ interface DominantColorOptions {
downScaleFactor: number;
skipPixels: number;
colorsPaletteLength: number;
colorBucketSize: number;
colorGroupingThreshold: number;
colorQuantization: ColorQuantization;
paletteWithCountOfOccurrences: boolean;
colorFormat: ColorFormat;
callback: DominantColorCallback;
errorCallback: DominantColorErrorCallback;
}

type DominantColorCallback = (dominant: string, colorsPalette: string[] | PrimaryColor[]) => void;
type DominantColorErrorCallback = (error: Error) => void;

interface DominantColorResult {
dominant: string;
colorsPalette: string[] | PrimaryColor[];
}

function getDominantColor(element: HTMLImageElement, options: Partial<DominantColorOptions>): void;
function getDominantColor(source: DominantColorSource, options?: Partial<DominantColorOptions>): void;
function getDominantColorAsync(source: DominantColorSource, options?: Partial<DominantColorOptions>): Promise<DominantColorResult>;
```

Feel free to explore and utilize these interfaces for better code development.
Expand Down
97 changes: 56 additions & 41 deletions css/index.css
Original file line number Diff line number Diff line change
@@ -1,81 +1,96 @@
@import 'style.css';

.uploader-section {
padding-bottom: 1.25rem;
padding-top: 1.5rem;
}

.uploader-section .page-title {
padding: 2rem 0 1.25rem;
}

#drop-area {
border: 2px dashed #ccc;
border-radius: 20px;
max-width: 480px;
width: 100%;
font-family: sans-serif;
margin: 20px auto;
align-items: center;
background: var(--surface);
border: 2px dashed #9bb7b5;
border-radius: 8px;
color: var(--brand-dark);
cursor: pointer;
display: flex;
height: 200px;
font-size: clamp(1.1rem, 3vw, 1.8rem);
font-weight: 800;
justify-content: center;
align-items: center;
font-size: 30px;
margin: 0 auto;
max-width: 720px;
min-height: 160px;
padding: 1.5rem;
text-align: center;
transition: border-color 0.2s ease, background-color 0.2s ease;
user-select: none;
cursor: pointer;
width: 100%;
}

#drop-area.highlight {
background: #09d url(../img/img-placeholder.png) center no-repeat;
background-size: contain;
border-color: #036193;
background: #dcefed url(../img/img-placeholder.png) center no-repeat;
background-size: 140px;
border-color: var(--brand);
color: transparent;
}

#drop-area:hover {
border-color: #036193;
border-color: var(--brand);
background-color: #eef7f5;
}

#gallery {
margin: 10px auto;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
max-width: 500px;
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
margin: 1.5rem auto 0;
max-width: 980px;
}

.uploader-section + .section-tight {
padding-top: 1.25rem;
}

.img-container {
width: 100%;
--dominant-color: transparent;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 16px 40px rgba(23, 32, 38, 0.1);
display: flex;
margin: 10px;
justify-content: center;
align-items: center;
flex-direction: column;
border: 1px solid #ccc;
border-radius: 20px;
overflow: hidden;
--dominant-color: transparent;
box-shadow: 0 0 5px 0 var(--dominant-color);
}

.img-color-container {
width: 100%;
padding: 10px;
display: flex;
justify-content: space-evenly;
align-items: center;
font-size: 20px;
display: flex;
gap: 1rem;
justify-content: space-between;
padding: 1rem;
}

.img-color-container .circle {
width: 100px;
height: 100px;
border-radius: 100px;
border: 1px solid rgba(0, 0, 0, 0.5);
background-color: var(--dominant-color);
border: 1px solid rgba(23, 32, 38, 0.2);
border-radius: 50%;
flex: 0 0 auto;
height: 72px;
width: 72px;
}

.image {
background: url(../img/transparent.png) repeat;
}

input {
display: none;
.image img {
height: auto;
width: 100%;
}

img {
object-fit: unset;
border-bottom: 1px solid #ddd;
input[type='file'] {
display: none;
}
Loading