use crate::blend::BlendMode;
pub const MATERIAL_FLOATS: usize = 64;
const _: () = assert!(
MATERIAL_FLOATS * 4 <= 16384,
"a material must fit the 16 KiB of uniform buffer range every device guarantees"
);
pub const MAX_STOPS: usize = 4;
pub mod layout {
pub const STOPS: usize = 0;
pub const OFFSETS: usize = 16;
pub const GEOMETRY: usize = 20;
pub const TO_LOCAL: usize = 24;
pub const PARAMS: usize = 36;
pub const FILTER: usize = 40;
pub const FILTER_OFFSET: usize = 56;
pub const FILTER_PARAMS: usize = 60;
pub const DITHER: usize = 62;
}
fn stop_count_code(count: usize, ramp: &Option<u32>) -> f32 {
match ramp {
Some(_) => 0.0,
None => count.max(1) as f32,
}
}
fn tile_code(tile: TileMode) -> f32 {
match tile {
TileMode::Clamp => tile::CLAMP,
TileMode::Repeat => tile::REPEAT,
TileMode::Decal => tile::DECAL,
TileMode::Mirror => tile::MIRROR,
}
}
pub mod kind {
pub const SOLID: f32 = 0.0;
pub const LINEAR: f32 = 1.0;
pub const RADIAL: f32 = 2.0;
pub const SWEEP: f32 = 3.0;
pub const IMAGE: f32 = 4.0;
pub const GLYPH: f32 = 5.0;
pub const BLUR: f32 = 6.0;
pub const ROUNDED_RECT: f32 = 7.0;
pub const ELLIPSE: f32 = 8.0;
pub const CONICAL: f32 = 9.0;
pub const MESH: f32 = 10.0;
pub const MORPHOLOGY: f32 = 11.0;
pub const ROUNDED_RECT_BLUR: f32 = 12.0;
pub const POINT_FIELD: f32 = 13.0;
}
pub const MAX_EFFECT_TEXTURES: usize = 4;
pub const MORPHOLOGY_TAPS: u32 = 32;
pub const RUNTIME_FLOATS: usize = MATERIAL_FLOATS;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Sampling {
#[default]
Linear,
Nearest,
Cubic,
Mipmap,
}
fn sampling_code(sampling: Sampling) -> f32 {
match sampling {
Sampling::Linear => sampling::LINEAR,
Sampling::Nearest => sampling::NEAREST,
Sampling::Cubic => sampling::CUBIC,
Sampling::Mipmap => sampling::MIPMAP,
}
}
pub mod sampling {
pub const LINEAR: f32 = 0.0;
pub const NEAREST: f32 = 1.0;
pub const CUBIC: f32 = 2.0;
pub const MIPMAP: f32 = 3.0;
}
pub mod tile {
pub const CLAMP: f32 = 0.0;
pub const REPEAT: f32 = 1.0;
pub const DECAL: f32 = 2.0;
pub const MIRROR: f32 = 3.0;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorForm {
#[default]
Premultiplied,
Straight,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ColorFilter {
#[default]
None,
Matrix {
columns: [[f32; 4]; 4],
offset: [f32; 4],
form: ColorForm,
},
Blend { color: [f32; 4], mode: BlendMode },
Gamma { direction: Gamma },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gamma {
LinearToSrgb,
SrgbToLinear,
}
impl ColorFilter {
pub fn matrix(rows: [f32; 20]) -> Self {
let mut columns = [[0.0f32; 4]; 4];
let mut offset = [0.0f32; 4];
for (out, row) in rows.chunks_exact(5).enumerate() {
for (channel, value) in row[..4].iter().enumerate() {
columns[channel][out] = *value;
}
offset[out] = row[4];
}
Self::Matrix {
columns,
offset,
form: ColorForm::Straight,
}
}
pub fn linear_to_srgb() -> Self {
Self::Gamma {
direction: Gamma::LinearToSrgb,
}
}
pub fn srgb_to_linear() -> Self {
Self::Gamma {
direction: Gamma::SrgbToLinear,
}
}
pub fn blend(color: [f32; 4], mode: BlendMode) -> Self {
use BlendMode as B;
let alpha = color[3];
let s = [color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha];
let zero = [[0.0f32; 4]; 4];
let scaled = |k: f32| {
let mut m = zero;
for (i, column) in m.iter_mut().enumerate() {
column[i] = k;
}
m
};
let with_alpha_column = |mut m: [[f32; 4]; 4], column: [f32; 4]| {
for (i, value) in column.iter().enumerate() {
m[3][i] += *value;
}
m
};
let negated = [-s[0], -s[1], -s[2], -s[3]];
let (columns, offset) = match mode {
B::Clear => (zero, [0.0; 4]),
B::Src => (zero, s),
B::Dst => (scaled(1.0), [0.0; 4]),
B::SrcOver => (scaled(1.0 - alpha), s),
B::DstOver => (with_alpha_column(scaled(1.0), negated), s),
B::SrcIn => (with_alpha_column(zero, s), [0.0; 4]),
B::DstIn => (scaled(alpha), [0.0; 4]),
B::SrcOut => (with_alpha_column(zero, negated), s),
B::DstOut => (scaled(1.0 - alpha), [0.0; 4]),
B::SrcATop => (with_alpha_column(scaled(1.0 - alpha), s), [0.0; 4]),
B::DstATop => (with_alpha_column(scaled(alpha), negated), s),
B::Xor => (with_alpha_column(scaled(1.0 - alpha), negated), s),
B::Plus => (scaled(1.0), s),
B::Modulate => {
let mut m = zero;
for (i, column) in m.iter_mut().enumerate() {
column[i] = s[i];
}
(m, [0.0; 4])
}
mode => return Self::Blend { color: s, mode },
};
Self::Matrix {
columns,
offset,
form: ColorForm::Premultiplied,
}
}
pub fn is_identity(&self) -> bool {
match self {
Self::None => true,
Self::Matrix {
columns,
offset,
form: _,
} => {
offset.iter().all(|v| *v == 0.0)
&& columns.iter().enumerate().all(|(j, column)| {
column
.iter()
.enumerate()
.all(|(i, v)| *v == if i == j { 1.0 } else { 0.0 })
})
}
Self::Blend { mode, .. } => *mode == BlendMode::Dst,
Self::Gamma { .. } => false,
}
}
fn code(&self) -> f32 {
match self {
Self::None => filter::NONE,
Self::Matrix {
form: ColorForm::Premultiplied,
..
} => filter::PREMULTIPLIED,
Self::Matrix {
form: ColorForm::Straight,
..
} => filter::STRAIGHT,
Self::Gamma {
direction: Gamma::LinearToSrgb,
} => filter::LINEAR_TO_SRGB,
Self::Gamma {
direction: Gamma::SrgbToLinear,
} => filter::SRGB_TO_LINEAR,
Self::Blend { .. } => filter::BLEND,
}
}
pub(crate) fn pack_into(&self, out: &mut [f32; MATERIAL_FLOATS]) {
out[layout::FILTER_PARAMS] = self.code();
match self {
Self::Matrix {
columns, offset, ..
} => {
for (j, column) in columns.iter().enumerate() {
out[layout::FILTER + j * 4..layout::FILTER + j * 4 + 4].copy_from_slice(column);
}
out[layout::FILTER_OFFSET..layout::FILTER_OFFSET + 4].copy_from_slice(offset);
}
Self::Blend { color, mode } => {
out[layout::FILTER_OFFSET..layout::FILTER_OFFSET + 4].copy_from_slice(color);
out[layout::FILTER] = mode.code();
}
Self::None | Self::Gamma { .. } => {}
}
}
}
pub mod filter {
pub const NONE: f32 = 0.0;
pub const PREMULTIPLIED: f32 = 1.0;
pub const STRAIGHT: f32 = 2.0;
pub const LINEAR_TO_SRGB: f32 = 3.0;
pub const SRGB_TO_LINEAR: f32 = 4.0;
pub const BLEND: f32 = 5.0;
pub fn is_straight(code: f32) -> bool {
code > PREMULTIPLIED && code < BLEND
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Stop {
pub color: [f32; 4],
pub offset: f32,
}
impl Stop {
pub fn new(color: [f32; 4], offset: f32) -> Self {
Self { color, offset }
}
}
pub type ToLocal = [f32; 12];
#[derive(Debug, Clone, PartialEq)]
pub enum Material {
Solid([f32; 4]),
VertexGradient,
LinearGradient {
axis: [f32; 2],
to_local: ToLocal,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
RadialGradient {
to_local: ToLocal,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
SweepGradient {
to_local: ToLocal,
start_angle: f32,
end_angle: f32,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
ConicalGradient {
to_local: ToLocal,
start_radius: f32,
radius_delta: f32,
separation: f32,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
Runtime {
program: u32,
uniforms: Vec<f32>,
textures: [Option<u32>; MAX_EFFECT_TEXTURES],
},
Mesh {
slot: u32,
alpha: f32,
tint: [f32; 4],
tile: TileMode,
sampling: Sampling,
},
Image {
to_local: ToLocal,
slot: u32,
alpha: f32,
tile: TileMode,
sampling: Sampling,
source: [f32; 4],
tint: [f32; 4],
},
RoundedRect {
color: [f32; 4],
half_size: [f32; 2],
to_local: ToLocal,
radius: f32,
outer_radius: f32,
stroke: f32,
},
RoundedRectBlur {
color: [f32; 4],
to_local: ToLocal,
adjust: [f32; 2],
r1: f32,
exponent: f32,
s_inv: f32,
min_edge: f32,
scale: f32,
},
Ellipse {
color: [f32; 4],
half_size: [f32; 2],
to_local: ToLocal,
stroke: f32,
},
Blur {
to_local: ToLocal,
slot: u32,
step: [f32; 2],
sigma: f32,
},
Morphology {
to_local: ToLocal,
slot: u32,
step: [f32; 2],
radius: f32,
dilate: bool,
},
Glyph {
color: [f32; 4],
slot: u32,
},
PointField {
color: [f32; 4],
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TileMode {
#[default]
Clamp,
Repeat,
Decal,
Mirror,
}
impl Material {
pub fn solid(color: [f32; 4]) -> Self {
Self::Solid(color)
}
pub fn sampling(&self) -> Option<Sampling> {
match self {
Self::VertexGradient => None,
Self::Image { sampling, .. } | Self::Mesh { sampling, .. } => Some(*sampling),
Self::Blur { .. }
| Self::Morphology { .. }
| Self::Glyph { .. }
| Self::Runtime { .. } => None,
Self::Solid(_)
| Self::LinearGradient { .. }
| Self::RadialGradient { .. }
| Self::SweepGradient { .. }
| Self::ConicalGradient { .. }
| Self::RoundedRect { .. }
| Self::RoundedRectBlur { .. }
| Self::Ellipse { .. }
| Self::PointField { .. } => None,
}
}
pub fn reads_at_computed_offsets(&self) -> bool {
matches!(self, Self::Blur { .. } | Self::Morphology { .. })
}
#[must_use]
pub fn with_opacity(mut self, factor: f32) -> Self {
let scale = |color: &mut [f32; 4]| color[3] *= factor;
match &mut self {
Self::Solid(color)
| Self::RoundedRect { color, .. }
| Self::RoundedRectBlur { color, .. }
| Self::Ellipse { color, .. }
| Self::PointField { color }
| Self::Glyph { color, .. } => scale(color),
Self::LinearGradient { stops, .. }
| Self::RadialGradient { stops, .. }
| Self::SweepGradient { stops, .. }
| Self::ConicalGradient { stops, .. } => {
for stop in stops.iter_mut() {
scale(&mut stop.color);
}
}
Self::Mesh { alpha, .. } | Self::Image { alpha, .. } => *alpha *= factor,
Self::Runtime { .. } | Self::Blur { .. } | Self::Morphology { .. } => {}
Self::VertexGradient => {}
}
self
}
pub fn is_invisible(&self) -> bool {
match self {
Self::Solid(color) | Self::PointField { color } => color[3] <= 0.0,
Self::VertexGradient => false,
Self::LinearGradient { stops, .. }
| Self::RadialGradient { stops, .. }
| Self::SweepGradient { stops, .. }
| Self::ConicalGradient { stops, .. } => {
stops.is_empty() || stops.iter().all(|s| s.color[3] <= 0.0)
}
Self::Image { alpha, .. } | Self::Mesh { alpha, .. } => *alpha <= 0.0,
Self::Runtime { .. } => false,
Self::Blur { .. } | Self::Morphology { .. } => false,
Self::RoundedRectBlur { color, .. } => color[3] <= 0.0,
Self::RoundedRect {
color, half_size, ..
}
| Self::Ellipse {
color, half_size, ..
} => color[3] <= 0.0 || half_size[0] <= 0.0 || half_size[1] <= 0.0,
Self::Glyph { color, .. } => color[3] <= 0.0,
}
}
pub fn texture_slots(&self) -> [Option<u32>; MAX_EFFECT_TEXTURES] {
match self {
Self::Runtime { textures, .. } => *textures,
other => {
let mut slots = [None; MAX_EFFECT_TEXTURES];
slots[0] = other.texture_slot();
slots
}
}
}
pub fn texture_slot(&self) -> Option<u32> {
match self {
Self::VertexGradient => None,
Self::Image { slot, .. }
| Self::Mesh { slot, .. }
| Self::Glyph { slot, .. }
| Self::Blur { slot, .. }
| Self::Morphology { slot, .. } => Some(*slot),
Self::LinearGradient { ramp, .. }
| Self::RadialGradient { ramp, .. }
| Self::SweepGradient { ramp, .. }
| Self::ConicalGradient { ramp, .. } => *ramp,
Self::Solid(_)
| Self::PointField { .. }
| Self::RoundedRect { .. }
| Self::RoundedRectBlur { .. }
| Self::Ellipse { .. } => None,
Self::Runtime { textures, .. } => textures[0],
}
}
fn stops(&self) -> &[Stop] {
match self {
Self::VertexGradient => &[],
Self::Solid(_)
| Self::PointField { .. }
| Self::Image { .. }
| Self::Mesh { .. }
| Self::Glyph { .. }
| Self::Blur { .. }
| Self::Morphology { .. }
| Self::RoundedRect { .. }
| Self::RoundedRectBlur { .. }
| Self::Ellipse { .. }
| Self::Runtime { .. } => &[],
Self::LinearGradient { stops, .. }
| Self::RadialGradient { stops, .. }
| Self::SweepGradient { stops, .. }
| Self::ConicalGradient { stops, .. } => stops,
}
}
pub fn is_opaque(&self) -> bool {
let stops_opaque =
|stops: &[Stop]| !stops.is_empty() && stops.iter().all(|s| s.color[3] >= 1.0);
match self {
Material::Solid(color) => color[3] >= 1.0,
Material::LinearGradient { stops, .. }
| Material::RadialGradient { stops, .. }
| Material::SweepGradient { stops, .. }
| Material::ConicalGradient { stops, .. } => stops_opaque(stops),
_ => false,
}
}
pub fn needs_screen_derivatives(&self) -> bool {
!matches!(
self,
Material::Solid(_)
| Material::VertexGradient
| Material::LinearGradient { .. }
| Material::RadialGradient { .. }
| Material::SweepGradient { .. }
| Material::ConicalGradient { .. }
)
}
pub fn dithers(&self) -> bool {
matches!(
self,
Material::VertexGradient
| Material::LinearGradient { .. }
| Material::RadialGradient { .. }
| Material::SweepGradient { .. }
| Material::ConicalGradient { .. }
)
}
pub fn to_uniform(&self) -> [f32; MATERIAL_FLOATS] {
let mut out = [0.0f32; MATERIAL_FLOATS];
if let Self::Solid(color) = self {
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::SOLID;
return out;
}
if matches!(self, Self::VertexGradient) {
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(&[1.0, 1.0, 1.0, 1.0]);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::SOLID;
return out;
}
if let Self::Glyph { color, .. } = self {
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::GLYPH;
return out;
}
if let Self::PointField { color } = self {
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::POINT_FIELD;
return out;
}
if let Self::Ellipse {
color,
half_size,
to_local,
stroke,
} = self
{
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
out[layout::GEOMETRY + 2] = half_size[0];
out[layout::GEOMETRY + 3] = half_size[1];
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::ELLIPSE;
out[layout::PARAMS + 3] = *stroke;
return out;
}
if let Self::RoundedRect {
color,
half_size,
to_local,
radius,
outer_radius,
stroke,
} = self
{
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
out[layout::GEOMETRY + 1] = *outer_radius;
out[layout::GEOMETRY + 2] = half_size[0];
out[layout::GEOMETRY + 3] = half_size[1];
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::ROUNDED_RECT;
out[layout::PARAMS + 2] = *radius;
out[layout::PARAMS + 3] = *stroke;
return out;
}
if let Self::RoundedRectBlur {
color,
to_local,
adjust,
r1,
exponent,
s_inv,
min_edge,
scale,
} = self
{
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::GEOMETRY] = adjust[0];
out[layout::GEOMETRY + 1] = adjust[1];
out[layout::GEOMETRY + 2] = *s_inv;
out[layout::GEOMETRY + 3] = *min_edge;
out[layout::OFFSETS] = *scale;
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::ROUNDED_RECT_BLUR;
out[layout::PARAMS + 2] = *r1;
out[layout::PARAMS + 3] = *exponent;
return out;
}
if let Self::Blur {
to_local,
step,
sigma,
..
} = self
{
out[layout::GEOMETRY + 2] = step[0];
out[layout::GEOMETRY + 3] = step[1];
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::BLUR;
out[layout::PARAMS + 2] = *sigma;
return out;
}
if let Self::Morphology {
to_local,
step,
radius,
dilate,
..
} = self
{
out[layout::GEOMETRY + 2] = step[0];
out[layout::GEOMETRY + 3] = step[1];
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::MORPHOLOGY;
out[layout::PARAMS + 2] = radius.clamp(0.0, MORPHOLOGY_TAPS as f32);
out[layout::PARAMS + 3] = if *dilate { 1.0 } else { 0.0 };
return out;
}
if let Self::Runtime { uniforms, .. } = self {
let count = uniforms.len().min(RUNTIME_FLOATS);
out[..count].copy_from_slice(&uniforms[..count]);
return out;
}
if let Self::Mesh {
alpha,
tint,
tile,
sampling,
..
} = self
{
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(tint);
out[layout::GEOMETRY] = *alpha;
out[layout::GEOMETRY + 1] = tile_code(*tile);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::MESH;
out[layout::PARAMS + 2] = sampling_code(*sampling);
return out;
}
if let Self::Image {
to_local,
alpha,
tile,
sampling,
source,
tint,
..
} = self
{
out[layout::STOPS..layout::STOPS + 4].copy_from_slice(source);
out[layout::STOPS + 4..layout::STOPS + 8].copy_from_slice(tint);
out[layout::GEOMETRY + 2] = *alpha;
out[layout::GEOMETRY + 3] = tile_code(*tile);
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS] = 1.0;
out[layout::PARAMS + 1] = kind::IMAGE;
out[layout::PARAMS + 2] = sampling_code(*sampling);
return out;
}
let stops = self.stops();
let count = stops.len().min(MAX_STOPS);
for (i, stop) in stops.iter().take(count).enumerate() {
out[layout::STOPS + i * 4..layout::STOPS + i * 4 + 4].copy_from_slice(&stop.color);
out[layout::OFFSETS + i] = stop.offset;
}
out[layout::PARAMS] = count.max(1) as f32;
if count < 2 {
out[layout::PARAMS + 1] = kind::SOLID;
return out;
}
match self {
Self::Solid(_)
| Self::VertexGradient
| Self::PointField { .. }
| Self::Image { .. }
| Self::Mesh { .. }
| Self::Glyph { .. }
| Self::Blur { .. }
| Self::Morphology { .. }
| Self::RoundedRect { .. }
| Self::RoundedRectBlur { .. }
| Self::Ellipse { .. }
| Self::Runtime { .. } => {
unreachable!("handled above")
}
Self::LinearGradient {
ramp,
axis,
to_local,
tile,
..
} => {
out[layout::PARAMS] = stop_count_code(count, ramp);
out[layout::PARAMS + 2] = tile_code(*tile);
out[layout::GEOMETRY + 2] = axis[0];
out[layout::GEOMETRY + 3] = axis[1];
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS + 1] = kind::LINEAR;
}
Self::RadialGradient {
ramp,
to_local,
tile,
..
} => {
out[layout::PARAMS] = stop_count_code(count, ramp);
out[layout::PARAMS + 2] = tile_code(*tile);
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS + 1] = kind::RADIAL;
}
Self::SweepGradient {
ramp,
to_local,
start_angle,
end_angle,
tile,
..
} => {
out[layout::PARAMS] = stop_count_code(count, ramp);
out[layout::PARAMS + 2] = tile_code(*tile);
out[layout::GEOMETRY + 2] = *start_angle;
out[layout::GEOMETRY + 3] = *end_angle;
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS + 1] = kind::SWEEP;
}
Self::ConicalGradient {
ramp,
to_local,
start_radius,
radius_delta,
separation,
tile,
..
} => {
out[layout::PARAMS] = stop_count_code(count, ramp);
out[layout::PARAMS + 2] = tile_code(*tile);
out[layout::GEOMETRY + 2] = *start_radius;
out[layout::GEOMETRY + 3] = *radius_delta;
out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
out[layout::PARAMS + 1] = kind::CONICAL;
out[layout::PARAMS + 3] = *separation;
}
}
out
}
pub fn program(&self) -> Option<u32> {
match self {
Self::Runtime { program, .. } => Some(*program),
_ => None,
}
}
pub fn variant(&self) -> MaterialVariant {
match self {
Self::Solid(_) => MaterialVariant::Solid,
_ => MaterialVariant::Gradient,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MaterialVariant {
Solid,
Gradient,
}
#[cfg(test)]
mod tests {
#[test]
fn a_vertex_gradient_packs_as_white_and_dithers() {
let packed = Material::VertexGradient.to_uniform();
assert_eq!(
&packed[layout::STOPS..layout::STOPS + 4],
&[1.0, 1.0, 1.0, 1.0],
"opaque white is the identity for a `Modulate` tint"
);
assert_eq!(packed[layout::PARAMS + 1], kind::SOLID);
assert_eq!(packed[layout::PARAMS], 1.0);
assert!(Material::VertexGradient.dithers());
assert!(!Material::solid([1.0; 4]).dithers());
}
#[test]
fn a_vertex_gradient_answers_the_other_predicates() {
let m = Material::VertexGradient;
assert!(!m.needs_screen_derivatives(), "a vertex color reads none");
assert_eq!(m.sampling(), None);
assert_eq!(m.texture_slot(), None);
assert!(!m.is_invisible(), "the colors are on the vertices");
assert!(
!m.is_opaque(),
"the vertex alphas are not visible from here"
);
}
use super::*;
#[test]
fn a_material_reports_its_filter_and_not_what_the_filter_does() {
let image = |sampling| Material::Image {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
slot: 3,
alpha: 1.0,
tile: TileMode::Clamp,
sampling,
source: [0.0, 0.0, 1.0, 1.0],
tint: [1.0; 4],
};
for sampling in [
Sampling::Nearest,
Sampling::Linear,
Sampling::Cubic,
Sampling::Mipmap,
] {
assert_eq!(image(sampling).sampling(), Some(sampling));
assert_eq!(image(sampling).texture_slot(), Some(3));
assert!(
!image(sampling).reads_at_computed_offsets(),
"an image is read where it is drawn, not at an offset of its own"
);
}
let morphology = Material::Morphology {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
slot: 0,
step: [1.0, 0.0],
radius: 2.0,
dilate: true,
};
assert_eq!(
morphology.sampling(),
None,
"it carries no filter to report"
);
assert!(morphology.reads_at_computed_offsets());
let glyph = Material::Glyph {
color: [1.0; 4],
slot: 1,
};
assert_eq!(glyph.sampling(), None);
assert_eq!(glyph.texture_slot(), Some(1));
assert!(!glyph.reads_at_computed_offsets());
let solid = Material::solid([1.0; 4]);
assert_eq!(solid.sampling(), None);
assert_eq!(solid.texture_slot(), None);
assert!(!solid.reads_at_computed_offsets());
}
fn linear_to_local(m: [f32; 4]) -> ToLocal {
[
m[0], m[1], 0.0, 0.0, m[2], m[3], 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
]
}
fn two_stops() -> Vec<Stop> {
vec![
Stop::new([1.0, 0.0, 0.0, 1.0], 0.0),
Stop::new([0.0, 0.0, 1.0, 1.0], 1.0),
]
}
#[test]
fn a_solid_color_lands_in_the_first_stop_and_selects_the_solid_path() {
let packed = Material::solid([0.25, 0.5, 0.75, 1.0]).to_uniform();
assert_eq!(&packed[0..4], &[0.25, 0.5, 0.75, 1.0]);
assert_eq!(packed[layout::PARAMS], 1.0, "stop count");
assert_eq!(packed[layout::PARAMS + 1], kind::SOLID);
}
#[test]
fn a_linear_gradient_packs_its_stops_axis_and_count() {
let packed = Material::LinearGradient {
axis: [2.0, 0.0],
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops: two_stops(),
tile: TileMode::Clamp,
ramp: None,
}
.to_uniform();
assert_eq!(&packed[0..4], &[1.0, 0.0, 0.0, 1.0], "first stop");
assert_eq!(&packed[4..8], &[0.0, 0.0, 1.0, 1.0], "second stop");
assert_eq!(packed[layout::OFFSETS], 0.0);
assert_eq!(packed[layout::OFFSETS + 1], 1.0);
assert_eq!(
&packed[layout::GEOMETRY..layout::GEOMETRY + 4],
&[0.0, 0.0, 2.0, 0.0],
"the axis rather than the end point, and nothing in the half the \
start used to occupy before it moved inside the mapping"
);
assert_eq!(
&packed[layout::TO_LOCAL..layout::TO_LOCAL + 12],
&linear_to_local([1.0, 0.0, 0.0, 1.0])
);
assert_eq!(packed[layout::PARAMS + 1], kind::LINEAR);
}
#[test]
fn a_radial_gradient_packs_its_mapping() {
let packed = Material::RadialGradient {
to_local: linear_to_local([2.0, 0.0, 0.0, 4.0]),
stops: two_stops(),
tile: TileMode::Clamp,
ramp: None,
}
.to_uniform();
assert_eq!(&packed[layout::GEOMETRY..layout::GEOMETRY + 2], &[0.0, 0.0]);
assert_eq!(
&packed[layout::TO_LOCAL..layout::TO_LOCAL + 12],
&linear_to_local([2.0, 0.0, 0.0, 4.0])
);
assert_eq!(packed[layout::PARAMS + 1], kind::RADIAL);
}
#[test]
fn a_sweep_gradient_packs_its_angles_alongside_its_center() {
let packed = Material::SweepGradient {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
start_angle: 0.5,
end_angle: 2.5,
stops: two_stops(),
tile: TileMode::Clamp,
ramp: None,
}
.to_uniform();
assert_eq!(
&packed[layout::GEOMETRY..layout::GEOMETRY + 4],
&[0.0, 0.0, 0.5, 2.5]
);
assert_eq!(packed[layout::PARAMS + 1], kind::SWEEP);
}
#[test]
fn every_gradient_kind_falls_back_to_solid_with_one_stop() {
let one = vec![Stop::new([1.0, 1.0, 1.0, 1.0], 0.0)];
let materials = [
Material::LinearGradient {
axis: [1.0 - 0.0, 0.0 - 0.0],
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops: one.clone(),
tile: TileMode::Clamp,
ramp: None,
},
Material::RadialGradient {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops: one.clone(),
tile: TileMode::Clamp,
ramp: None,
},
Material::SweepGradient {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
start_angle: 0.0,
end_angle: 1.0,
stops: one,
tile: TileMode::Clamp,
ramp: None,
},
];
for material in materials {
let packed = material.to_uniform();
assert_eq!(packed[layout::PARAMS + 1], kind::SOLID, "{material:?}");
assert_eq!(&packed[0..4], &[1.0, 1.0, 1.0, 1.0]);
}
}
#[test]
fn stops_beyond_the_limit_are_dropped_rather_than_overflowing() {
let stops: Vec<Stop> = (0..8)
.map(|i| Stop::new([i as f32 / 8.0, 0.0, 0.0, 1.0], i as f32 / 7.0))
.collect();
let packed = Material::LinearGradient {
axis: [1.0 - 0.0, 0.0 - 0.0],
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops,
tile: TileMode::Clamp,
ramp: None,
}
.to_uniform();
assert_eq!(packed[layout::PARAMS], MAX_STOPS as f32);
}
#[test]
fn visibility_accounts_for_every_stop_of_every_kind() {
assert!(Material::solid([1.0, 1.0, 1.0, 0.0]).is_invisible());
assert!(!Material::solid([0.0, 0.0, 0.0, 1.0]).is_invisible());
let clear = vec![
Stop::new([1.0, 0.0, 0.0, 0.0], 0.0),
Stop::new([0.0, 0.0, 1.0, 0.0], 1.0),
];
assert!(Material::RadialGradient {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops: clear,
tile: TileMode::Clamp,
ramp: None,
}
.is_invisible());
let partly = vec![
Stop::new([1.0, 0.0, 0.0, 0.0], 0.0),
Stop::new([0.0, 0.0, 1.0, 1.0], 1.0),
];
assert!(
!Material::SweepGradient {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
start_angle: 0.0,
end_angle: 1.0,
stops: partly,
tile: TileMode::Clamp,
ramp: None,
}
.is_invisible(),
"one visible stop is enough"
);
}
#[test]
fn every_gradient_kind_reports_the_same_variant() {
assert_eq!(Material::solid([0.0; 4]).variant(), MaterialVariant::Solid);
for material in [
Material::LinearGradient {
axis: [1.0, 0.0],
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops: two_stops(),
tile: TileMode::Clamp,
ramp: None,
},
Material::RadialGradient {
to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
stops: two_stops(),
tile: TileMode::Clamp,
ramp: None,
},
] {
assert_eq!(material.variant(), MaterialVariant::Gradient);
}
}
}