Add notifications.
This commit is contained in:
1 parent
afad6a2636
commit
7414c89fe6
33 files changed
+1026
-381
No files matched your search
+20
-20
@@ -1,6 +1,6 @@
|
||||
//! sRGB in, sRGB out, with a perceptual space in the middle.
|
||||
//!
|
||||
//! This exists for `tint.zig`, which builds a whole palette out of one colour
|
||||
//! This exists for `tint.zig`, which builds a whole palette out of one color
|
||||
//! and therefore has to answer questions like "the same hue, a third as bright"
|
||||
//! and "one step lighter than that". Those questions have no good answer in RGB
|
||||
//! — halving the channels of a mid blue gives a navy, halving the channels of a
|
||||
@@ -14,10 +14,10 @@
|
||||
//! polar form — lightness, chroma, hue — is what a palette is really made of:
|
||||
//! hold the hue, walk the lightness, and the surfaces of a theme fall out.
|
||||
//!
|
||||
//! The one thing OKLCh will happily do that sRGB will not is name a colour that
|
||||
//! The one thing OKLCh will happily do that sRGB will not is name a color that
|
||||
//! does not exist on a monitor — a fully saturated yellow at the lightness of a
|
||||
//! midtone, say. `toRgb` deals with that by giving up chroma rather than
|
||||
//! lightness or hue: the result is the most colourful version of the colour that
|
||||
//! lightness or hue: the result is the most colorful version of the color that
|
||||
//! can actually be shown, which is what "as close as the screen gets" should
|
||||
//! mean for a palette. Clipping the channels instead would shift the hue, and a
|
||||
//! generated theme whose reds drift orange as they darken looks broken in a way
|
||||
@@ -31,13 +31,13 @@ const palette = @import("palette.zig");
|
||||
|
||||
const Rgb = palette.Rgb;
|
||||
|
||||
/// A colour in OKLCh: how light, how colourful, and which colour.
|
||||
/// A color in OKLCh: how light, how colorful, and which color.
|
||||
pub const Lch = struct {
|
||||
/// Perceptual lightness. 0 is black, 1 is white, and 0.5 looks like a
|
||||
/// midtone rather than merely being one arithmetically.
|
||||
l: f32,
|
||||
|
||||
/// Colourfulness. 0 is a grey; how high it can go before leaving sRGB
|
||||
/// Colorfulness. 0 is a gray; how high it can go before leaving sRGB
|
||||
/// depends on both the lightness and the hue, and is around 0.32 at best.
|
||||
c: f32,
|
||||
|
||||
@@ -59,17 +59,17 @@ pub fn fromRgb(rgb: Rgb) Lch {
|
||||
return .{
|
||||
.l = lab.l,
|
||||
.c = c,
|
||||
// A grey has no hue to report, and `atan2` on two zeroes is entitled to
|
||||
// A gray has no hue to report, and `atan2` on two zeroes is entitled to
|
||||
// say anything. Zero is as good as any other answer and is at least the
|
||||
// same one every time, which matters: the palette generator reads a hue
|
||||
// off the base colour and gives it to forty other colours, and a grey
|
||||
// off the base color and gives it to forty other colors, and a gray
|
||||
// base that produced a different hue on each launch would be a theme
|
||||
// that changed colour when you restarted the app.
|
||||
// that changed color when you restarted the app.
|
||||
.h = if (c < 1e-6) 0 else std.math.radiansToDegrees(std.math.atan2(lab.b, lab.a)),
|
||||
};
|
||||
}
|
||||
|
||||
/// The nearest colour a screen can show, giving up chroma before anything else.
|
||||
/// The nearest color a screen can show, giving up chroma before anything else.
|
||||
pub fn toRgb(lch: Lch) Rgb {
|
||||
const l = std.math.clamp(lch.l, 0, 1);
|
||||
const rad = std.math.degreesToRadians(lch.h);
|
||||
@@ -90,7 +90,7 @@ pub fn toRgb(lch: Lch) Rgb {
|
||||
/// is not one: it is the image of a cube through a cube root, and the closed
|
||||
/// forms for it are approximations with their own error. Twelve halvings of a
|
||||
/// range that is at most 1.0 wide lands well inside a 1/255 step, and this runs
|
||||
/// forty times when someone drags a colour picker.
|
||||
/// forty times when someone drags a color picker.
|
||||
fn fit(l: f32, c: f32, rad: f32) f32 {
|
||||
if (Lab.at(l, c, rad).inGamut()) return c;
|
||||
|
||||
@@ -133,9 +133,9 @@ const Lab = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Whether this colour is one a screen can show. The tolerance is a hair
|
||||
/// Whether this color is one a screen can show. The tolerance is a hair
|
||||
/// over half of an 8-bit step in linear light near the top of the range, so
|
||||
/// a colour that is out of gamut only by the arithmetic isn't hunted down
|
||||
/// a color that is out of gamut only by the arithmetic isn't hunted down
|
||||
/// by the bisection above for no visible gain.
|
||||
fn inGamut(self: Lab) bool {
|
||||
const lin = self.toLinear();
|
||||
@@ -188,8 +188,8 @@ fn encode(channel: f32) u8 {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
test "a colour survives the round trip" {
|
||||
// Every hue family, plus the two ends and a grey, since those are the cases
|
||||
test "a color survives the round trip" {
|
||||
// Every hue family, plus the two ends and a gray, since those are the cases
|
||||
// where the hue is undefined and the transfer function is at its steepest.
|
||||
const cases = [_][]const u8{
|
||||
"#000000", "#ffffff", "#808080", "#3a76f0", "#d2691e",
|
||||
@@ -220,14 +220,14 @@ test "black and white are where they should be" {
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0), fromRgb(.{ .r = 0, .g = 0, .b = 0 }).l, 1e-4);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 1), fromRgb(.{ .r = 255, .g = 255, .b = 255 }).l, 1e-4);
|
||||
|
||||
// A grey has no hue, and says so rather than saying whatever `atan2` makes
|
||||
// A gray has no hue, and says so rather than saying whatever `atan2` makes
|
||||
// of two zeroes.
|
||||
const grey = fromRgb(.{ .r = 128, .g = 128, .b = 128 });
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0), grey.c, 1e-3);
|
||||
try std.testing.expectEqual(@as(f32, 0), grey.h);
|
||||
const gray = fromRgb(.{ .r = 128, .g = 128, .b = 128 });
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0), gray.c, 1e-3);
|
||||
try std.testing.expectEqual(@as(f32, 0), gray.h);
|
||||
}
|
||||
|
||||
test "an impossible colour gives up chroma, not hue" {
|
||||
test "an impossible color gives up chroma, not hue" {
|
||||
// A fully saturated yellow at the lightness of a midtone: nothing like it
|
||||
// exists in sRGB, and asking for it has to produce *something*.
|
||||
const asked: Lch = .{ .l = 0.5, .c = 0.3, .h = 100 };
|
||||
@@ -237,7 +237,7 @@ test "an impossible colour gives up chroma, not hue" {
|
||||
try std.testing.expectApproxEqAbs(asked.h, got.h, 1.5);
|
||||
try std.testing.expect(got.c < asked.c);
|
||||
|
||||
// And it is still as colourful as sRGB allows, rather than having been
|
||||
// And it is still as colorful as sRGB allows, rather than having been
|
||||
// rounded down to something safe: pushing it back up leaves the gamut.
|
||||
try std.testing.expect(!Lab.at(asked.l, got.c + 0.01, std.math.degreesToRadians(asked.h)).inGamut());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user