feat: release Colour Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import {
|
||||
alphaText,
|
||||
asColor,
|
||||
clamp,
|
||||
mappedColor,
|
||||
numberText,
|
||||
round,
|
||||
srgbChannels,
|
||||
toColourValue,
|
||||
} from "./internal";
|
||||
import { parseColour } from "./parse";
|
||||
import type {
|
||||
ColourFormat,
|
||||
ColourInput,
|
||||
ConversionRow,
|
||||
FormatColourOptions,
|
||||
SrgbPreview,
|
||||
} from "./types";
|
||||
|
||||
const FORMAT_LABELS: Record<ColourFormat, string> = {
|
||||
hex: "HEX",
|
||||
hex8: "HEX + alpha",
|
||||
rgb: "RGB",
|
||||
rgba: "RGBA",
|
||||
hsl: "HSL",
|
||||
hsv: "HSV / HSB",
|
||||
hwb: "HWB",
|
||||
cmyk: "CMYK",
|
||||
lab: "CIELAB",
|
||||
lch: "CIELCH",
|
||||
oklab: "OKLab",
|
||||
oklch: "OKLCH",
|
||||
p3: "Display P3",
|
||||
rec2020: "Rec. 2020",
|
||||
a98rgb: "Adobe RGB (1998)",
|
||||
prophoto: "ProPhoto RGB",
|
||||
"xyz-d50": "XYZ D50",
|
||||
"xyz-d65": "XYZ D65",
|
||||
css: "Original CSS",
|
||||
};
|
||||
|
||||
export const DEFAULT_CONVERSION_FORMATS: readonly ColourFormat[] = [
|
||||
"hex",
|
||||
"hex8",
|
||||
"rgb",
|
||||
"hsl",
|
||||
"hsv",
|
||||
"hwb",
|
||||
"cmyk",
|
||||
"lab",
|
||||
"lch",
|
||||
"oklab",
|
||||
"oklch",
|
||||
"p3",
|
||||
"rec2020",
|
||||
] as const;
|
||||
|
||||
function byte(value: number): number {
|
||||
return Math.round(clamp(value) * 255);
|
||||
}
|
||||
|
||||
function byteHex(value: number): string {
|
||||
return byte(value).toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
function asHex(input: ColourInput, includeAlpha: boolean): string {
|
||||
const value = parseColour(input);
|
||||
const channels = srgbChannels(value);
|
||||
const rgb = channels.map(byteHex).join("");
|
||||
return `#${rgb}${includeAlpha ? byteHex(value.alpha) : ""}`;
|
||||
}
|
||||
|
||||
function percent(value: number, precision: number): string {
|
||||
return `${numberText(value, precision)}%`;
|
||||
}
|
||||
|
||||
function functional(
|
||||
name: string,
|
||||
coords: readonly string[],
|
||||
alpha: number,
|
||||
includeAlpha: boolean,
|
||||
): string {
|
||||
return `${name}(${coords.join(" ")}${includeAlpha && alpha < 1 ? ` / ${alphaText(alpha)}` : ""})`;
|
||||
}
|
||||
|
||||
function serialiseColorFunction(
|
||||
space: string,
|
||||
coords: readonly number[],
|
||||
alpha: number,
|
||||
precision: number,
|
||||
): string {
|
||||
return `color(${space} ${coords.map((coordinate) => numberText(coordinate, precision)).join(" ")}${
|
||||
alpha < 1 ? ` / ${alphaText(alpha)}` : ""
|
||||
})`;
|
||||
}
|
||||
|
||||
export function formatColour(
|
||||
input: ColourInput,
|
||||
format: ColourFormat = "css",
|
||||
options: FormatColourOptions = {},
|
||||
): string {
|
||||
const precision = options.precision ?? 4;
|
||||
const value = parseColour(input);
|
||||
const alpha = value.alpha;
|
||||
const includeAlpha = options.includeAlpha ?? alpha < 1;
|
||||
|
||||
if (format === "hex") return asHex(value, false);
|
||||
if (format === "hex8") return asHex(value, true);
|
||||
|
||||
if (format === "rgb" || format === "rgba") {
|
||||
const channels = srgbChannels(value, options.mapToSrgb ?? true).map(byte);
|
||||
const forceAlpha = format === "rgba" || includeAlpha;
|
||||
return forceAlpha
|
||||
? `rgba(${channels.join(", ")}, ${alphaText(alpha)})`
|
||||
: `rgb(${channels.join(", ")})`;
|
||||
}
|
||||
|
||||
if (format === "cmyk") {
|
||||
const [red, green, blue] = srgbChannels(value, options.mapToSrgb ?? true);
|
||||
const black = 1 - Math.max(red, green, blue);
|
||||
const denominator = 1 - black;
|
||||
const cyan = denominator <= 1e-12 ? 0 : (1 - red - black) / denominator;
|
||||
const magenta =
|
||||
denominator <= 1e-12 ? 0 : (1 - green - black) / denominator;
|
||||
const yellow = denominator <= 1e-12 ? 0 : (1 - blue - black) / denominator;
|
||||
return functional(
|
||||
"cmyk",
|
||||
[cyan, magenta, yellow, black].map((channel) =>
|
||||
percent(channel * 100, precision),
|
||||
),
|
||||
alpha,
|
||||
includeAlpha,
|
||||
);
|
||||
}
|
||||
|
||||
const color = asColor(value);
|
||||
if (format === "hsl" || format === "hsv" || format === "hwb") {
|
||||
const converted = color.to(format);
|
||||
const [hue, first, second] = converted.coords.map((coordinate) =>
|
||||
Number(coordinate ?? 0),
|
||||
);
|
||||
return functional(
|
||||
format,
|
||||
[
|
||||
numberText(hue ?? 0, precision),
|
||||
percent(first ?? 0, precision),
|
||||
percent(second ?? 0, precision),
|
||||
],
|
||||
alpha,
|
||||
includeAlpha,
|
||||
);
|
||||
}
|
||||
|
||||
const space =
|
||||
format === "p3"
|
||||
? "p3"
|
||||
: format === "a98rgb"
|
||||
? "a98rgb"
|
||||
: format === "prophoto"
|
||||
? "prophoto"
|
||||
: format;
|
||||
|
||||
if (format === "lab" || format === "lch") {
|
||||
const converted = color.to(space);
|
||||
const coords = converted.coords.map((coordinate) =>
|
||||
Number(coordinate ?? 0),
|
||||
);
|
||||
return functional(
|
||||
format,
|
||||
[
|
||||
percent(coords[0] ?? 0, precision),
|
||||
numberText(coords[1] ?? 0, precision),
|
||||
numberText(coords[2] ?? 0, precision),
|
||||
],
|
||||
alpha,
|
||||
includeAlpha,
|
||||
);
|
||||
}
|
||||
if (format === "oklab" || format === "oklch") {
|
||||
const converted = color.to(space);
|
||||
const coords = converted.coords.map((coordinate) =>
|
||||
Number(coordinate ?? 0),
|
||||
);
|
||||
return functional(
|
||||
format,
|
||||
[
|
||||
percent((coords[0] ?? 0) * 100, precision),
|
||||
numberText(coords[1] ?? 0, precision),
|
||||
numberText(coords[2] ?? 0, precision),
|
||||
],
|
||||
alpha,
|
||||
includeAlpha,
|
||||
);
|
||||
}
|
||||
if (
|
||||
["p3", "rec2020", "a98rgb", "prophoto", "xyz-d50", "xyz-d65"].includes(
|
||||
format,
|
||||
)
|
||||
) {
|
||||
const converted = color.to(space);
|
||||
const cssSpace =
|
||||
format === "p3"
|
||||
? "display-p3"
|
||||
: format === "a98rgb"
|
||||
? "a98-rgb"
|
||||
: format === "prophoto"
|
||||
? "prophoto-rgb"
|
||||
: format;
|
||||
return serialiseColorFunction(
|
||||
cssSpace,
|
||||
converted.coords.map((coordinate) =>
|
||||
round(Number(coordinate ?? 0), precision),
|
||||
),
|
||||
alpha,
|
||||
precision,
|
||||
);
|
||||
}
|
||||
|
||||
if (format === "css") {
|
||||
return color.toString({ precision, inGamut: options.mapToSrgb === true });
|
||||
}
|
||||
|
||||
// Exhaustiveness safeguard for callers compiled against older declarations.
|
||||
return color.toString({ precision, inGamut: false });
|
||||
}
|
||||
|
||||
export function conversionRows(
|
||||
input: ColourInput,
|
||||
formats: readonly ColourFormat[] = DEFAULT_CONVERSION_FORMATS,
|
||||
): ConversionRow[] {
|
||||
return formats.map((format) => {
|
||||
const value = formatColour(input, format);
|
||||
return {
|
||||
id: format,
|
||||
label: FORMAT_LABELS[format],
|
||||
value,
|
||||
copyValue: value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const formatRows = conversionRows;
|
||||
|
||||
export function toSrgbPreview(input: ColourInput): SrgbPreview {
|
||||
const value = parseColour(input);
|
||||
const original = asColor(value);
|
||||
const mapped = mappedColor(value, "srgb");
|
||||
const rgb = mapped.coords.map((coordinate) =>
|
||||
clamp(Number(coordinate ?? 0)),
|
||||
) as [number, number, number];
|
||||
const integerChannels = rgb.map(byte);
|
||||
return {
|
||||
css:
|
||||
value.alpha < 1
|
||||
? `rgb(${integerChannels.join(" ")} / ${alphaText(value.alpha)})`
|
||||
: `rgb(${integerChannels.join(" ")})`,
|
||||
hex: formatColour(toColourValue(mapped), value.alpha < 1 ? "hex8" : "hex", {
|
||||
includeAlpha: value.alpha < 1,
|
||||
}),
|
||||
rgb,
|
||||
alpha: value.alpha,
|
||||
wasMapped: !original.inGamut("srgb"),
|
||||
};
|
||||
}
|
||||
|
||||
export function toSrgbCss(input: ColourInput): string {
|
||||
return toSrgbPreview(input).css;
|
||||
}
|
||||
Reference in New Issue
Block a user