use blinc_core::{Color, GradientStop};
use lru::LruCache;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
const GRADIENT_CACHE_CAPACITY: usize = 32;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpreadMode {
#[default]
Pad,
Repeat,
Reflect,
}
pub const GRADIENT_TEXTURE_WIDTH: u32 = 256;
pub struct RasterizedGradient {
pub pixels: [u8; GRADIENT_TEXTURE_WIDTH as usize * 4],
pub stop_count: usize,
}
impl RasterizedGradient {
pub fn from_stops(stops: &[GradientStop], spread: SpreadMode) -> Self {
let mut pixels = [0u8; GRADIENT_TEXTURE_WIDTH as usize * 4];
if stops.is_empty() {
return Self {
pixels,
stop_count: 0,
};
}
if stops.len() == 1 {
let c = &stops[0].color;
let r = (c.r * 255.0).clamp(0.0, 255.0) as u8;
let g = (c.g * 255.0).clamp(0.0, 255.0) as u8;
let b = (c.b * 255.0).clamp(0.0, 255.0) as u8;
let a = (c.a * 255.0).clamp(0.0, 255.0) as u8;
for i in 0..GRADIENT_TEXTURE_WIDTH as usize {
pixels[i * 4] = r;
pixels[i * 4 + 1] = g;
pixels[i * 4 + 2] = b;
pixels[i * 4 + 3] = a;
}
return Self {
pixels,
stop_count: 1,
};
}
for i in 0..GRADIENT_TEXTURE_WIDTH as usize {
let t = i as f32 / (GRADIENT_TEXTURE_WIDTH - 1) as f32;
let t = apply_spread_mode(t, spread);
let color = sample_gradient(stops, t);
pixels[i * 4] = (color.r * 255.0).clamp(0.0, 255.0) as u8;
pixels[i * 4 + 1] = (color.g * 255.0).clamp(0.0, 255.0) as u8;
pixels[i * 4 + 2] = (color.b * 255.0).clamp(0.0, 255.0) as u8;
pixels[i * 4 + 3] = (color.a * 255.0).clamp(0.0, 255.0) as u8;
}
Self {
pixels,
stop_count: stops.len(),
}
}
pub fn two_stop(start: Color, end: Color) -> Self {
let stops = [
GradientStop {
offset: 0.0,
color: start,
},
GradientStop {
offset: 1.0,
color: end,
},
];
Self::from_stops(&stops, SpreadMode::Pad)
}
}
fn apply_spread_mode(t: f32, spread: SpreadMode) -> f32 {
match spread {
SpreadMode::Pad => t.clamp(0.0, 1.0),
SpreadMode::Repeat => t.fract().abs(),
SpreadMode::Reflect => {
let t_mod = t.abs() % 2.0;
if t_mod > 1.0 {
2.0 - t_mod
} else {
t_mod
}
}
}
}
fn sample_gradient(stops: &[GradientStop], t: f32) -> Color {
if stops.is_empty() {
return Color::TRANSPARENT;
}
if t <= stops[0].offset {
return stops[0].color;
}
if t >= stops[stops.len() - 1].offset {
return stops[stops.len() - 1].color;
}
for i in 0..stops.len() - 1 {
let s0 = &stops[i];
let s1 = &stops[i + 1];
if t >= s0.offset && t <= s1.offset {
let range = s1.offset - s0.offset;
if range < 0.0001 {
return s0.color;
}
let local_t = (t - s0.offset) / range;
return lerp_color(&s0.color, &s1.color, local_t);
}
}
stops[stops.len() - 1].color
}
fn lerp_color(a: &Color, b: &Color, t: f32) -> Color {
Color {
r: a.r + (b.r - a.r) * t,
g: a.g + (b.g - a.g) * t,
b: a.b + (b.b - a.b) * t,
a: a.a + (b.a - a.a) * t,
}
}
fn hash_gradient_stops(stops: &[GradientStop], spread: SpreadMode) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
(spread as u8).hash(&mut hasher);
for stop in stops {
stop.offset.to_bits().hash(&mut hasher);
stop.color.r.to_bits().hash(&mut hasher);
stop.color.g.to_bits().hash(&mut hasher);
stop.color.b.to_bits().hash(&mut hasher);
stop.color.a.to_bits().hash(&mut hasher);
}
hasher.finish()
}
pub struct GradientTextureCache {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
pub has_gradient: bool,
rasterized_cache: LruCache<u64, Box<[u8; GRADIENT_TEXTURE_WIDTH as usize * 4]>>,
current_hash: Option<u64>,
}
impl GradientTextureCache {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Gradient Texture"),
size: wgpu::Extent3d {
width: GRADIENT_TEXTURE_WIDTH,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Gradient Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let placeholder = RasterizedGradient::two_stop(Color::WHITE, Color::WHITE);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&placeholder.pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(GRADIENT_TEXTURE_WIDTH * 4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: GRADIENT_TEXTURE_WIDTH,
height: 1,
depth_or_array_layers: 1,
},
);
Self {
texture,
view,
sampler,
has_gradient: false,
rasterized_cache: LruCache::new(NonZeroUsize::new(GRADIENT_CACHE_CAPACITY).unwrap()),
current_hash: None,
}
}
pub fn upload_stops(
&mut self,
queue: &wgpu::Queue,
stops: &[GradientStop],
spread: SpreadMode,
) -> bool {
let hash = hash_gradient_stops(stops, spread);
if self.current_hash == Some(hash) {
return false;
}
let pixels: &[u8; GRADIENT_TEXTURE_WIDTH as usize * 4] =
if let Some(cached) = self.rasterized_cache.get(&hash) {
cached.as_ref()
} else {
let rasterized = RasterizedGradient::from_stops(stops, spread);
self.rasterized_cache.put(hash, Box::new(rasterized.pixels));
self.rasterized_cache.get(&hash).unwrap().as_ref()
};
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(GRADIENT_TEXTURE_WIDTH * 4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: GRADIENT_TEXTURE_WIDTH,
height: 1,
depth_or_array_layers: 1,
},
);
self.has_gradient = stops.len() > 2;
self.current_hash = Some(hash);
true
}
pub fn upload(&mut self, queue: &wgpu::Queue, gradient: &RasterizedGradient) {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&gradient.pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(GRADIENT_TEXTURE_WIDTH * 4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: GRADIENT_TEXTURE_WIDTH,
height: 1,
depth_or_array_layers: 1,
},
);
self.has_gradient = gradient.stop_count > 2;
}
pub fn clear(&mut self, queue: &wgpu::Queue) {
let placeholder = RasterizedGradient::two_stop(Color::WHITE, Color::WHITE);
self.upload(queue, &placeholder);
self.has_gradient = false;
self.current_hash = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_two_stop_gradient() {
let gradient = RasterizedGradient::two_stop(Color::BLACK, Color::WHITE);
assert_eq!(gradient.stop_count, 2);
assert_eq!(gradient.pixels[0], 0); assert_eq!(gradient.pixels[1], 0); assert_eq!(gradient.pixels[2], 0); assert_eq!(gradient.pixels[3], 255);
let last_idx = (GRADIENT_TEXTURE_WIDTH as usize - 1) * 4;
assert_eq!(gradient.pixels[last_idx], 255); assert_eq!(gradient.pixels[last_idx + 1], 255); assert_eq!(gradient.pixels[last_idx + 2], 255); assert_eq!(gradient.pixels[last_idx + 3], 255); }
#[test]
fn test_multi_stop_gradient() {
let stops = vec![
GradientStop {
offset: 0.0,
color: Color::RED,
},
GradientStop {
offset: 0.5,
color: Color::GREEN,
},
GradientStop {
offset: 1.0,
color: Color::BLUE,
},
];
let gradient = RasterizedGradient::from_stops(&stops, SpreadMode::Pad);
assert_eq!(gradient.stop_count, 3);
assert!(gradient.pixels[0] > 200); assert!(gradient.pixels[1] < 50); assert!(gradient.pixels[2] < 50);
let mid_idx = 128 * 4;
assert!(gradient.pixels[mid_idx + 1] > gradient.pixels[mid_idx]);
let last_idx = 255 * 4;
assert!(gradient.pixels[last_idx] < 50); assert!(gradient.pixels[last_idx + 1] < 50); assert!(gradient.pixels[last_idx + 2] > 200); }
}