90 lines
2.7 KiB
Zig
90 lines
2.7 KiB
Zig
//! Colour conversion.
|
|
//!
|
|
//! Config declares colours as the familiar 0xRRGGBBAA with straight alpha.
|
|
//! The two sinks want something different:
|
|
//!
|
|
//! * `river_window_v1.set_borders` takes one full-range u32 per channel —
|
|
//! river divides each by maxInt(u32) — with premultiplied alpha.
|
|
//! * wl_shm ARGB8888 wants premultiplied 8-bit channels packed into a u32.
|
|
|
|
const std = @import("std");
|
|
|
|
pub const Rgba = u32;
|
|
|
|
pub const Channels = struct {
|
|
r: u32,
|
|
g: u32,
|
|
b: u32,
|
|
a: u32,
|
|
};
|
|
|
|
fn premul8(c: u8, a: u8) u8 {
|
|
// Round to nearest rather than truncating, so 0xff at full alpha stays
|
|
// 0xff instead of drifting down.
|
|
return @intCast((@as(u32, c) * @as(u32, a) + 127) / 255);
|
|
}
|
|
|
|
/// Expand an 8-bit channel to the full u32 range: 0xff maps exactly to
|
|
/// 0xffffffff, which is what river treats as 1.0.
|
|
fn expand(c: u8) u32 {
|
|
return @as(u32, c) * 0x01010101;
|
|
}
|
|
|
|
fn split(rgba: Rgba) [4]u8 {
|
|
return .{
|
|
@intCast((rgba >> 24) & 0xff),
|
|
@intCast((rgba >> 16) & 0xff),
|
|
@intCast((rgba >> 8) & 0xff),
|
|
@intCast(rgba & 0xff),
|
|
};
|
|
}
|
|
|
|
/// Premultiplied, full-range channels for `set_borders`.
|
|
pub fn toChannels(rgba: Rgba) Channels {
|
|
const c = split(rgba);
|
|
const a = c[3];
|
|
return .{
|
|
.r = expand(premul8(c[0], a)),
|
|
.g = expand(premul8(c[1], a)),
|
|
.b = expand(premul8(c[2], a)),
|
|
.a = expand(a),
|
|
};
|
|
}
|
|
|
|
/// Premultiplied ARGB8888 as a native-endian u32, for wl_shm buffers.
|
|
pub fn toArgb8888(rgba: Rgba) u32 {
|
|
const c = split(rgba);
|
|
const a = c[3];
|
|
return (@as(u32, a) << 24) |
|
|
(@as(u32, premul8(c[0], a)) << 16) |
|
|
(@as(u32, premul8(c[1], a)) << 8) |
|
|
@as(u32, premul8(c[2], a));
|
|
}
|
|
|
|
test "opaque white survives both conversions intact" {
|
|
const ch = toChannels(0xffffffff);
|
|
try std.testing.expectEqual(@as(u32, 0xffffffff), ch.r);
|
|
try std.testing.expectEqual(@as(u32, 0xffffffff), ch.a);
|
|
try std.testing.expectEqual(@as(u32, 0xffffffff), toArgb8888(0xffffffff));
|
|
}
|
|
|
|
test "fully transparent premultiplies to zero" {
|
|
const ch = toChannels(0xffffff00);
|
|
try std.testing.expectEqual(@as(u32, 0), ch.r);
|
|
try std.testing.expectEqual(@as(u32, 0), ch.a);
|
|
try std.testing.expectEqual(@as(u32, 0), toArgb8888(0xffffff00));
|
|
}
|
|
|
|
test "opaque colour keeps its channels in argb order" {
|
|
// 0xRRGGBBAA -> 0xAARRGGBB
|
|
try std.testing.expectEqual(@as(u32, 0xff5294e2), toArgb8888(0x5294e2ff));
|
|
}
|
|
|
|
test "half alpha premultiplies channels but not alpha" {
|
|
const ch = toChannels(0xff000080);
|
|
try std.testing.expectEqual(@as(u32, 0x80808080), ch.a);
|
|
// 0xff * 0x80 / 0xff == 0x80
|
|
try std.testing.expectEqual(@as(u32, 0x80808080), ch.r);
|
|
try std.testing.expectEqual(@as(u32, 0), ch.g);
|
|
}
|