use std::fmt;
use color::{AlphaColor, ColorSpaceTag, DynamicColor, HueDirection, Rgba8, Srgb};
use cssparser::Parser;
use smallvec::SmallVec;
use taffy::Point;
use tiny_skia::{ColorU8, PremultipliedColorU8};
use crate::style::{
Color, ColorInput, ColorInterpolationMethod, FromCss, GradientStop, ParseResult,
ResolvedGradientStop, SizingContext, StopPosition, ToCss, fast_div_255,
};
const MIN_GRADIENT_LUT_SIZE: usize = 2;
const MAX_GRADIENT_LUT_SIZE: usize = 8193;
#[cfg(test)]
pub(crate) fn red_blue_stops(
red_hint: Option<StopPosition>,
blue_hint: Option<StopPosition>,
) -> [GradientStop; 2] {
[
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: red_hint,
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: blue_hint,
},
]
}
macro_rules! gradient_tile_accessors {
() => {
#[inline(always)]
fn width(&self) -> u32 {
self.width
}
#[inline(always)]
fn height(&self) -> u32 {
self.height
}
#[inline(always)]
fn lut_len(&self) -> usize {
self.color_lut.len()
}
#[inline(always)]
fn sample_at(&self, lut_idx: usize) -> PremultipliedColorU8 {
self.color_lut[lut_idx]
}
#[inline(always)]
fn fully_opaque(&self) -> bool {
self.fully_opaque
}
};
}
pub(crate) use gradient_tile_accessors;
pub(crate) fn compute_repeat_setup(
repeating: bool,
resolved_stops: SmallVec<[ResolvedGradientStop; 4]>,
fallback_axis: f32,
) -> (bool, f32, f32, f32, SmallVec<[ResolvedGradientStop; 4]>) {
if repeating && let (Some(first), Some(last)) = (resolved_stops.first(), resolved_stops.last()) {
let repeat_start = first.position;
let repeat_period = (last.position - first.position).max(0.0);
if repeat_period > 1e-6 {
let shifted = resolved_stops
.iter()
.map(|stop| ResolvedGradientStop {
color: stop.color,
position: stop.position - repeat_start,
})
.collect();
return (true, repeat_start, repeat_period, repeat_period, shifted);
}
}
(false, 0.0, 0.0, fallback_axis, resolved_stops)
}
pub(crate) fn parse_gradient_stops<'i>(
input: &mut Parser<'i, '_>,
parse_position: fn(&mut Parser<'i, '_>) -> ParseResult<'i, StopPosition>,
) -> ParseResult<'i, Vec<GradientStop>> {
let mut stops = Vec::new();
loop {
if let Ok(hint) = input.try_parse(parse_position) {
stops.push(GradientStop::Hint(hint));
} else {
let color = ColorInput::from_css(input)?;
let first_position = input.try_parse(parse_position).ok();
let second_position = if first_position.is_some() {
input.try_parse(parse_position).ok()
} else {
None
};
match (first_position, second_position) {
(Some(first_position), Some(second_position)) => {
stops.push(GradientStop::ColorHint {
color,
hint: Some(first_position),
});
stops.push(GradientStop::ColorHint {
color,
hint: Some(second_position),
});
}
(first_position, _) => {
stops.push(GradientStop::ColorHint {
color,
hint: first_position,
});
}
}
}
if input.try_parse(Parser::expect_comma).is_err() {
break;
}
}
Ok(stops)
}
pub(crate) fn write_gradient_css<W: fmt::Write>(
dest: &mut W,
name: &str,
params: &str,
interpolation: &ColorInterpolationMethod,
stops: &[GradientStop],
) -> fmt::Result {
dest.write_str(name)?;
dest.write_char('(')?;
let mut first = true;
if !params.is_empty() {
dest.write_str(params)?;
first = false;
}
let mut interp_buf = String::new();
interpolation.to_css(&mut interp_buf)?;
if !interp_buf.is_empty() {
if !first {
dest.write_str(", ")?;
}
dest.write_str(&interp_buf)?;
first = false;
}
for stop in stops.iter() {
if !first {
dest.write_str(", ")?;
}
stop.to_css(dest)?;
first = false;
}
dest.write_char(')')
}
pub fn interpolate_rgba(c1: Color, c2: Color, t: f32) -> Color {
if t <= f32::EPSILON {
return c1;
}
if t >= 1.0 - f32::EPSILON {
return c2;
}
let [r1, g1, b1, a1] = c1.0;
let [r2, g2, b2, a2] = c2.0;
let premul_1 = [
fast_div_255(r1 as u32 * a1 as u32),
fast_div_255(g1 as u32 * a1 as u32),
fast_div_255(b1 as u32 * a1 as u32),
a1,
];
let premul_2 = [
fast_div_255(r2 as u32 * a2 as u32),
fast_div_255(g2 as u32 * a2 as u32),
fast_div_255(b2 as u32 * a2 as u32),
a2,
];
let mut result = [0u8; 4];
for i in 0..4 {
result[i] = (premul_1[i] as f32 * (1.0 - t) + premul_2[i] as f32 * t)
.round()
.clamp(0.0, 255.0) as u8;
}
let premul = PremultipliedColorU8::from_rgba(
result[0].min(result[3]),
result[1].min(result[3]),
result[2].min(result[3]),
result[3],
)
.unwrap_or(PremultipliedColorU8::TRANSPARENT);
let demul: ColorU8 = premul.demultiply();
Color([demul.red(), demul.green(), demul.blue(), demul.alpha()])
}
pub fn interpolate_with_color_space(
c1: Color,
c2: Color,
t: f32,
color_space: ColorSpaceTag,
hue_direction: HueDirection,
) -> Color {
if c1 == c2 {
return c1;
}
if color_space == ColorSpaceTag::Srgb && hue_direction == HueDirection::Shorter {
return interpolate_rgba(c1, c2, t);
}
if t <= f32::EPSILON {
return c1;
}
if t >= 1.0 - f32::EPSILON {
return c2;
}
let dynamic_1 =
DynamicColor::from_alpha_color(AlphaColor::<Srgb>::from(Rgba8::from_u8_array(c1.0)));
let dynamic_2 =
DynamicColor::from_alpha_color(AlphaColor::<Srgb>::from(Rgba8::from_u8_array(c2.0)));
let mixed = dynamic_1
.interpolate(dynamic_2, color_space, hue_direction)
.eval(t);
let rgba = mixed.to_alpha_color::<Srgb>().to_rgba8().to_u8_array();
Color(rgba)
}
pub fn interpolate_rgba_premultiplied(
c1: PremultipliedColorU8,
c2: PremultipliedColorU8,
t: f32,
) -> PremultipliedColorU8 {
if t <= f32::EPSILON {
return c1;
}
if t >= 1.0 - f32::EPSILON {
return c2;
}
let mut result = [0u8; 4];
let c1_rgba = [c1.red(), c1.green(), c1.blue(), c1.alpha()];
let c2_rgba = [c2.red(), c2.green(), c2.blue(), c2.alpha()];
for i in 0..4 {
result[i] = (c1_rgba[i] as f32 * (1.0 - t) + c2_rgba[i] as f32 * t)
.round()
.clamp(0.0, 255.0) as u8;
}
PremultipliedColorU8::from_rgba(
result[0].min(result[3]),
result[1].min(result[3]),
result[2].min(result[3]),
result[3],
)
.unwrap_or(PremultipliedColorU8::TRANSPARENT)
}
pub trait GradientOverlayTile {
type RowState;
fn width(&self) -> u32;
fn height(&self) -> u32;
fn lut_len(&self) -> usize;
fn sample_at(&self, lut_idx: usize) -> PremultipliedColorU8;
fn sample_pixel(&self, x: u32, y: u32) -> PremultipliedColorU8;
fn begin_row(&self, src_x_start: u32, src_y: u32, lut_len: usize) -> Self::RowState;
fn next_lut_index(&self, row_state: &mut Self::RowState) -> usize;
fn fully_opaque(&self) -> bool {
false
}
}
pub fn overlay_gradient_tile_fast_normal_unconstrained<T: GradientOverlayTile>(
data: &mut [u8],
bottom_width: u32,
bottom_height: u32,
tile: &T,
offset: Point<f32>,
) {
let Some((offset_x, offset_y, dest_x_min, dest_x_max, dest_y_min, dest_y_max)) =
compute_overlay_bounds_raw(
bottom_width,
bottom_height,
offset,
tile.width(),
tile.height(),
)
else {
return;
};
let lut_len = tile.lut_len();
if lut_len == 0 {
return;
}
let pixels: &mut [[u8; 4]] = bytemuck::cast_slice_mut(data);
let row_pixels = bottom_width as usize;
let dest_x_min_usize = dest_x_min as usize;
let dest_x_max_usize = dest_x_max as usize;
let fully_opaque = tile.fully_opaque();
for dest_y in dest_y_min..dest_y_max {
let src_y = (dest_y - offset_y) as u32;
let src_x_start = (dest_x_min - offset_x) as u32;
let mut row_state = tile.begin_row(src_x_start, src_y, lut_len);
let row_start = dest_y as usize * row_pixels;
let row = &mut pixels[row_start + dest_x_min_usize..row_start + dest_x_max_usize];
if fully_opaque {
for dst in row.iter_mut() {
let lut_idx = tile.next_lut_index(&mut row_state);
debug_assert!(lut_idx < lut_len);
let pixel = tile.sample_at(lut_idx);
*dst = [pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()];
}
} else {
const CHUNK: usize = 256;
let mut buf = [[0u8; 4]; CHUNK];
let mut remaining = row;
while !remaining.is_empty() {
let n = remaining.len().min(CHUNK);
let (chunk, rest) = remaining.split_at_mut(n);
remaining = rest;
for slot in buf.iter_mut().take(n) {
let lut_idx = tile.next_lut_index(&mut row_state);
debug_assert!(lut_idx < lut_len);
let pixel = tile.sample_at(lut_idx);
*slot = [pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()];
}
for (dst, &src) in chunk.iter_mut().zip(buf[..n].iter()) {
let src_a = src[3];
let inv_src_a = (u8::MAX - src_a) as u32;
dst[0] = src[0].saturating_add(fast_div_255(dst[0] as u32 * inv_src_a));
dst[1] = src[1].saturating_add(fast_div_255(dst[1] as u32 * inv_src_a));
dst[2] = src[2].saturating_add(fast_div_255(dst[2] as u32 * inv_src_a));
dst[3] = src_a.saturating_add(fast_div_255(dst[3] as u32 * inv_src_a));
}
}
}
}
}
#[inline(always)]
fn compute_overlay_bounds_raw(
bottom_width: u32,
bottom_height: u32,
offset: Point<f32>,
width: u32,
height: u32,
) -> Option<(i32, i32, i32, i32, i32, i32)> {
if width == 0 || height == 0 {
return None;
}
let offset_x = offset.x.trunc() as i32;
let offset_y = offset.y.trunc() as i32;
let bottom_width = bottom_width as i32;
let bottom_height = bottom_height as i32;
let dest_y_min = offset_y.max(0);
let dest_y_max = (offset_y + height as i32).min(bottom_height);
if dest_y_min >= dest_y_max {
return None;
}
let dest_x_min = offset_x.max(0);
let dest_x_max = (offset_x + width as i32).min(bottom_width);
if dest_x_min >= dest_x_max {
return None;
}
Some((
offset_x, offset_y, dest_x_min, dest_x_max, dest_y_min, dest_y_max,
))
}
#[inline(always)]
fn position_to_sample_index(position: f32, axis_length: f32, lut_size: usize) -> usize {
if lut_size <= 1 || axis_length.abs() <= f32::EPSILON {
return 0;
}
let max_index = lut_size - 1;
((position.clamp(0.0, axis_length) * max_index as f32 / axis_length).round() as usize)
.min(max_index)
}
fn assign_stop_sample_indices(
resolved_stops: &[ResolvedGradientStop],
axis_length: f32,
lut_size: usize,
) -> Vec<usize> {
if resolved_stops.is_empty() || lut_size == 0 {
return Vec::new();
}
let stop_count = resolved_stops.len();
let max_index = lut_size - 1;
let mut indices = vec![0usize; stop_count];
let mut i = 0usize;
while i < stop_count {
let position = resolved_stops[i].position;
let preferred = position_to_sample_index(position, axis_length, lut_size);
let mut run_end = i + 1;
while run_end < stop_count
&& (resolved_stops[run_end].position - position).abs() <= f32::EPSILON
{
run_end += 1;
}
let run_len = run_end - i;
let run_start_index = preferred.saturating_sub(run_len.saturating_sub(1));
for (offset, slot) in indices[i..run_end].iter_mut().enumerate() {
let logical_index = run_start_index.saturating_add(offset).min(max_index);
let stop_index = i + offset;
let lower_bound = stop_index.min(max_index);
let upper_bound = max_index.saturating_sub(stop_count - 1 - stop_index);
*slot = logical_index.clamp(lower_bound, upper_bound);
}
i = run_end;
}
for i in 1..stop_count {
indices[i] = indices[i].max(indices[i - 1].saturating_add(1));
}
for i in (0..stop_count.saturating_sub(1)).rev() {
indices[i] = indices[i].min(indices[i + 1].saturating_sub(1));
}
indices
}
fn snap_stop_samples(
typed_lut: &mut [PremultipliedColorU8],
resolved_stops: &[ResolvedGradientStop],
axis_length: f32,
) {
if typed_lut.is_empty() || resolved_stops.is_empty() {
return;
}
let stop_indices = assign_stop_sample_indices(resolved_stops, axis_length, typed_lut.len());
for (stop, &sample_index) in resolved_stops.iter().zip(&stop_indices) {
typed_lut[sample_index] = stop.color.into();
}
}
#[inline(always)]
fn interpolation_position(left_position: f32, right_position: f32, sample_position: f32) -> f32 {
let denominator = right_position - left_position;
if denominator.abs() < f32::EPSILON {
return 0.0;
}
((sample_position - left_position) / denominator).clamp(0.0, 1.0)
}
pub fn build_color_lut_with_interpolation(
resolved_stops: &[ResolvedGradientStop],
axis_length: f32,
lut_size: usize,
color_space: ColorSpaceTag,
hue_direction: HueDirection,
) -> Vec<PremultipliedColorU8> {
if lut_size == 0 {
return Vec::new();
}
if resolved_stops.len() <= 1 {
let color = resolved_stops
.first()
.map(|s| s.color)
.unwrap_or(crate::style::Color::transparent());
return vec![color.into()];
}
let mut left_index = 0usize;
let mut right_index = 1usize;
let sample_step = if lut_size <= 1 {
0.0
} else {
axis_length / (lut_size - 1) as f32
};
let mut write_sample = |sample_index: usize| -> PremultipliedColorU8 {
let position_px = sample_index as f32 * sample_step;
while right_index < resolved_stops.len() && resolved_stops[right_index].position <= position_px
{
left_index = right_index;
right_index += 1;
}
let color = if right_index >= resolved_stops.len() {
resolved_stops[left_index].color
} else {
let left_stop = &resolved_stops[left_index];
let right_stop = &resolved_stops[right_index];
if left_stop.color == right_stop.color {
return left_stop.color.into();
}
let t = interpolation_position(left_stop.position, right_stop.position, position_px);
if color_space == ColorSpaceTag::Srgb && hue_direction == HueDirection::Shorter {
return interpolate_rgba_premultiplied(left_stop.color.into(), right_stop.color.into(), t);
}
interpolate_with_color_space(
left_stop.color,
right_stop.color,
t,
color_space,
hue_direction,
)
};
color.into()
};
let mut typed_lut = vec![PremultipliedColorU8::TRANSPARENT; lut_size];
for (sample_index, chunk) in typed_lut.iter_mut().enumerate() {
*chunk = write_sample(sample_index);
}
snap_stop_samples(&mut typed_lut, resolved_stops, axis_length);
typed_lut
}
pub fn adaptive_lut_size(axis_length: f32, resolved_stops: &[ResolvedGradientStop]) -> usize {
adaptive_lut_size_with_visible_samples(
(axis_length.ceil() as usize)
.saturating_add(1)
.max(MIN_GRADIENT_LUT_SIZE),
axis_length,
resolved_stops,
)
}
pub fn adaptive_lut_size_with_visible_samples(
visible_samples: usize,
axis_length: f32,
resolved_stops: &[ResolvedGradientStop],
) -> usize {
let visible_samples = visible_samples.max(MIN_GRADIENT_LUT_SIZE);
let min_interval = resolved_stops
.windows(2)
.map(|stops| stops[1].position - stops[0].position)
.filter(|interval| *interval > f32::EPSILON)
.fold(f32::INFINITY, f32::min);
let segment_aware_size = if min_interval.is_finite() {
((axis_length / min_interval).ceil() as usize)
.saturating_add(resolved_stops.len())
.saturating_add(1)
.max(MIN_GRADIENT_LUT_SIZE)
} else {
resolved_stops
.len()
.saturating_add(1)
.max(MIN_GRADIENT_LUT_SIZE)
};
let size = visible_samples
.max(segment_aware_size)
.max(resolved_stops.len().saturating_mul(2))
.max(MIN_GRADIENT_LUT_SIZE);
size.min(MAX_GRADIENT_LUT_SIZE)
}
const UNDEFINED_POSITION: f32 = -1.0;
pub fn resolve_stops_along_axis(
stops: &[GradientStop],
axis_size_px: f32,
sizing: &SizingContext,
current_color: Color,
) -> SmallVec<[ResolvedGradientStop; 4]> {
let mut resolved: SmallVec<[ResolvedGradientStop; 4]> = SmallVec::new();
let mut last_position = 0.0;
for (i, step) in stops.iter().enumerate() {
match step {
GradientStop::ColorHint {
color,
hint: Some(hint),
} => {
let position = hint.0.to_px(sizing, axis_size_px).max(last_position);
last_position = position;
resolved.push(ResolvedGradientStop {
color: color.resolve(current_color),
position,
});
}
GradientStop::ColorHint { color, hint: None } => {
resolved.push(ResolvedGradientStop {
color: color.resolve(current_color),
position: UNDEFINED_POSITION,
});
}
GradientStop::Hint(hint) => {
let Some(before) = resolved.last() else {
continue;
};
let Some(after_color) = stops.get(i + 1).and_then(|stop| match stop {
GradientStop::ColorHint { color, hint: _ } => Some(color.resolve(current_color)),
GradientStop::Hint(_) => None,
}) else {
continue;
};
let interpolated_color = interpolate_rgba(before.color, after_color, 0.5);
let position = hint.0.to_px(sizing, axis_size_px).max(last_position);
resolved.push(ResolvedGradientStop {
color: interpolated_color,
position,
});
last_position = position;
}
}
}
if resolved.is_empty() {
return resolved;
}
if resolved.len() == 1 {
if let Some(first_stop) = resolved.first_mut() {
first_stop.position = axis_size_px;
}
return resolved;
}
if let Some(first_stop) = resolved.first_mut()
&& first_stop.position == UNDEFINED_POSITION
{
first_stop.position = 0.0;
}
if let Some(last_stop) = resolved.last_mut()
&& last_stop.position == UNDEFINED_POSITION
{
last_stop.position = axis_size_px;
}
let mut i = 1usize;
while i < resolved.len() - 1 {
if resolved[i].position != UNDEFINED_POSITION {
i += 1;
continue;
}
let last_defined_position = resolved.get(i - 1).map(|s| s.position).unwrap_or(0.0);
let next_index = resolved
.iter()
.skip(i + 1)
.position(|s| s.position != UNDEFINED_POSITION)
.map(|idx| i + 1 + idx)
.unwrap_or(resolved.len() - 1);
let next_position = resolved[next_index].position;
let segments_count = (next_index - i + 1) as f32;
let step_for_each_segment = (next_position - last_defined_position) / segments_count;
for j in i..next_index {
let offset = (j - i + 1) as f32;
resolved[j].position = last_defined_position + step_for_each_segment * offset;
}
i = next_index + 1;
}
resolved
}
#[cfg(test)]
mod tests {
use crate::Viewport;
use crate::style::{Color, Length, StopPosition};
use super::*;
#[test]
fn test_resolve_stops_along_axis() {
let stops = vec![
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: Some(StopPosition(Length::Px(10.0))),
},
GradientStop::ColorHint {
color: Color([0, 255, 0, 255]).into(),
hint: Some(StopPosition(Length::Px(20.0))),
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: Some(StopPosition(Length::Percentage(30.0))),
},
];
let sizing = SizingContext::new_test(Viewport::new((40, 40)));
let width = sizing.viewport.size.width;
assert!(width.is_some());
let resolved = resolve_stops_along_axis(
&stops,
width.unwrap_or_default() as f32,
&sizing,
Color::black(),
);
assert_eq!(
resolved[0],
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 10.0,
},
);
assert_eq!(
resolved[1],
ResolvedGradientStop {
color: Color([0, 255, 0, 255]),
position: 20.0,
},
);
assert_eq!(
resolved[2],
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: 20.0, },
);
}
#[test]
fn test_distribute_evenly_between_positions() {
let stops = vec![
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: None,
},
GradientStop::ColorHint {
color: Color([0, 255, 0, 255]).into(),
hint: None,
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: None,
},
];
let sizing = SizingContext::new_test(Viewport::new((40, 40)));
let resolved = resolve_stops_along_axis(
&stops,
sizing.viewport.size.width.unwrap_or_default() as f32,
&sizing,
Color::black(),
);
assert_eq!(
resolved.as_slice(),
&[
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 0.0,
},
ResolvedGradientStop {
color: Color([0, 255, 0, 255]),
position: sizing.viewport.size.width.unwrap_or_default() as f32 / 2.0,
},
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: sizing.viewport.size.width.unwrap_or_default() as f32,
},
]
);
}
#[test]
fn test_hint_only() {
let stops = vec![
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: None,
},
GradientStop::Hint(StopPosition(Length::Percentage(10.0))),
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: None,
},
];
let sizing = SizingContext::new_test(Viewport::new((40, 40)));
let resolved = resolve_stops_along_axis(
&stops,
sizing.viewport.size.width.unwrap_or_default() as f32,
&sizing,
Color::black(),
);
assert_eq!(
resolved[0],
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 0.0,
},
);
assert_eq!(
resolved[1],
ResolvedGradientStop {
color: interpolate_rgba(Color([255, 0, 0, 255]), Color([0, 0, 255, 255]), 0.5),
position: sizing.viewport.size.width.unwrap_or_default() as f32 * 0.1,
},
);
assert_eq!(
resolved[2],
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: sizing.viewport.size.width.unwrap_or_default() as f32,
},
);
}
#[test]
fn test_adaptive_lut_size_grows_for_tight_stop_clusters() {
let resolved = [
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 0.0,
},
ResolvedGradientStop {
color: Color([0, 255, 0, 255]),
position: 0.25,
},
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: 256.0,
},
];
let size = adaptive_lut_size(256.0, &resolved);
assert!(size > 1025);
assert!(size <= MAX_GRADIENT_LUT_SIZE);
}
#[test]
fn test_build_color_lut_preserves_hard_stop_transition() {
let resolved = [
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 0.0,
},
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 8.0,
},
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: 8.0,
},
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: 16.0,
},
];
let lut = build_color_lut_with_interpolation(
&resolved,
16.0,
17,
ColorSpaceTag::Srgb,
HueDirection::Shorter,
);
assert_eq!(lut[7], Color([255, 0, 0, 255]).into());
assert_eq!(lut[8], Color([0, 0, 255, 255]).into());
}
#[test]
fn test_build_color_lut_gives_distinct_samples_to_narrow_interval() {
let resolved = [
ResolvedGradientStop {
color: Color([255, 0, 0, 255]),
position: 0.0,
},
ResolvedGradientStop {
color: Color([0, 255, 0, 255]),
position: 0.05,
},
ResolvedGradientStop {
color: Color([0, 0, 255, 255]),
position: 32.0,
},
];
let lut_size = adaptive_lut_size(32.0, &resolved);
let lut = build_color_lut_with_interpolation(
&resolved,
32.0,
lut_size,
ColorSpaceTag::Srgb,
HueDirection::Shorter,
);
let stop_indices = assign_stop_sample_indices(&resolved, 32.0, lut.len());
assert!(stop_indices[0] < stop_indices[1]);
assert_eq!(lut[stop_indices[0]], resolved[0].color.into());
assert_eq!(lut[stop_indices[1]], resolved[1].color.into());
}
#[test]
fn test_build_color_lut_remains_monotonic_for_even_spacing() {
let resolved = [
ResolvedGradientStop {
color: Color([0, 0, 0, 255]),
position: 0.0,
},
ResolvedGradientStop {
color: Color([255, 255, 255, 255]),
position: 10.0,
},
];
let lut = build_color_lut_with_interpolation(
&resolved,
10.0,
33,
ColorSpaceTag::Srgb,
HueDirection::Shorter,
);
for pair in lut.windows(2) {
assert!(pair[0].red() <= pair[1].red());
assert!(pair[0].green() <= pair[1].green());
assert!(pair[0].blue() <= pair[1].blue());
assert_eq!(pair[0].alpha(), 255);
assert_eq!(pair[1].alpha(), 255);
}
}
#[test]
fn test_interpolate_rgba_uses_premultiplied_alpha() {
let mixed = interpolate_rgba(Color([255, 255, 255, 255]), Color([0, 0, 0, 0]), 0.5);
assert_eq!(mixed, Color([255, 255, 255, 128]));
}
}