///|
/// A compact HSV color value. Hue is measured in degrees in the range 0..360,
/// saturation and value are stored in the range 0..255.
pub(all) struct HsvColor {
hue : Int
saturation : Byte
value : Byte
alpha : Byte
} derive(Eq, Debug)
///|
pub fn HsvColor::hue(self : HsvColor) -> Int {
self.hue
}
///|
pub fn HsvColor::saturation(self : HsvColor) -> Byte {
self.saturation
}
///|
pub fn HsvColor::value(self : HsvColor) -> Byte {
self.value
}
///|
pub fn HsvColor::alpha(self : HsvColor) -> Byte {
self.alpha
}
///|
/// A compact HSL color value. Hue is measured in degrees in the range 0..360.
pub(all) struct HslColor {
hue : Int
saturation : Byte
lightness : Byte
alpha : Byte
} derive(Eq, Debug)
///|
pub fn HslColor::hue(self : HslColor) -> Int {
self.hue
}
///|
pub fn HslColor::saturation(self : HslColor) -> Byte {
self.saturation
}
///|
pub fn HslColor::lightness(self : HslColor) -> Byte {
self.lightness
}
///|
pub fn HslColor::alpha(self : HslColor) -> Byte {
self.alpha
}
///|
/// Blend operators used by compositing and layer pipelines.
pub(all) enum BlendMode {
Normal
Multiply
Screen
Overlay
Darken
Lighten
Add
Subtract
Difference
Exclude
} derive(Eq, Debug)
///|
fn clamp_color_byte(value : Int) -> Byte {
if value < 0 {
0
} else if value > 255 {
255
} else {
value.to_byte()
}
}
///|
fn normalize_hue(value : Int) -> Int {
let mut result = value % 360
if result < 0 {
result += 360
}
result
}
///|
/// Convert an RGBA color to HSV using integer arithmetic for deterministic
/// results across native, JavaScript, and WebAssembly targets.
pub fn Rgba::hsv(self : Rgba) -> HsvColor {
let red = self.red().to_int()
let green = self.green().to_int()
let blue = self.blue().to_int()
let maximum = if red > green {
if red > blue {
red
} else {
blue
}
} else if green > blue {
green
} else {
blue
}
let minimum = if red < green {
if red < blue {
red
} else {
blue
}
} else if green < blue {
green
} else {
blue
}
let delta = maximum - minimum
let hue = if delta == 0 {
0
} else if maximum == red {
normalize_hue(60 * (green - blue) / delta)
} else if maximum == green {
normalize_hue(60 * (blue - red) / delta + 120)
} else {
normalize_hue(60 * (red - green) / delta + 240)
}
let saturation = if maximum == 0 { 0 } else { delta * 255 / maximum }
HsvColor::{
hue,
saturation: saturation.to_byte(),
value: maximum.to_byte(),
alpha: self.alpha(),
}
}
///|
/// Convert an RGBA color to HSL.
pub fn Rgba::hsl(self : Rgba) -> HslColor {
let red = self.red().to_int()
let green = self.green().to_int()
let blue = self.blue().to_int()
let maximum = if red > green {
if red > blue {
red
} else {
blue
}
} else if green > blue {
green
} else {
blue
}
let minimum = if red < green {
if red < blue {
red
} else {
blue
}
} else if green < blue {
green
} else {
blue
}
let delta = maximum - minimum
let sum = maximum + minimum
let lightness = sum / 2
let saturation = if delta == 0 {
0
} else if lightness < 128 {
delta * 255 / sum
} else {
delta * 255 / (510 - sum)
}
let hue = if delta == 0 {
0
} else if maximum == red {
normalize_hue(60 * (green - blue) / delta)
} else if maximum == green {
normalize_hue(60 * (blue - red) / delta + 120)
} else {
normalize_hue(60 * (red - green) / delta + 240)
}
HslColor::{
hue,
saturation: saturation.to_byte(),
lightness: lightness.to_byte(),
alpha: self.alpha(),
}
}
///|
/// Construct an RGBA value from HSV components.
pub fn rgba_from_hsv(
hue : Int,
saturation : Byte,
value : Byte,
alpha : Byte,
) -> Rgba {
let maximum = value.to_int()
if saturation == 0 {
rgba(value, value, value, alpha)
} else {
let minimum = maximum * (255 - saturation.to_int()) / 255
let delta = maximum - minimum
let sector = normalize_hue(hue) / 60
let offset = normalize_hue(hue) % 60
let rising = minimum + delta * offset / 60
let falling = maximum - delta * offset / 60
let red = match sector {
0 => maximum
1 => falling
2 => minimum
3 => minimum
4 => rising
_ => maximum
}
let green = match sector {
0 => rising
1 => maximum
2 => maximum
3 => falling
4 => minimum
_ => minimum
}
let blue = match sector {
0 => minimum
1 => minimum
2 => rising
3 => maximum
4 => maximum
_ => falling
}
rgba(red.to_byte(), green.to_byte(), blue.to_byte(), alpha)
}
}
///|
fn hsl_component(p : Int, q : Int, hue : Int) -> Int {
let h = normalize_hue(hue)
if h < 60 {
p + (q - p) * h / 60
} else if h < 180 {
q
} else if h < 240 {
p + (q - p) * (240 - h) / 60
} else {
p
}
}
///|
/// Construct an RGBA value from HSL components.
pub fn rgba_from_hsl(
hue : Int,
saturation : Byte,
lightness : Byte,
alpha : Byte,
) -> Rgba {
let light = lightness.to_int()
if saturation == 0 {
rgba(lightness, lightness, lightness, alpha)
} else {
let saturation_value = saturation.to_int()
let q = if light < 128 {
light * (255 + saturation_value) / 255
} else {
light + saturation_value - light * saturation_value / 255
}
let p = 2 * light - q
let red = hsl_component(p, q, hue + 120)
let green = hsl_component(p, q, hue)
let blue = hsl_component(p, q, hue - 120)
rgba(
clamp_color_byte(red),
clamp_color_byte(green),
clamp_color_byte(blue),
alpha,
)
}
}
///|
pub fn Rgba::premultiply(self : Rgba) -> Rgba {
let alpha = self.alpha().to_int()
rgba(
(self.red().to_int() * alpha / 255).to_byte(),
(self.green().to_int() * alpha / 255).to_byte(),
(self.blue().to_int() * alpha / 255).to_byte(),
self.alpha(),
)
}
///|
pub fn Rgba::unpremultiply(self : Rgba) -> Rgba {
let alpha = self.alpha().to_int()
if alpha == 0 {
rgba(0, 0, 0, 0)
} else {
rgba(
clamp_color_byte(self.red().to_int() * 255 / alpha),
clamp_color_byte(self.green().to_int() * 255 / alpha),
clamp_color_byte(self.blue().to_int() * 255 / alpha),
self.alpha(),
)
}
}
///|
fn clamp_color_int(value : Int) -> Int {
if value < 0 {
0
} else if value > 255 {
255
} else {
value
}
}
///|
pub fn Rgba::is_transparent(self : Rgba) -> Bool {
self.alpha() == 0
}
///|
pub fn Rgba::is_opaque(self : Rgba) -> Bool {
self.alpha() == 255
}
///|
pub fn Rgba::opaque_copy(self : Rgba) -> Rgba {
self.with_alpha(255)
}
///|
pub fn Rgba::blend_over(self : Rgba, destination : Rgba) -> Rgba {
blend_normal(self, destination)
}
///|
fn blend_channel(mode : BlendMode, source : Int, destination : Int) -> Int {
match mode {
Normal => source
Multiply => source * destination / 255
Screen => 255 - (255 - source) * (255 - destination) / 255
Overlay =>
if destination < 128 {
2 * source * destination / 255
} else {
255 - 2 * (255 - source) * (255 - destination) / 255
}
Darken => if source < destination { source } else { destination }
Lighten => if source > destination { source } else { destination }
Add => clamp_color_int(source + destination)
Subtract => clamp_color_int(destination - source)
Difference =>
if source > destination {
source - destination
} else {
destination - source
}
Exclude => destination + source - 2 * source * destination / 255
}
}
///|
fn blend_normal(source : Rgba, destination : Rgba) -> Rgba {
let source_alpha = source.alpha().to_int()
let destination_alpha = destination.alpha().to_int()
if source_alpha == 0 {
destination
} else if source_alpha == 255 {
source
} else {
let inverse = 255 - source_alpha
let output_alpha = source_alpha + destination_alpha * inverse / 255
if output_alpha == 0 {
rgba(0, 0, 0, 0)
} else {
rgba(
clamp_color_int(
(
source.red().to_int() * source_alpha +
destination.red().to_int() * destination_alpha * inverse / 255
) /
output_alpha,
).to_byte(),
clamp_color_int(
(
source.green().to_int() * source_alpha +
destination.green().to_int() * destination_alpha * inverse / 255
) /
output_alpha,
).to_byte(),
clamp_color_int(
(
source.blue().to_int() * source_alpha +
destination.blue().to_int() * destination_alpha * inverse / 255
) /
output_alpha,
).to_byte(),
output_alpha.to_byte(),
)
}
}
}
///|
/// Blend two pixels while preserving source-over alpha semantics.
pub fn blend_rgba_mode(
source : Rgba,
destination : Rgba,
mode : BlendMode,
) -> Rgba {
if mode == Normal {
blend_normal(source, destination)
} else {
let color = rgba(
blend_channel(mode, source.red().to_int(), destination.red().to_int()).to_byte(),
blend_channel(mode, source.green().to_int(), destination.green().to_int()).to_byte(),
blend_channel(mode, source.blue().to_int(), destination.blue().to_int()).to_byte(),
source.alpha(),
)
blend_normal(color, destination)
}
}
///|
/// Apply a layer using a blend mode over every overlapping pixel.
pub fn Canvas::blend(self : Canvas, layer : Canvas, mode : BlendMode) -> Canvas {
let result = self.copy()
let width = if self.width < layer.width { self.width } else { layer.width }
let height = if self.height < layer.height {
self.height
} else {
layer.height
}
for y in 0.. ColorAdjustment {
{ brightness: 0, contrast: 0, saturation: 0, hue_degrees: 0, alpha: 0 }
}
///|
pub fn ColorAdjustment::brightness(
self : ColorAdjustment,
value : Int,
) -> ColorAdjustment {
{ ..self, brightness: value }
}
///|
pub fn ColorAdjustment::contrast(
self : ColorAdjustment,
value : Int,
) -> ColorAdjustment {
{ ..self, contrast: value }
}
///|
pub fn ColorAdjustment::saturation(
self : ColorAdjustment,
value : Int,
) -> ColorAdjustment {
{ ..self, saturation: value }
}
///|
pub fn ColorAdjustment::hue(
self : ColorAdjustment,
value : Int,
) -> ColorAdjustment {
{ ..self, hue_degrees: value }
}
///|
pub fn ColorAdjustment::alpha(
self : ColorAdjustment,
value : Int,
) -> ColorAdjustment {
{ ..self, alpha: value }
}
///|
fn adjust_color(color : Rgba, adjustment : ColorAdjustment) -> Rgba {
let hsv = color.hsv()
let value = clamp_color_int(hsv.value().to_int() + adjustment.brightness)
let saturation = clamp_color_int(
hsv.saturation().to_int() + adjustment.saturation,
)
let mut result = rgba_from_hsv(
hsv.hue() + adjustment.hue_degrees,
saturation.to_byte(),
value.to_byte(),
color.alpha(),
)
if adjustment.contrast != 0 {
let factor = 255 + adjustment.contrast
result = rgba(
clamp_color_int((result.red().to_int() - 128) * factor / 255 + 128).to_byte(),
clamp_color_int((result.green().to_int() - 128) * factor / 255 + 128).to_byte(),
clamp_color_int((result.blue().to_int() - 128) * factor / 255 + 128).to_byte(),
result.alpha(),
)
}
result.with_alpha(
clamp_color_int(result.alpha().to_int() + adjustment.alpha).to_byte(),
)
}
///|
/// Apply brightness, contrast, saturation, hue, and alpha adjustments.
pub fn Canvas::adjust(self : Canvas, adjustment : ColorAdjustment) -> Canvas {
let result = self.copy()
for i in 0.. ColorMatrix {
{
values: [256, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 256, 0],
}
}
///|
pub fn ColorMatrix::from_values(values : ArrayView[Int]) -> ColorMatrix {
if values.length() != 20 {
panic()
}
let copied : Array[Int] = []
for value in values {
copied.push(value)
}
{ values: copied }
}
///|
pub fn ColorMatrix::values(self : ColorMatrix) -> ArrayView[Int] {
self.values[:]
}
///|
pub fn ColorMatrix::brightness(value : Int) -> ColorMatrix {
let result = ColorMatrix::identity()
result.values[4] = value
result.values[9] = value
result.values[14] = value
result
}
///|
pub fn ColorMatrix::grayscale() -> ColorMatrix {
{
values: [
77, 150, 29, 0, 0, 77, 150, 29, 0, 0, 77, 150, 29, 0, 0, 0, 0, 0, 256, 0,
],
}
}
///|
pub fn ColorMatrix::apply(self : ColorMatrix, color : Rgba) -> Rgba {
if self.values.length() != 20 {
color
} else {
let input = [
color.red().to_int(),
color.green().to_int(),
color.blue().to_int(),
color.alpha().to_int(),
]
let red = (
self.values[0] * input[0] +
self.values[1] * input[1] +
self.values[2] * input[2] +
self.values[3] * input[3]
) /
256 +
self.values[4]
let green = (
self.values[5] * input[0] +
self.values[6] * input[1] +
self.values[7] * input[2] +
self.values[8] * input[3]
) /
256 +
self.values[9]
let blue = (
self.values[10] * input[0] +
self.values[11] * input[1] +
self.values[12] * input[2] +
self.values[13] * input[3]
) /
256 +
self.values[14]
let alpha = (
self.values[15] * input[0] +
self.values[16] * input[1] +
self.values[17] * input[2] +
self.values[18] * input[3]
) /
256 +
self.values[19]
rgba(
clamp_color_int(red).to_byte(),
clamp_color_int(green).to_byte(),
clamp_color_int(blue).to_byte(),
clamp_color_int(alpha).to_byte(),
)
}
}
///|
pub fn Canvas::apply_matrix(self : Canvas, matrix : ColorMatrix) -> Canvas {
let result = self.copy()
for i in 0..