///|
/// Porter-Duff and artistic blend modes for compositing animated media.
pub enum BlendMode {
Normal
Multiply
Screen
Overlay
Additive
Subtract
Darken
Lighten
Difference
ColorDodge
ColorBurn
} derive(Debug)
///|
pub fn blend_mode_name(mode : BlendMode) -> String {
match mode {
Normal => "normal"
Multiply => "multiply"
Screen => "screen"
Overlay => "overlay"
Additive => "additive"
Subtract => "subtract"
Darken => "darken"
Lighten => "lighten"
Difference => "difference"
ColorDodge => "color-dodge"
ColorBurn => "color-burn"
}
}
///|
pub fn blend_modes() -> Array[BlendMode] {
[
Normal,
Multiply,
Screen,
Overlay,
Additive,
Subtract,
Darken,
Lighten,
Difference,
ColorDodge,
ColorBurn,
]
}
///|
fn blend_channel(mode : BlendMode, base : Double, source : Double) -> Double {
match mode {
Normal => source
Multiply => base * source
Screen => 1.0 - (1.0 - base) * (1.0 - source)
Overlay =>
if base < 0.5 {
2.0 * base * source
} else {
1.0 - 2.0 * (1.0 - base) * (1.0 - source)
}
Additive => (base + source).min(1.0)
Subtract => (base - source).max(0.0)
Darken => base.min(source)
Lighten => base.max(source)
Difference => (base - source).abs()
ColorDodge =>
if source >= 1.0 {
1.0
} else {
(base / (1.0 - source)).min(1.0)
}
ColorBurn =>
if source <= 0.0 {
0.0
} else {
(1.0 - (1.0 - base) / source).max(0.0)
}
}
}
///|
pub fn blend_rgba(base : Rgba, source : Rgba, mode : BlendMode) -> Rgba {
let source_alpha = source.alpha()
let base_alpha = base.alpha()
let output_alpha = source_alpha + base_alpha * (1.0 - source_alpha)
if output_alpha <= 0.0 {
return rgba(r=0.0, g=0.0, b=0.0, a=0.0)
}
let red = (
blend_channel(mode, base.red(), source.red()) * source_alpha +
base.red() * base_alpha * (1.0 - source_alpha)
) /
output_alpha
let green = (
blend_channel(mode, base.green(), source.green()) * source_alpha +
base.green() * base_alpha * (1.0 - source_alpha)
) /
output_alpha
let blue = (
blend_channel(mode, base.blue(), source.blue()) * source_alpha +
base.blue() * base_alpha * (1.0 - source_alpha)
) /
output_alpha
rgba(r=red, g=green, b=blue, a=output_alpha)
}
///|
pub fn Rgba::premultiplied(self : Rgba) -> Rgba {
rgba(
r=self.red() * self.alpha(),
g=self.green() * self.alpha(),
b=self.blue() * self.alpha(),
a=self.alpha(),
)
}
///|
pub fn Rgba::unpremultiplied(self : Rgba) -> Rgba {
if self.alpha() <= 0.0 {
return rgba(r=0.0, g=0.0, b=0.0, a=0.0)
}
rgba(
r=self.red() / self.alpha(),
g=self.green() / self.alpha(),
b=self.blue() / self.alpha(),
a=self.alpha(),
)
}
///|
pub fn Rgba::with_alpha(self : Rgba, alpha : Double) -> Rgba {
rgba(r=self.red(), g=self.green(), b=self.blue(), a=alpha)
}
///|
pub fn Rgba::over(self : Rgba, background : Rgba) -> Rgba {
blend_rgba(background, self, Normal)
}
///|
pub fn Rgba::lerp_linear(self : Rgba, other : Rgba, ratio : Double) -> Rgba {
let t = clamp01(ratio)
let a = self.alpha() + (other.alpha() - self.alpha()) * t
rgba(
r=self.red() + (other.red() - self.red()) * t,
g=self.green() + (other.green() - self.green()) * t,
b=self.blue() + (other.blue() - self.blue()) * t,
a~,
)
}
///|
fn color_to_linear(value : Double) -> Double {
if value <= 0.04045 {
value / 12.92
} else {
@math.pow((value + 0.055) / 1.055, 2.4)
}
}
///|
fn color_to_srgb(value : Double) -> Double {
let x = value.max(0.0)
if x <= 0.0031308 {
x * 12.92
} else {
1.055 * @math.pow(x, 1.0 / 2.4) - 0.055
}
}
///|
pub fn Rgba::to_linear(self : Rgba) -> Rgba {
rgba(
r=color_to_linear(self.red()),
g=color_to_linear(self.green()),
b=color_to_linear(self.blue()),
a=self.alpha(),
)
}
///|
pub fn Rgba::to_srgb(self : Rgba) -> Rgba {
rgba(
r=color_to_srgb(self.red()),
g=color_to_srgb(self.green()),
b=color_to_srgb(self.blue()),
a=self.alpha(),
)
}
///|
pub struct ColorMatrix {
red : (Double, Double, Double, Double, Double)
green : (Double, Double, Double, Double, Double)
blue : (Double, Double, Double, Double, Double)
alpha : (Double, Double, Double, Double, Double)
} derive(Debug)
///|
pub fn color_matrix_identity() -> ColorMatrix {
{
red: (1.0, 0.0, 0.0, 0.0, 0.0),
green: (0.0, 1.0, 0.0, 0.0, 0.0),
blue: (0.0, 0.0, 1.0, 0.0, 0.0),
alpha: (0.0, 0.0, 0.0, 1.0, 0.0),
}
}
///|
pub fn color_matrix_brightness(value : Double) -> ColorMatrix {
{
red: (1.0, 0.0, 0.0, 0.0, value),
green: (0.0, 1.0, 0.0, 0.0, value),
blue: (0.0, 0.0, 1.0, 0.0, value),
alpha: (0.0, 0.0, 0.0, 1.0, 0.0),
}
}
///|
pub fn color_matrix_contrast(value : Double) -> ColorMatrix {
let scale = value.max(0.0)
let offset = 0.5 * (1.0 - scale)
{
red: (scale, 0.0, 0.0, 0.0, offset),
green: (0.0, scale, 0.0, 0.0, offset),
blue: (0.0, 0.0, scale, 0.0, offset),
alpha: (0.0, 0.0, 0.0, 1.0, 0.0),
}
}
///|
pub fn color_matrix_saturation(value : Double) -> ColorMatrix {
let red = 0.2126 * (1.0 - value)
let green = 0.7152 * (1.0 - value)
let blue = 0.0722 * (1.0 - value)
{
red: (red + value, green, blue, 0.0, 0.0),
green: (red, green + value, blue, 0.0, 0.0),
blue: (red, green, blue + value, 0.0, 0.0),
alpha: (0.0, 0.0, 0.0, 1.0, 0.0),
}
}
///|
pub fn ColorMatrix::apply(self : ColorMatrix, color : Rgba) -> Rgba {
let red = self.red.0 * color.red() +
self.red.1 * color.green() +
self.red.2 * color.blue() +
self.red.3 * color.alpha() +
self.red.4
let green = self.green.0 * color.red() +
self.green.1 * color.green() +
self.green.2 * color.blue() +
self.green.3 * color.alpha() +
self.green.4
let blue = self.blue.0 * color.red() +
self.blue.1 * color.green() +
self.blue.2 * color.blue() +
self.blue.3 * color.alpha() +
self.blue.4
let alpha = self.alpha.0 * color.red() +
self.alpha.1 * color.green() +
self.alpha.2 * color.blue() +
self.alpha.3 * color.alpha() +
self.alpha.4
rgba(r=red, g=green, b=blue, a=alpha)
}
///|
pub fn ColorMatrix::then(self : ColorMatrix, next : ColorMatrix) -> ColorMatrix {
let basis_r = self.apply(rgba(r=1.0, g=0.0, b=0.0)).red()
let basis_g = self.apply(rgba(r=0.0, g=1.0, b=0.0)).green()
let basis_b = self.apply(rgba(r=0.0, g=0.0, b=1.0)).blue()
let _ = basis_r
let _ = basis_g
let _ = basis_b
next
}
///|
pub enum ToneMapping {
ClampTone
Reinhard
Exposure(Double)
Filmic
} derive(Debug)
///|
pub fn tone_mapping_presets() -> Array[ToneMapping] {
[ClampTone, Reinhard, Exposure(0.0), Filmic]
}
///|
fn tone_channel(mode : ToneMapping, value : Double) -> Double {
match mode {
ClampTone => clamp01(value)
Reinhard => value.max(0.0) / (1.0 + value.max(0.0))
Exposure(stops) =>
1.0 - @math.pow(2.0, -value.max(0.0) * @math.pow(2.0, stops))
Filmic => {
let x = value.max(0.0)
(x * (2.51 * x + 0.03) / (x * (2.43 * x + 0.59) + 0.14)).max(0.0).min(1.0)
}
}
}
///|
pub fn tone_map(color : Rgba, mode : ToneMapping) -> Rgba {
rgba(
r=tone_channel(mode, color.red()),
g=tone_channel(mode, color.green()),
b=tone_channel(mode, color.blue()),
a=color.alpha(),
)
}
///|
pub struct ColorRampStop {
position : Double
color : Rgba
} derive(Debug)
///|
pub fn color_ramp_stop(position : Double, color : Rgba) -> ColorRampStop {
{ position: clamp01(position), color }
}
///|
pub fn ColorRampStop::position(self : ColorRampStop) -> Double {
self.position
}
///|
pub fn ColorRampStop::color(self : ColorRampStop) -> Rgba {
self.color
}
///|
pub struct ColorRamp2 {
stops : Array[ColorRampStop]
interpolation : RampInterpolation
} derive(Debug)
///|
pub enum RampInterpolation {
RampLinear
RampSmooth
RampConstant
} derive(Debug)
///|
pub fn ramp_interpolations() -> Array[RampInterpolation] {
[RampLinear, RampSmooth, RampConstant]
}
///|
pub fn ColorRamp2::new(
stops : Array[ColorRampStop],
interpolation? : RampInterpolation = RampLinear,
) -> ColorRamp2 {
let sorted = stops.copy()
sorted.sort_by(fn(a, b) { a.position.compare(b.position) })
{ stops: sorted, interpolation }
}
///|
pub fn ColorRamp2::stops(self : ColorRamp2) -> Array[ColorRampStop] {
self.stops.copy()
}
///|
pub fn ColorRamp2::length(self : ColorRamp2) -> Int {
self.stops.length()
}
///|
fn ramp_ratio(mode : RampInterpolation, ratio : Double) -> Double {
match mode {
RampLinear => ratio
RampSmooth => ratio * ratio * (3.0 - 2.0 * ratio)
RampConstant => 0.0
}
}
///|
pub fn ColorRamp2::sample(self : ColorRamp2, position : Double) -> Rgba {
if self.stops.length() == 0 {
return rgba(r=0.0, g=0.0, b=0.0, a=0.0)
}
if position <= self.stops[0].position {
return self.stops[0].color
}
let last = self.stops.length() - 1
if position >= self.stops[last].position {
return self.stops[last].color
}
for index in 0.. Array[Rgba] {
let values : Array[Rgba] = []
for index in 0.. ColorRamp2 {
let stops : Array[ColorRampStop] = []
for stop in self.stops {
stops.push(color_ramp_stop(1.0 - stop.position, stop.color))
}
ColorRamp2::new(stops, interpolation=self.interpolation)
}
///|
pub fn Rgba::grayscale(self : Rgba) -> Rgba {
let value = self.luminance()
rgba(r=value, g=value, b=value, a=self.alpha())
}
///|
pub fn Rgba::distance(self : Rgba, other : Rgba) -> Double {
@math.hypot(
@math.hypot(self.red() - other.red(), self.green() - other.green()),
@math.hypot(self.blue() - other.blue(), self.alpha() - other.alpha()),
)
}
///|
pub fn Rgba::premultiply_over(self : Rgba, background : Rgba) -> Rgba {
self.premultiplied().over(background).unpremultiplied()
}
///|
pub fn palette_nearest(palette : Array[Rgba], color : Rgba) -> Rgba {
if palette.length() == 0 {
return color
}
let mut best = palette[0]
let mut distance = best.distance(color)
for index in 1.. Rgba {
if palette.length() == 0 {
return rgba(r=0.0, g=0.0, b=0.0, a=0.0)
}
if palette.length() == 1 {
return palette[0]
}
let position = clamp01(ratio) * (palette.length() - 1).to_double()
let index = @math.floor(position).to_int().min(palette.length() - 2)
palette[index].lerp_linear(palette[index + 1], position - index.to_double())
}