Add notifications.

This commit is contained in:
Greyson Parrelli committed 2026-08-26 23:24:14 -04:00
1 parent afad6a2636
commit 7414c89fe6
33 files changed
+1026 -381

No files matched your search

+20 -20
View File
@@ -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());
}