Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 2.10.1

- Fix: Read the alpha byte of a HEX color exactly, so `#cc88` no longer comes back as `#cccc8887` ❤️ [@spokodev](https://github.com/spokodev)
- Fix: Stop `minify` from turning down an alpha hex that holds the color exactly, and from returning nothing at all when every notation is switched off

### 2.10.0

- Improve `mix` plugin by adding an optional `"rgb"` interpolation mode to `mix`, `tints`, `tones` and `shades`
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/cmyk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RgbaColor, InputObject, CmykaColor } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, isPresent, round } from "../helpers";
import { clamp, isPresent, round, roundAlpha } from "../helpers";

/**
* Clamps the CMYK color object values.
Expand All @@ -21,7 +20,7 @@ export const roundCmyka = (cmyka: CmykaColor): CmykaColor => ({
m: round(cmyka.m, 2),
y: round(cmyka.y, 2),
k: round(cmyka.k, 2),
a: round(cmyka.a, ALPHA_PRECISION),
a: roundAlpha(cmyka.a),
});

/**
Expand Down
4 changes: 2 additions & 2 deletions src/colorModels/hex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const parseHex = (hex: string): RgbaColor | null => {
r: digitAt(hex, 1) * 0x11,
g: digitAt(hex, 2) * 0x11,
b: digitAt(hex, 3) * 0x11,
a: length === 5 ? round((digitAt(hex, 4) * 0x11) / 255, 2) : 1,
a: length === 5 ? (digitAt(hex, 4) * 0x11) / 255 : 1,
};
}

Expand All @@ -47,7 +47,7 @@ export const parseHex = (hex: string): RgbaColor | null => {
r: byteAt(hex, 1),
g: byteAt(hex, 3),
b: byteAt(hex, 5),
a: length === 9 ? round(byteAt(hex, 7) / 255, 2) : 1,
a: length === 9 ? byteAt(hex, 7) / 255 : 1,
};
}

Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/hsl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InputObject, RgbaColor, HslaColor, HsvaColor } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, clampHue, round, roundHue, isPresent } from "../helpers";
import { clamp, clampHue, round, roundAlpha, roundHue, isPresent } from "../helpers";
import { hsvaToRgba, rgbaToHsva } from "./hsv";

export const clampHsla = (hsla: HslaColor): HslaColor => ({
Expand All @@ -14,7 +13,7 @@ export const roundHsla = (hsla: HslaColor): HslaColor => ({
h: roundHue(hsla.h),
s: round(hsla.s),
l: round(hsla.l),
a: round(hsla.a, ALPHA_PRECISION),
a: roundAlpha(hsla.a),
});

export const parseHsla = ({ h, s, l, a = 1 }: InputObject): RgbaColor | null => {
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/hsv.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InputObject, RgbaColor, HsvaColor } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, clampHue, isPresent, round, roundHue } from "../helpers";
import { clamp, clampHue, isPresent, round, roundAlpha, roundHue } from "../helpers";

export const clampHsva = (hsva: HsvaColor): HsvaColor => ({
h: clampHue(hsva.h),
Expand All @@ -13,7 +12,7 @@ export const roundHsva = (hsva: HsvaColor): HsvaColor => ({
h: roundHue(hsva.h),
s: round(hsva.s),
v: round(hsva.v),
a: round(hsva.a, ALPHA_PRECISION),
a: roundAlpha(hsva.a),
});

export const parseHsva = ({ h, s, v, a = 1 }: InputObject): RgbaColor | null => {
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/hwb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RgbaColor, HwbaColor, InputObject } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, clampHue, round, roundHue, isPresent } from "../helpers";
import { clamp, clampHue, round, roundAlpha, roundHue, isPresent } from "../helpers";
import { hsvaToRgba, rgbaToHsva } from "./hsv";

export const clampHwba = (hwba: HwbaColor): HwbaColor => ({
Expand All @@ -14,7 +13,7 @@ export const roundHwba = (hwba: HwbaColor): HwbaColor => ({
h: roundHue(hwba.h),
w: round(hwba.w),
b: round(hwba.b),
a: round(hwba.a, ALPHA_PRECISION),
a: roundAlpha(hwba.a),
});

export const rgbaToHwba = (rgba: RgbaColor): HwbaColor => {
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/lab.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RgbaColor, LabaColor, InputObject } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, isPresent, round } from "../helpers";
import { clamp, isPresent, round, roundAlpha } from "../helpers";
import { D50, rgbaToXyza, xyzaToRgba } from "./xyz";

// Conversion factors from https://en.wikipedia.org/wiki/CIELAB_color_space
Expand All @@ -26,7 +25,7 @@ export const roundLaba = (laba: LabaColor): LabaColor => ({
l: round(laba.l, 2),
a: round(laba.a, 2),
b: round(laba.b, 2),
alpha: round(laba.alpha, ALPHA_PRECISION),
alpha: roundAlpha(laba.alpha),
});

export const parseLaba = ({ l, a, b, alpha = 1 }: InputObject): RgbaColor | null => {
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/lch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RgbaColor, InputObject, LchaColor } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, clampHue, isPresent, round, roundHue } from "../helpers";
import { clamp, clampHue, isPresent, round, roundAlpha, roundHue } from "../helpers";
import { labaToRgba, rgbaToLaba } from "./lab";

/**
Expand All @@ -19,7 +18,7 @@ export const roundLcha = (laba: LchaColor): LchaColor => ({
l: round(laba.l, 2),
c: round(laba.c, 2),
h: roundHue(laba.h, 2),
a: round(laba.a, ALPHA_PRECISION),
a: roundAlpha(laba.a),
});

export const parseLcha = ({ l, c, h, a = 1 }: InputObject): RgbaColor | null => {
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/rgb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InputObject, RgbaColor } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { round, clamp, isPresent } from "../helpers";
import { round, roundAlpha, clamp, isPresent } from "../helpers";

export const clampRgba = (rgba: RgbaColor): RgbaColor => ({
r: clamp(rgba.r, 0, 255),
Expand All @@ -13,7 +12,7 @@ export const roundRgba = (rgba: RgbaColor): RgbaColor => ({
r: round(rgba.r),
g: round(rgba.g),
b: round(rgba.b),
a: round(rgba.a, ALPHA_PRECISION),
a: roundAlpha(rgba.a),
});

export const parseRgba = ({ r, g, b, a = 1 }: InputObject): RgbaColor | null => {
Expand Down
5 changes: 2 additions & 3 deletions src/colorModels/xyz.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InputObject, RgbaColor, XyzColor, XyzaColor } from "../types";
import { ALPHA_PRECISION } from "../constants";
import { clamp, isPresent, round } from "../helpers";
import { clamp, isPresent, round, roundAlpha } from "../helpers";
import { clampRgba, linearizeRgbChannel, unlinearizeRgbChannel } from "./rgb";

// Theoretical light source that approximates "warm daylight" and follows the CIE standard.
Expand All @@ -25,7 +24,7 @@ export const roundXyza = (xyza: XyzaColor): XyzaColor => ({
x: round(xyza.x, 2),
y: round(xyza.y, 2),
z: round(xyza.z, 2),
a: round(xyza.a, ALPHA_PRECISION),
a: roundAlpha(xyza.a),
});

export const parseXyza = ({ x, y, z, a = 1 }: InputObject): RgbaColor | null => {
Expand Down
5 changes: 2 additions & 3 deletions src/colord.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Input, AnyColor, RgbaColor, HslaColor, HsvaColor } from "./types";
import { round, roundHue } from "./helpers";
import { ALPHA_PRECISION } from "./constants";
import { round, roundAlpha, roundHue } from "./helpers";
import { parse } from "./parse";
import { rgbaToHex } from "./colorModels/hex";
import { roundRgba } from "./colorModels/rgb";
Expand Down Expand Up @@ -162,7 +161,7 @@ export class Colord {
public alpha(value: number): Colord;
public alpha(value?: number): Colord | number {
if (typeof value === "number") return colord(changeAlpha(this.rgba, value));
return round(this.rgba.a, ALPHA_PRECISION);
return roundAlpha(this.rgba.a);
}

/**
Expand Down
6 changes: 0 additions & 6 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* We used to work with 2 digits after the decimal point, but it wasn't accurate enough,
* so the library produced colors that were perceived differently.
*/
export const ALPHA_PRECISION = 3;

/**
* Valid CSS <angle> units.
* https://developer.mozilla.org/en-US/docs/Web/CSS/angle
Expand Down
16 changes: 16 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ export const floor = (number: number, digits = 0, base = Math.pow(10, digits)):
return Math.floor(base * number) / base + 0;
};

/**
* Rounds an alpha value for output.
*
* Two digits are not enough: an alpha read from a HEX is an exact 8-bit ratio, and
* rounding 155 of the 256 of them that way sends the color back as a different byte.
* Three digits hold every byte, but the short form is kept wherever it names the very
* same one, which is how 0x80 stays 0.5 rather than 0.502 — the way browsers print it.
*
* The factors are spelled out instead of calling `round`, which recomputes
* `Math.pow(10, digits)` on every call, and this runs on the way out of every model.
*/
export const roundAlpha = (alpha: number): number => {
const short = Math.round(alpha * 100) / 100;
return Math.round(short * 255) === alpha * 255 ? short : Math.round(alpha * 1000) / 1000;
};

/**
* Clamps a value between an upper and lower bound.
* We use ternary operators because it makes the minified code
Expand Down
18 changes: 10 additions & 8 deletions src/plugins/minify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Colord } from "../colord";
import { Plugin } from "../extend";
import { round } from "../helpers";
import { roundAlpha } from "../helpers";

interface MinificationOptions {
hex?: boolean;
Expand Down Expand Up @@ -28,8 +28,10 @@ const minifyPlugin: Plugin = (ColordClass): void => {
const alpha = instance.alpha();
const [, r1, r2, g1, g2, b1, b2, a1, a2] = hex.split("");

// Make sure conversion is lossless
if (alpha > 0 && alpha < 1 && round(parseInt(a1 + a2, 16) / 255, 2) !== alpha) return null;
// Make sure conversion is lossless: the alpha byte has to read back as the very
// alpha this color has. It is the same rounding rule the whole library prints
// through, so 0x88 keeps its own byte instead of drifting into 0x87.
if (alpha > 0 && alpha < 1 && roundAlpha(parseInt(a1 + a2, 16) / 255) !== alpha) return null;

// Check if the string can be shorten
if (r1 === r2 && g1 === g2 && b1 === b2) {
Expand Down Expand Up @@ -92,10 +94,10 @@ const minifyPlugin: Plugin = (ColordClass): void => {
if (hex) variants.push(hex);
}

// rgb() functional notation with no spaces
if (settings.rgb) {
variants.push(a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`);
}
// rgb() functional notation with no spaces. It is the one form that can hold
// any color, so it doubles as the answer when every other one is turned down
const rgbVariant = a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
if (settings.rgb) variants.push(rgbVariant);

// hsl() functional notation with no spaces
if (settings.hsl) {
Expand All @@ -111,7 +113,7 @@ const minifyPlugin: Plugin = (ColordClass): void => {
if (name) variants.push(name);
}

return findShortestString(variants);
return variants.length ? findShortestString(variants) : rgbVariant;
};
};

Expand Down
Loading
Loading