Skip to content

Commit

Permalink
feat: Support CMYK Color Mode (#250)
Browse files Browse the repository at this point in the history
* Support CMYK color mode

* Add CMYK to readme
  • Loading branch information
ChandlerVS committed Apr 13, 2024
1 parent 8dd8b3a commit 2927a9d
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 9 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ new TinyColor('hsv 0 1 1');
new TinyColor({ h: 0, s: 100, v: 100 });
```

### CMYK

```ts
new TinyColor('cmyk(0, 25, 20, 0)');
new TinyColor('cmyk(0, 100, 100, 0)');
new TinyColor('cmyk 100 0 100 0)');
new TinyColor({c: 0, m: 25, y: 25, k: 0});
```

### Named

```ts
Expand Down Expand Up @@ -284,6 +293,13 @@ color.setAlpha(0.5);
color.toHslString(); // "hsla(0, 100%, 50%, 0.5)"
```

### toCmykString

```ts
const color = new TinyColor('red');
color.toCmykString(); // "cmyk(0, 100, 100, 0)"
```

### toNumber
```ts
new TinyColor('#aabbcc').toNumber() === 0xaabbcc // true
Expand Down
3 changes: 2 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ <h4>Demo</h4>
<a onclick="handleChange('0f0')">0f0</a> -
<a onclick="handleChange('rgb 255 128 128')">rgb 255 128 128</a> -
<a onclick="handleChange('hsl(0, 100%, 50%)')">hsl(0, 100%, 50%)</a> -
<a onclick="handleChange('hsv 0, 100%, 50%')">hsv 0, 100%, 50%</a>
<a onclick="handleChange('hsv 0, 100%, 50%')">hsv 0, 100%, 50%</a> -
<a onclick="handleChange('cmyk(0, 25, 20, 0)')">cmyk(0, 25, 20, 0)</a>
</p>
<label>And I'll tell you what I know about it:</label>
</div>
Expand Down
1 change: 1 addition & 0 deletions demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function colorChange(color) {
'rgb:\t' + tiny.toRgbString(),
'hsl:\t' + tiny.toHslString(),
'hsv:\t' + tiny.toHsvString(),
'cmyk:\t' + tiny.toCmykString(),
'name:\t' + (tiny.toName() || 'none'),
'format:\t' + tiny.format,
'string:\t' + tiny.toString(),
Expand Down
43 changes: 43 additions & 0 deletions src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,49 @@ export function rgbaToArgbHex(r: number, g: number, b: number, a: number): strin
return hex.join('');
}

/**
* Converts CMYK to RBG
* Assumes c, m, y, k are in the set [0, 100]
*/
export function cmykToRgb(c: number, m: number, y: number, k: number) {
const cConv = c / 100;
const mConv = m / 100;
const yConv = y / 100;
const kConv = k / 100;

const r = 255 * (1 - cConv) * (1 - kConv);
const g = 255 * (1 - mConv) * (1 - kConv);
const b = 255 * (1 - yConv) * (1 - kConv);

return { r, g, b };
}

export function rgbToCmyk(r: number, g: number, b: number) {
let c = 1 - r / 255;
let m = 1 - g / 255;
let y = 1 - b / 255;
let k = Math.min(c, m, y);

if (k === 1) {
c = 0;
m = 0;
y = 0;
} else {
c = ((c - k) / (1 - k)) * 100;
m = ((m - k) / (1 - k)) * 100;
y = ((y - k) / (1 - k)) * 100;
}

k *= 100;

return {
c: Math.round(c),
m: Math.round(m),
y: Math.round(y),
k: Math.round(k),
};
}

/** Converts a decimal to a hex value */
export function convertDecimalToHex(d: string | number): string {
return Math.round(parseFloat(d as string) * 255).toString(16);
Expand Down
28 changes: 25 additions & 3 deletions src/format-input.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {
cmykToRgb,
convertHexToDecimal,
hslToRgb,
hsvToRgb,
parseIntFromHex,
rgbToRgb,
} from './conversion.js';
import { names } from './css-color-names.js';
import { HSL, HSLA, HSV, HSVA, RGB, RGBA } from './interfaces.js';
import { CMYK, HSL, HSLA, HSV, HSVA, RGB, RGBA } from './interfaces.js';
import { boundAlpha, convertToPercentage } from './util.js';

/**
Expand All @@ -25,9 +26,10 @@ import { boundAlpha, convertToPercentage } from './util.js';
* "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
* "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
* "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
* "cmyk(0, 20, 0, 0)" or "cmyk 0 20 0 0"
* ```
*/
export function inputToRGB(color: string | RGB | RGBA | HSL | HSLA | HSV | HSVA | any): {
export function inputToRGB(color: string | RGB | RGBA | HSL | HSLA | HSV | HSVA | CMYK | any): {
ok: boolean;
format: any;
r: number;
Expand Down Expand Up @@ -64,6 +66,15 @@ export function inputToRGB(color: string | RGB | RGBA | HSL | HSLA | HSV | HSVA
rgb = hslToRgb(color.h, s as number, l as number);
ok = true;
format = 'hsl';
} else if (
isValidCSSUnit(color.c) &&
isValidCSSUnit(color.m) &&
isValidCSSUnit(color.y) &&
isValidCSSUnit(color.k)
) {
rgb = cmykToRgb(color.c, color.m, color.y, color.k);
ok = true;
format = 'cmyk';
}

if (Object.prototype.hasOwnProperty.call(color, 'a')) {
Expand Down Expand Up @@ -109,6 +120,7 @@ const matchers = {
hsla: new RegExp('hsla' + PERMISSIVE_MATCH4),
hsv: new RegExp('hsv' + PERMISSIVE_MATCH3),
hsva: new RegExp('hsva' + PERMISSIVE_MATCH4),
cmyk: new RegExp('cmyk' + PERMISSIVE_MATCH4),
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
Expand All @@ -117,7 +129,7 @@ const matchers = {

/**
* Permissive string parsing. Take in a number of formats, and output an object
* based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
* based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` or `{c, m, y, k}` or `{c, m, y, k, a}`
*/
export function stringInputToObject(color: string): any {
color = color.trim().toLowerCase();
Expand Down Expand Up @@ -167,6 +179,16 @@ export function stringInputToObject(color: string): any {
return { h: match[1], s: match[2], v: match[3], a: match[4] };
}

match = matchers.cmyk.exec(color);
if (match) {
return {
c: match[1],
m: match[2],
y: match[3],
k: match[4],
};
}

match = matchers.hex8.exec(color);
if (match) {
return {
Expand Down
43 changes: 38 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { numberInputToObject, rgbaToHex, rgbToHex, rgbToHsl, rgbToHsv } from './conversion.js';
import {
numberInputToObject,
rgbaToHex,
rgbToCmyk,
rgbToHex,
rgbToHsl,
rgbToHsv,
} from './conversion.js';
import { names } from './css-color-names.js';
import { inputToRGB } from './format-input.js';
import { HSL, HSLA, HSV, HSVA, Numberify, RGB, RGBA } from './interfaces.js';
import { CMYK, HSL, HSLA, HSV, HSVA, Numberify, RGB, RGBA } from './interfaces.js';
import { bound01, boundAlpha, clamp01 } from './util.js';

export interface TinyColorOptions {
format: string;
gradientType: string;
}

export type ColorInput = string | number | RGB | RGBA | HSL | HSLA | HSV | HSVA | TinyColor;
export type ColorInput = string | number | RGB | RGBA | HSL | HSLA | HSV | HSVA | CMYK | TinyColor;

export type ColorFormats =
| 'rgb'
Expand All @@ -21,7 +28,8 @@ export type ColorFormats =
| 'hex8'
| 'name'
| 'hsl'
| 'hsv';
| 'hsv'
| 'cmyk';

export class TinyColor {
/** red */
Expand Down Expand Up @@ -297,6 +305,17 @@ export class TinyColor {
: `rgba(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%, ${this.roundA})`;
}

toCmyk(): Numberify<CMYK> {
return {
...rgbToCmyk(this.r, this.g, this.b),
};
}

toCmykString(): string {
const { c, m, y, k } = rgbToCmyk(this.r, this.g, this.b);
return `cmyk(${c}, ${m}, ${y}, ${k})`;
}

/**
* The 'real' name of the color -if there is one.
*/
Expand Down Expand Up @@ -381,6 +400,10 @@ export class TinyColor {
formattedString = this.toHsvString();
}

if (format === 'cmyk') {
formattedString = this.toCmykString();
}

return formattedString || this.toHexString();
}

Expand Down Expand Up @@ -605,6 +628,16 @@ export class TinyColor {
* compare color vs current color
*/
equals(color?: ColorInput): boolean {
return this.toRgbString() === new TinyColor(color).toRgbString();
const comparedColor = new TinyColor(color);

/**
* RGB and CMYK do not have the same color gamut, so a CMYK conversion will never be 100%.
* This means we need to compare CMYK to CMYK to ensure accuracy of the equals function.
*/
if (this.format === 'cmyk' || comparedColor.format === 'cmyk') {
return this.toCmykString() === comparedColor.toCmykString();
}

return this.toRgbString() === comparedColor.toRgbString();
}
}
12 changes: 12 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ export interface HSV {
export interface HSVA extends HSV {
a: number;
}

/**
* The CMYK color model is a subtractive color model used in the printing process.
* It described four ink palettes: Cyan, Magenta, Yellow, and Black.
* @link https://en.wikipedia.org/wiki/CMYK_color_model
*/
export interface CMYK {
c: number | string;
m: number | string;
y: number | string;
k: number | string;
}
1 change: 1 addition & 0 deletions test/conversions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('TinyColor Conversions', () => {
expect(new TinyColor(c.hex).equals(c.hsl)).toBe(true);
expect(new TinyColor(c.hex).equals(c.hsv)).toBe(true);
expect(new TinyColor(c.hsl).equals(c.hsv)).toBe(true);
expect(new TinyColor(c.cmyk).equals(c.hex)).toBe(true);
}
});
it('HSL Object', () => {
Expand Down
Loading

0 comments on commit 2927a9d

Please sign in to comment.