-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit cb5d5b2
Showing
30 changed files
with
4,223 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Color space conversion utilities | ||
|
||
As part of the [CSS Color Module Level 4 specification](https://drafts.csswg.org/css-color-4), two new color notation are being proposed: `lch()` and `lab()`. Some browsers are already working on implementing those! In order to use them without breaking browser support, we need to be able to convert those notations to older notations, like `rgb()` or `color()`. | ||
|
||
Those functions have been ported from the code written by [Chris Lilley](https://svgees.us/) in the CSS Color specification direclty. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
export enum ColorSpace { | ||
AdobeRGB = "AdobeRGB", | ||
CMYK = "CMYK", | ||
HSL = "HSL", | ||
Lab = "Lab", | ||
LCH = "LCH", | ||
P3 = "P3", | ||
ProPhoto = "ProPhoto", | ||
Rec2020 = "Rec2020", | ||
sRGB = "sRGB", | ||
XYZ = "XYZ", | ||
} | ||
|
||
export type RGBBasedSpace = | ||
| ColorSpace.AdobeRGB | ||
| ColorSpace.P3 | ||
| ColorSpace.ProPhoto | ||
| ColorSpace.Rec2020 | ||
| ColorSpace.sRGB; | ||
|
||
export const isRGBBased = (space: ColorSpace): space is RGBBasedSpace => | ||
space === ColorSpace.AdobeRGB || | ||
space === ColorSpace.P3 || | ||
space === ColorSpace.ProPhoto || | ||
space === ColorSpace.Rec2020 || | ||
space === ColorSpace.sRGB; | ||
|
||
export type CSSSpace = | ||
| ColorSpace.AdobeRGB | ||
| ColorSpace.HSL | ||
| ColorSpace.Lab | ||
| ColorSpace.LCH | ||
| ColorSpace.P3 | ||
| ColorSpace.ProPhoto | ||
| ColorSpace.Rec2020 | ||
| ColorSpace.sRGB; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { AdobeRGB } from "./spaces/AdobeRGB"; | ||
import { CMYK } from "./spaces/CMYK"; | ||
import { HSL } from "./spaces/HSL"; | ||
import { Lab } from "./spaces/Lab"; | ||
import { LCH } from "./spaces/LCH"; | ||
import { P3 } from "./spaces/P3"; | ||
import { ProPhoto } from "./spaces/ProPhoto"; | ||
import { Rec2020 } from "./spaces/Rec2020"; | ||
import { sRGB } from "./spaces/sRGB"; | ||
import { XYZ } from "./spaces/XYZ"; | ||
|
||
export type Color = | ||
| AdobeRGB | ||
| CMYK | ||
| HSL | ||
| Lab | ||
| LCH | ||
| P3 | ||
| ProPhoto | ||
| Rec2020 | ||
| sRGB | ||
| XYZ; | ||
export type CSSColor = | ||
| AdobeRGB | ||
| HSL | ||
| Lab | ||
| LCH | ||
| P3 | ||
| ProPhoto | ||
| Rec2020 | ||
| sRGB; | ||
|
||
export type { AdobeRGB, CMYK, HSL, Lab, LCH, P3, ProPhoto, Rec2020, sRGB, XYZ }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { sRGB } from "../spaces/sRGB"; | ||
import { P3 } from "../spaces/P3"; | ||
import { ProPhoto } from "../spaces/ProPhoto"; | ||
import { AdobeRGB } from "../spaces/AdobeRGB"; | ||
import { Rec2020 } from "../spaces/Rec2020"; | ||
import { ColorSpace } from "../color-spaces"; | ||
|
||
export type PredefinedSpaces = | ||
| ColorSpace.AdobeRGB | ||
| ColorSpace.P3 | ||
| ColorSpace.ProPhoto | ||
| ColorSpace.Rec2020 | ||
| ColorSpace.sRGB; | ||
|
||
const stringToSpace: { [key: string]: PredefinedSpaces } = { | ||
"a98-rgb": ColorSpace.AdobeRGB, | ||
"display-p3": ColorSpace.P3, | ||
"prophoto-rgb": ColorSpace.ProPhoto, | ||
rec2020: ColorSpace.Rec2020, | ||
sRGB: ColorSpace.sRGB, | ||
}; | ||
|
||
// color(space, value / ?alpha) | ||
export const parse = ( | ||
color: string | ||
): sRGB | P3 | ProPhoto | AdobeRGB | Rec2020 | null => { | ||
const parsed = color.match(/\((.*?)\)/); | ||
if (parsed === null) { | ||
return null; | ||
} | ||
const [space, ...values] = parsed[1].split(" "); | ||
const [red, green, blue] = values.map((value: string) => | ||
Number.parseFloat(value) | ||
); | ||
return { type: stringToSpace[space], values: [red, green, blue] }; | ||
}; | ||
|
||
export const stringify = ( | ||
color: AdobeRGB | P3 | ProPhoto | Rec2020 | sRGB, | ||
alpha?: string | ||
): string => { | ||
const [red, green, blue] = color.values.map((value) => value.toFixed(4)); | ||
const space = Object.entries(stringToSpace).find( | ||
([_, value]) => value === color.type | ||
); | ||
return `color(${space ? space[0] : null} ${red} ${green} ${blue}${ | ||
alpha ? ` / ${alpha}` : "" | ||
})`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { ColorSpace, isRGBBased, CSSSpace } from "../color-spaces"; | ||
import { parse } from "./parse"; | ||
import { LCH } from "../color"; | ||
import { convertColorToSpace } from "../spaces/convertColorToSpace"; | ||
import { stringify } from "./stringify"; | ||
import { forceIntoGamut } from "./gamut-correction"; | ||
|
||
const startLCH = parse("lch(60% 67 266)") as LCH; | ||
const endLCH = parse("lch(79% 73 175)") as LCH; | ||
|
||
export const convertCSSColor = (colorString: string, to: CSSSpace) => { | ||
let color = parse(colorString); | ||
|
||
if (color === null) { | ||
return null; | ||
} | ||
|
||
if (color.type === ColorSpace.LCH && isRGBBased(to)) { | ||
color = forceIntoGamut(color, to); | ||
} | ||
|
||
const converted = convertColorToSpace(color, to); | ||
return stringify(converted); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { convertColorToSpace } from "../spaces/convertColorToSpace"; | ||
import { RGBBasedSpace } from "../color-spaces"; | ||
import { LCH } from "../spaces/LCH"; | ||
|
||
const isLCHWithinRGBSpace = (color: LCH, rgbSpace: RGBBasedSpace) => { | ||
const rgb = convertColorToSpace(color, rgbSpace); | ||
const ε = 0.000005; | ||
return rgb.values.reduce( | ||
(a: boolean, b: number) => a && b >= 0 - ε && b <= 1 + ε, | ||
true | ||
); | ||
}; | ||
|
||
export const forceIntoGamut = (color: LCH, rgbSpace: RGBBasedSpace): LCH => { | ||
let [l, c, h] = color.values; | ||
// Moves an lch color into the sRGB gamut | ||
// by holding the l and h steady, | ||
// and adjusting the c via binary-search | ||
// until the color is on the sRGB boundary. | ||
if (isLCHWithinRGBSpace(color, rgbSpace)) { | ||
return color; | ||
} | ||
|
||
let hiC = c; | ||
let loC = 0; | ||
const ε = 0.0001; | ||
c /= 2; | ||
|
||
// .0001 chosen fairly arbitrarily as "close enough" | ||
while (hiC - loC > ε) { | ||
if (isLCHWithinRGBSpace({ ...color, values: [l, c, h] }, rgbSpace)) { | ||
loC = c; | ||
} else { | ||
hiC = c; | ||
} | ||
c = (hiC + loC) / 2; | ||
} | ||
|
||
return { ...color, values: [l, c, h] }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { sRGB } from "../spaces/sRGB"; | ||
import { ColorSpace } from "../color-spaces"; | ||
|
||
export const parse = (color: string): sRGB | null => { | ||
const parsed = color | ||
.replace("#", "") | ||
// shorthand notation | ||
.match(color.length < 7 ? /.{1}/g : /.{2}/g); | ||
|
||
if (parsed === null || parsed.length < 3) { | ||
return null; | ||
} | ||
const [red, green, blue] = parsed.map( | ||
(value) => Number.parseInt(value, 16) / (value.length === 1 ? 15 : 255) | ||
); | ||
|
||
return { type: ColorSpace.sRGB, values: [red, green, blue] }; | ||
}; | ||
|
||
export const stringify = (color: sRGB, alpha?: string): string => { | ||
const [red, green, blue] = color.values.map((value) => | ||
(value * 255).toString(16) | ||
); | ||
return `#${red}${green}${blue}${alpha ? `${alpha}` : ""}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { HSL } from "../spaces/HSL"; | ||
import { ColorSpace } from "../color-spaces"; | ||
|
||
// the hue needs to be a number in the half-open range [0, 6) | ||
const parseAngle = (angle: string) => { | ||
let angleValue = Number.parseFloat(angle); | ||
if (angle.includes("grad")) { | ||
angleValue /= 400; | ||
} else if (angle.includes("rad")) { | ||
angleValue /= 2 * Math.PI; | ||
} else if (angle.includes("turn")) { | ||
angleValue /= 1; | ||
} else { | ||
// let's assume it's degrees | ||
angleValue /= 360; | ||
} | ||
|
||
return (angleValue === 1 ? 0 : angleValue) * 6; | ||
}; | ||
|
||
// hsl(h s l / a) | ||
// hue is an angle (deg, grad, rad, turn) | ||
// saturation and lightness are percentages | ||
export const parse = (color: string): HSL | null => { | ||
const parsed = color.match(/^[+-]?\d+(\.\d+)?$/g); | ||
if (parsed === null || parsed.length < 3) { | ||
return null; | ||
} | ||
const [hue, saturation, lightness] = parsed.map((value) => | ||
value.includes("%") ? Number.parseFloat(value) / 100 : parseAngle(value) | ||
); | ||
return { type: ColorSpace.HSL, values: [hue, saturation, lightness] }; | ||
}; | ||
|
||
export const stringify = (color: HSL, alpha?: string): string => { | ||
const [hue, saturation, lightness] = color.values.map((value, index) => | ||
index === 0 | ||
? `${((value / 6) * 360).toFixed(4)}deg` | ||
: `${(value * 100).toFixed(2)}%` | ||
); | ||
return `hsl(${hue} ${saturation} ${lightness}${alpha ? ` / ${alpha}` : ""})`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Lab } from "../spaces/Lab"; | ||
import { ColorSpace } from "../color-spaces"; | ||
|
||
// lab(l a b / a) | ||
// l is a percentage from 0 to Infinity | ||
// a and b are unbounded numbers | ||
// we do not process alpha values | ||
export const parse = (color: string): Lab | null => { | ||
const parsed = color.match(/-?[\d.%?]+/g); | ||
if (parsed === null || parsed.length < 3) { | ||
return null; | ||
} | ||
const [L, a, b] = parsed.map(Number.parseFloat); | ||
return { type: ColorSpace.Lab, values: [L, a, b] }; | ||
}; | ||
|
||
export const stringify = (color: Lab, alpha?: string): string => { | ||
const [L, a, b] = color.values.map( | ||
(value: number, index: number) => | ||
`${value.toFixed(index === 0 ? 2 : 4)}${index === 0 ? "%" : ""}` | ||
); | ||
return `lab(${L} ${a} ${b}${alpha ? ` / ${alpha}` : ""})`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { LCH } from "../spaces/LCH"; | ||
import { ColorSpace } from "../color-spaces"; | ||
|
||
// lch(l c h / a) | ||
// l is a percentage from 0 to Infinity | ||
// c and h are unbounded numbers | ||
// we do not process alpha values | ||
export const parse = (color: string): LCH | null => { | ||
const parsed = color.match(/-?[\d.%?]+/g); | ||
if (parsed === null || parsed.length < 3) { | ||
return null; | ||
} | ||
const [l, c, h] = parsed.map(Number.parseFloat); | ||
return { type: ColorSpace.LCH, values: [l, c, h] }; | ||
}; | ||
|
||
export const stringify = (color: LCH, alpha?: string): string => { | ||
const [L, C, H] = color.values.map( | ||
(value: number, index: number) => | ||
`${value.toFixed(index === 0 ? 2 : 4)}${index === 0 ? "%" : ""}` | ||
); | ||
return `lch(${L} ${C} ${H}${alpha ? ` / ${alpha}` : ""})`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { parse as colorParse } from "./color"; | ||
import { parse as hexadecimalParse } from "./hexadecimal"; | ||
import { parse as hslParse } from "./hsl"; | ||
import { parse as labParse } from "./lab"; | ||
import { parse as lchParse } from "./lch"; | ||
import { parse as rgbParse } from "./rgb"; | ||
import { CSSColor } from "../color"; | ||
|
||
export const parse = (color: string): CSSColor | null => { | ||
if (color.includes("color(")) { | ||
return colorParse(color); | ||
} else if (color.includes("hsl(")) { | ||
return hslParse(color); | ||
} else if (color.includes("lab(")) { | ||
return labParse(color); | ||
} else if (color.includes("lch(")) { | ||
return lchParse(color); | ||
} else if (color.startsWith("#")) { | ||
return hexadecimalParse(color); | ||
} | ||
return rgbParse(color); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { sRGB } from "../spaces/sRGB"; | ||
import { ColorSpace } from "../color-spaces"; | ||
|
||
// rgb(r g b / a) | ||
// red, green and blue values can be either a percentage or a number between 0 and 255 | ||
// we do not process alpha values | ||
export const parse = (color: string): sRGB | null => { | ||
const parsed = color.match(/-?[\d.%?]+/g); | ||
if (parsed === null || parsed.length < 3) { | ||
return null; | ||
} | ||
const [red, green, blue] = parsed.map((value: string) => | ||
value.includes("%") | ||
? Number.parseFloat(value) / 100 | ||
: Number.parseFloat(value) / 255 | ||
); | ||
return { type: ColorSpace.sRGB, values: [red, green, blue] }; | ||
}; | ||
|
||
export const stringify = (color: sRGB, alpha?: string): string => { | ||
const [red, green, blue] = color.values.map( | ||
(value: number) => `${(value * 100).toFixed(2)}%` | ||
); | ||
return `rgb(${red} ${green} ${blue}${alpha ? ` / ${alpha}` : ""})`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { ColorSpace } from "../color-spaces"; | ||
import { stringify as colorStringify } from "./color"; | ||
import { stringify as hslStringify } from "./hsl"; | ||
import { stringify as labStringify } from "./lab"; | ||
import { stringify as lchStringify } from "./lch"; | ||
import { stringify as rgbStringify } from "./rgb"; | ||
import { CSSColor } from "../color"; | ||
|
||
export const stringify = (color: CSSColor): string => { | ||
switch (color.type) { | ||
case ColorSpace.AdobeRGB: | ||
case ColorSpace.P3: | ||
case ColorSpace.ProPhoto: | ||
case ColorSpace.Rec2020: | ||
return colorStringify(color); | ||
case ColorSpace.HSL: | ||
return hslStringify(color); | ||
case ColorSpace.Lab: | ||
return labStringify(color); | ||
case ColorSpace.LCH: | ||
return lchStringify(color); | ||
case ColorSpace.sRGB: | ||
return rgbStringify(color); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./color-spaces"; | ||
export * from "./spaces/convertColorToSpace"; | ||
export * from "./css/convert"; |
Oops, something went wrong.