Skip to content

Commit

Permalink
add OklabColor implementation/tests
Browse files Browse the repository at this point in the history
  • Loading branch information
soc committed Aug 4, 2024
1 parent 84027fc commit c7ef5f6
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 9 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ This library is written in Java, and runs on the JVM (≥ 17).
## Status

- RGB: done
- Oklab: in progress, see [topic/oklab](https://github.com/co-lors/colors-jvm/tree/topic/oklab)
- Oklab: done
- Oklch: next up

## Usage

Expand Down
9 changes: 7 additions & 2 deletions src/main/java/co/lors/Color.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package co.lors;

public sealed interface Color permits RgbColor {
public sealed interface Color permits RgbColor, OklabColor {

static Color parse(String value) {
if (value.startsWith("#")) {
return RgbColor.fromHex(value);
} else if (value.startsWith("rgb")) {
return RgbColor.fromCss(value);
} else if (value.startsWith("oklab")) {
return OklabColor.fromCss(value);
}
// todo: handle more colorspaces

throw new UnsupportedOperationException("Colorspace of " + value + " is unsupported!");
}

String toCss();

RgbColor toRgb();

OklabColor toOklab();

}
94 changes: 94 additions & 0 deletions src/main/java/co/lors/OklabColor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package co.lors;

/**
* A type representing an Oklab color with optional transparency.
*
* It uses the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab">Oklab</a> colorspace,
* which uses the D65 white point.
* Compared to sRGB, it can express a wider range of colors.
*/
public record OklabColor(float lightness, float aAxis, float bAxis, float alpha) implements Color {

public OklabColor {
checkUnitRange("lightness", lightness);
checkAxisRange("aAxis", aAxis);
checkAxisRange("bAxis", bAxis);
checkUnitRange("alpha", alpha);
}

public static OklabColor of(float lightness, float aAxis, float bAxis, float alpha) {
return new OklabColor(lightness, aAxis, bAxis, alpha);
}

public static OklabColor of(float lightness, float aAxis, float bAxis) {
return new OklabColor(lightness, aAxis, bAxis, 1.0f);
}

public static OklabColor fromCss(String value) {
String[] colors = value.substring(7, value.length() - 1).split(" / |, |,| ");
float lightness = Float.parseFloat(colors[0].trim());
float aAxis = Float.parseFloat(colors[1].trim());
float bAxis = Float.parseFloat(colors[2].trim());
float alpha = 1.0f;
if (colors.length > 3) {
alpha = Float.parseFloat(colors[3].trim());
}
return OklabColor.of(lightness, aAxis, bAxis, alpha);
}

@Override
public String toCss() {
String alphaString = alpha == 0.0f ? "" : " / " + alpha;
return "oklab(" + lightness + " " + aAxis + " " + bAxis + alphaString + ")";
}

@Override
public RgbColor toRgb() {
double l = lightness + 0.3963377774 * aAxis + 0.2158037573 * bAxis;
double m = lightness - 0.1055613458 * aAxis - 0.0638541728 * bAxis;
double s = lightness - 0.0894841775 * aAxis - 1.2914855480 * bAxis;

double l3 = l * l * l;
double m3 = m * m * m;
double s3 = s * s * s;

double red = +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
double green = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
double blue = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
return RgbColor.of(scaleToByte(red), scaleToByte(green), scaleToByte(blue), (int) (alpha * 255));
}

@Override
public OklabColor toOklab() {
return this;
}

private static double gamma(double value) {
return value >= 0.0031308
? 1.055 * Math.pow(value, 1 / 2.4) - 0.055
: 12.92 * value;
}

private static int scaleToByte(double value) {
return Math.max(0, Math.min(255, (int) (Math.round(gamma(value) * 255))));
}

private static void checkUnitRange(String name, float value) {
if (value < 0) {
throw new IllegalArgumentException(name + " cannot be less than 0, but was " + value);
}
if (value > 1) {
throw new IllegalArgumentException(name + " cannot be greater than 1, but was " + value);
}
}

private static void checkAxisRange(String name, float value) {
if (value < -0.4f) {
throw new IllegalArgumentException(name + " cannot be less than -0.4, but was " + value);
}
if (value > 0.4f) {
throw new IllegalArgumentException(name + " cannot be greater than 0.4, but was " + value);
}
}

}
42 changes: 36 additions & 6 deletions src/main/java/co/lors/RgbColor.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ public static RgbColor fromHex(String value) {
if (value.length() == 3 || value.length() == 6 || value.length() == 8) {
value = '#' + value;
}
int intval = Integer.decode(value);
long intval = Long.decode(value);
if (value.length() == 4) { // color shorthand?
int red = intval & 0xF00;
int green = intval & 0x0F0;
int blue = intval & 0x00F;
int red = (int) (intval & 0xF00);
int green = (int) (intval & 0x0F0);
int blue = (int) (intval & 0x00F);
intval = red << 20 | red << 16;
intval |= green << 16 | green << 12;
intval |= blue << 12 | blue << 8;
intval |= 0xFF;
} else if (value.length() == 7) { // alpha missing?
intval = (intval << 8) | 0xFF;
}
return new RgbColor(intval);
return new RgbColor((int) intval);
}

/** `RRGGBBAA` */
Expand All @@ -74,7 +74,7 @@ public String toHexWithHash() {

/** `rgb(r, g, b, a)` */
public static RgbColor fromCss(String value) {
String[] colors = value.substring(4, value.length() - 1).split(",");
String[] colors = value.substring(4, value.length() - 1).split(" / |, |,| ");
int r = Integer.parseInt(colors[0].trim());
int g = Integer.parseInt(colors[1].trim());
int b = Integer.parseInt(colors[2].trim());
Expand All @@ -93,6 +93,36 @@ public String toCss() {
return "rgb(" + red() + ", " + green() + ", " + blue() + ", " + df.format(alphaScaled()) + ")";
}

@Override
public RgbColor toRgb() {
return this;
}

@Override
public OklabColor toOklab() {
double r = gammaInv(red() / 255.0);
double g = gammaInv(green() / 255.0);
double b = gammaInv(blue() / 255.0);

double l3 = 0.4121656120 * r + 0.5362752080 * g + 0.0514575653 * b;
double m3 = 0.2118591070 * r + 0.6807189584 * g + 0.1074065790 * b;
double s3 = 0.0883097947 * r + 0.2818474174 * g + 0.6302613616 * b;

double l = Math.cbrt(l3);
double m = Math.cbrt(m3);
double s = Math.cbrt(s3);

return new OklabColor(
(float) (0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s),
(float) (1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s),
(float) (0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s),
(float) alphaScaled());
}

private static double gammaInv(double r) {
return (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
}

public int red() {
return (value >> 24) & 0xFF;
}
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/co/lors/ColorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.lors;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ColorTest {

@Test
void parse() {
assertEquals(RgbColor.of(255, 204, 153), Color.parse("#FFCC99"));
assertEquals(RgbColor.of(255, 204, 153, 34), Color.parse("#FFCC9922"));

assertEquals(RgbColor.of(255, 204, 153), Color.parse("rgb(255, 204, 153)"));
assertEquals(RgbColor.of(255, 204, 153, 34), Color.parse("rgb(255, 204, 153 / 0.133)"));

assertEquals(OklabColor.of(0.700f, 0.200f, 0.000f), Color.parse("oklab(0.7 0.2 0.0)"));
assertEquals(OklabColor.of(0.700f, 0.200f, 0.000f, 1.0f), Color.parse("oklab(0.7 0.2 0.0 / 1.0)"));
}

}
54 changes: 54 additions & 0 deletions src/test/java/co/lors/OklabColorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package co.lors;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class OklabColorTest {

public static final OklabColor COLOR1 = OklabColor.of(0.700f, 0.200f, 0.000f, 1.0f);
public static final OklabColor COLOR2 = OklabColor.of(0.500f, 0.100f, 0.100f, 1.0f);

@Test
void fromCss() {
assertEquals(COLOR1, OklabColor.fromCss("oklab(0.7 0.2 0.0 / 1.0)"));
assertEquals(COLOR2, OklabColor.fromCss("oklab(0.5 0.1 0.1 / 1.0)"));
}

@Test
void toCss() {
assertEquals(COLOR1.toCss(), "oklab(0.7 0.2 0.0 / 1.0)");
assertEquals(COLOR2.toCss(), "oklab(0.5 0.1 0.1 / 1.0)");
}

@Test
void toRgb() {
assertEquals(COLOR1.toRgb(), RgbColor.of(251, 92, 153, 255));
assertEquals(COLOR2.toRgb(), RgbColor.of(161, 66, 3, 255));
}

@Test
void lightness() {
assertEquals(COLOR1.lightness(), 0.7f);
assertEquals(COLOR2.lightness(), 0.5f);
}

@Test
void aAxis() {
assertEquals(COLOR1.aAxis(), 0.2f);
assertEquals(COLOR2.aAxis(), 0.1f);
}

@Test
void bAxis() {
assertEquals(COLOR1.bAxis(), 0.0f);
assertEquals(COLOR2.bAxis(), 0.1f);
}

@Test
void alpha() {
assertEquals(COLOR1.alpha(), 1.0f);
assertEquals(COLOR2.alpha(), 1.0f);
}

}
40 changes: 40 additions & 0 deletions src/test/java/co/lors/RgbColorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,46 @@ public void toHexHash5NoAlpha() {

@Test
public void fromCss() {
assertEquals(RgbColor.BLACK, RgbColor.fromCss("rgb(0 0 0 1.0)"));
assertEquals(RgbColor.WHITE, RgbColor.fromCss("rgb(255 255, 255 1.0)"));
assertEquals(RgbColor.RED, RgbColor.fromCss("rgb(255 0 0 1.0)"));
assertEquals(RgbColor.GREEN, RgbColor.fromCss("rgb(0 255 0 1.0)"));
assertEquals(RgbColor.BLUE, RgbColor.fromCss("rgb(0 0 255 1.0)"));

assertEquals(RgbColor.of(0, 0, 0, 0), RgbColor.fromCss("rgb(0 0 0 0.0)"));
assertEquals(RgbColor.of(0, 0, 0, 1), RgbColor.fromCss("rgb(0 0 0 0.004)"));
assertEquals(RgbColor.of(0, 0, 0, 2), RgbColor.fromCss("rgb(0 0 0 0.008)"));
assertEquals(RgbColor.of(0, 0, 0, 11), RgbColor.fromCss("rgb(0 0 0 0.043)"));
assertEquals(RgbColor.of(0, 0, 0, 23), RgbColor.fromCss("rgb(0 0 0 0.09)"));
assertEquals(RgbColor.of(0, 0, 0, 42), RgbColor.fromCss("rgb(0 0 0 0.165)"));
assertEquals(RgbColor.of(0, 0, 0, 59), RgbColor.fromCss("rgb(0 0 0 0.231)"));
assertEquals(RgbColor.of(0, 0, 0, 113), RgbColor.fromCss("rgb(0 0 0 0.443)"));
assertEquals(RgbColor.of(0, 0, 0, 127), RgbColor.fromCss("rgb(0 0 0 0.498)"));
assertEquals(RgbColor.of(0, 0, 0, 128), RgbColor.fromCss("rgb(0 0 0 0.502)"));
assertEquals(RgbColor.of(0, 0, 0, 129), RgbColor.fromCss("rgb(0 0 0 0.506)"));
assertEquals(RgbColor.of(0, 0, 0, 253), RgbColor.fromCss("rgb(0 0 0 0.992)"));
assertEquals(RgbColor.of(0, 0, 0, 254), RgbColor.fromCss("rgb(0 0 0 0.996)"));
assertEquals(RgbColor.of(0, 0, 0, 255), RgbColor.fromCss("rgb(0 0 0 1.0)"));

// values without rounding to max 3 fractional digits
assertEquals(RgbColor.of(0, 0, 0, 0), RgbColor.fromCss("rgb(0 0 0 0.0)"));
assertEquals(RgbColor.of(0, 0, 0, 1), RgbColor.fromCss("rgb(0 0 0 0.00392156862745098)"));
assertEquals(RgbColor.of(0, 0, 0, 2), RgbColor.fromCss("rgb(0 0 0 0.00784313725490196)"));
assertEquals(RgbColor.of(0, 0, 0, 11), RgbColor.fromCss("rgb(0 0 0 0.043137254901960784)"));
assertEquals(RgbColor.of(0, 0, 0, 23), RgbColor.fromCss("rgb(0 0 0 0.09019607843137255)"));
assertEquals(RgbColor.of(0, 0, 0, 42), RgbColor.fromCss("rgb(0 0 0 0.16470588235294117)"));
assertEquals(RgbColor.of(0, 0, 0, 59), RgbColor.fromCss("rgb(0 0 0 0.23137254901960785)"));
assertEquals(RgbColor.of(0, 0, 0, 113), RgbColor.fromCss("rgb(0 0 0 0.44313725490196076)"));
assertEquals(RgbColor.of(0, 0, 0, 127), RgbColor.fromCss("rgb(0 0 0 0.4980392156862745)"));
assertEquals(RgbColor.of(0, 0, 0, 128), RgbColor.fromCss("rgb(0 0 0 0.5019607843137255)"));
assertEquals(RgbColor.of(0, 0, 0, 129), RgbColor.fromCss("rgb(0 0 0 0.5058823529411764)"));
assertEquals(RgbColor.of(0, 0, 0, 253), RgbColor.fromCss("rgb(0 0 0 0.9921568627450981)"));
assertEquals(RgbColor.of(0, 0, 0, 254), RgbColor.fromCss("rgb(0 0 0 0.996078431372549)"));
assertEquals(RgbColor.of(0, 0, 0, 255), RgbColor.fromCss("rgb(0 0 0 1.0)"));
}

@Test
public void fromCssLegacyCommaSyntax() {
assertEquals(RgbColor.BLACK, RgbColor.fromCss("rgb(0, 0, 0, 1.0)"));
assertEquals(RgbColor.WHITE, RgbColor.fromCss("rgb(255, 255, 255, 1.0)"));
assertEquals(RgbColor.RED, RgbColor.fromCss("rgb(255, 0, 0, 1.0)"));
Expand Down

0 comments on commit c7ef5f6

Please sign in to comment.