Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 7.6 kB View raw
1use bone_render::{ 2 AtlasGrid, AtlasPage, ClearColor, OffscreenContext, Result, SnapshotFrame, SolidRenderer, 3 Style, TILE_PAD, ViewportExtent, ViewportPx, 4}; 5use bone_types::{DisplayMode, IconId}; 6 7use crate::icon::{IconModel, icon_camera, icon_style}; 8use crate::models::model; 9use crate::offscreen_context; 10 11pub const MASTER_TILE: u32 = 192; 12 13const SHADOW_OFFSET: usize = 7; 14const SHADOW_BLUR: usize = 9; 15const SHADOW_ALPHA: u32 = 90; 16const SHADOW_LEVEL: u32 = 10; 17 18#[derive(Clone, Debug)] 19pub struct BakedPage { 20 pub page: AtlasPage, 21 pub extent: ViewportExtent, 22 pub rgba: Vec<u8>, 23} 24 25pub fn bake() -> Result<Vec<BakedPage>> { 26 let ctx = offscreen_context(ViewportExtent::square(ViewportPx::new(MASTER_TILE)))?; 27 let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format()); 28 let master = to_usize(MASTER_TILE); 29 let shadowed: Vec<(IconId, Vec<u8>)> = IconId::ALL 30 .iter() 31 .map(|&id| { 32 let premul = render_master(&mut renderer, &ctx, &model(id))?; 33 Ok((id, with_shadow(master, &premul))) 34 }) 35 .collect::<Result<Vec<_>>>()?; 36 37 let grid = AtlasGrid::DEFAULT; 38 Ok(AtlasPage::ALL 39 .into_iter() 40 .map(|page| { 41 let page_side = to_usize(page.side_px()); 42 let tiles: Vec<(IconId, Vec<u8>)> = shadowed 43 .iter() 44 .map(|(id, premul)| (*id, downsample(master, premul, page_side))) 45 .collect(); 46 let (width, height) = grid.pixel_size(page); 47 BakedPage { 48 page, 49 extent: ViewportExtent::new(ViewportPx::new(width), ViewportPx::new(height)), 50 rgba: pack(grid, page, &tiles), 51 } 52 }) 53 .collect()) 54} 55 56fn render_master( 57 renderer: &mut SolidRenderer, 58 ctx: &OffscreenContext, 59 m: &IconModel, 60) -> Result<Vec<u8>> { 61 let black = render_one(renderer, ctx, m, ClearColor::opaque(0.0, 0.0, 0.0))?; 62 let white = render_one(renderer, ctx, m, ClearColor::opaque(1.0, 1.0, 1.0))?; 63 Ok(black 64 .rgba() 65 .chunks_exact(4) 66 .zip(white.rgba().chunks_exact(4)) 67 .flat_map(|(dark, light)| { 68 let drop = 69 |channel: usize| u32::from(light[channel]).saturating_sub(u32::from(dark[channel])); 70 let coverage_drop = (drop(0) + drop(1) + drop(2)) / 3; 71 let alpha = u8_of(255u32.saturating_sub(coverage_drop)); 72 [dark[0], dark[1], dark[2], alpha] 73 }) 74 .collect()) 75} 76 77fn render_one( 78 renderer: &mut SolidRenderer, 79 ctx: &OffscreenContext, 80 m: &IconModel, 81 background: ClearColor, 82) -> Result<SnapshotFrame> { 83 let (scene, edges) = m.tessellate(); 84 let style: Style = icon_style().with_background(background); 85 renderer.render_display( 86 ctx, 87 &scene, 88 &edges, 89 icon_camera(m.view()), 90 &style, 91 DisplayMode::ShadedWithEdges, 92 ) 93} 94 95fn with_shadow(side: usize, icon: &[u8]) -> Vec<u8> { 96 let alpha: Vec<u8> = icon.chunks_exact(4).map(|px| px[3]).collect(); 97 let blurred = box_blur(side, &alpha, SHADOW_BLUR); 98 icon.chunks_exact(4) 99 .enumerate() 100 .flat_map(|(index, top)| { 101 let x = index % side; 102 let y = index / side; 103 let source = if y >= SHADOW_OFFSET { 104 u32::from(blurred[(y - SHADOW_OFFSET) * side + x]) 105 } else { 106 0 107 }; 108 let shadow_alpha = source * SHADOW_ALPHA / 255; 109 let shadow_rgb = u8_of(SHADOW_LEVEL * shadow_alpha / 255); 110 let shadow = [shadow_rgb, shadow_rgb, shadow_rgb, u8_of(shadow_alpha)]; 111 over([top[0], top[1], top[2], top[3]], shadow) 112 }) 113 .collect() 114} 115 116fn box_blur(side: usize, src: &[u8], radius: usize) -> Vec<u8> { 117 let horizontal = blur_axis(side, src, radius, true); 118 blur_axis(side, &horizontal, radius, false) 119} 120 121fn blur_axis(side: usize, src: &[u8], radius: usize, horizontal: bool) -> Vec<u8> { 122 (0..side) 123 .flat_map(|y| (0..side).map(move |x| (x, y))) 124 .map(|(x, y)| { 125 let center = if horizontal { x } else { y }; 126 let lo = center.saturating_sub(radius); 127 let hi = (center + radius).min(side - 1); 128 let (sum, count) = (lo..=hi).fold((0u32, 0u32), |(sum, count), p| { 129 let (sx, sy) = if horizontal { (p, y) } else { (x, p) }; 130 (sum + u32::from(src[sy * side + sx]), count + 1) 131 }); 132 div_round(sum, count) 133 }) 134 .collect() 135} 136 137fn downsample(master_side: usize, master: &[u8], page_side: usize) -> Vec<u8> { 138 assert_eq!( 139 master_side % page_side, 140 0, 141 "master tile must divide evenly into each atlas page side", 142 ); 143 let factor = master_side / page_side; 144 let area = to_u32(factor * factor); 145 (0..page_side) 146 .flat_map(|oy| (0..page_side).map(move |ox| (ox, oy))) 147 .flat_map(|(ox, oy)| { 148 let sums = (0..factor) 149 .flat_map(|dy| (0..factor).map(move |dx| (dx, dy))) 150 .fold([0u32; 4], |mut acc, (dx, dy)| { 151 let base = ((oy * factor + dy) * master_side + (ox * factor + dx)) * 4; 152 acc[0] += u32::from(master[base]); 153 acc[1] += u32::from(master[base + 1]); 154 acc[2] += u32::from(master[base + 2]); 155 acc[3] += u32::from(master[base + 3]); 156 acc 157 }); 158 [ 159 div_round(sums[0], area), 160 div_round(sums[1], area), 161 div_round(sums[2], area), 162 div_round(sums[3], area), 163 ] 164 }) 165 .collect() 166} 167 168fn pack(grid: AtlasGrid, page: AtlasPage, tiles: &[(IconId, Vec<u8>)]) -> Vec<u8> { 169 let (width, height) = grid.pixel_size(page); 170 let width = to_usize(width); 171 let height = to_usize(height); 172 let page_side = to_usize(page.side_px()); 173 let cell = to_usize(page.cell_px()); 174 let pad = to_usize(TILE_PAD); 175 tiles 176 .iter() 177 .fold(vec![0u8; width * height * 4], |mut atlas, (id, tile)| { 178 let grid_cell = grid.cell(id.tile()); 179 let cell_x = grid_cell.col * cell; 180 let cell_y = grid_cell.row * cell; 181 (0..cell).for_each(|ly| { 182 (0..cell).for_each(|lx| { 183 let sx = lx.saturating_sub(pad).min(page_side - 1); 184 let sy = ly.saturating_sub(pad).min(page_side - 1); 185 let src = (sy * page_side + sx) * 4; 186 let dst = ((cell_y + ly) * width + (cell_x + lx)) * 4; 187 atlas[dst..dst + 4].copy_from_slice(&tile[src..src + 4]); 188 }); 189 }); 190 atlas 191 }) 192} 193 194fn over(top: [u8; 4], bottom: [u8; 4]) -> [u8; 4] { 195 let inverse = 255 - u32::from(top[3]); 196 let blend = |channel: usize| { 197 u8_of(u32::from(top[channel]) + (inverse * u32::from(bottom[channel]) + 127) / 255) 198 }; 199 [blend(0), blend(1), blend(2), blend(3)] 200} 201 202fn div_round(sum: u32, count: u32) -> u8 { 203 u8_of((sum + count / 2) / count) 204} 205 206fn u8_of(value: u32) -> u8 { 207 let Ok(byte) = u8::try_from(value.min(255)) else { 208 panic!("value clamped to 255 fits a u8"); 209 }; 210 byte 211} 212 213fn to_usize(value: u32) -> usize { 214 let Ok(out) = usize::try_from(value) else { 215 panic!("atlas dimension fits usize"); 216 }; 217 out 218} 219 220fn to_u32(value: usize) -> u32 { 221 let Ok(out) = u32::try_from(value) else { 222 panic!("atlas count fits u32"); 223 }; 224 out 225}