use crate::tile_manager::VisibleTile;
use rustial_math::{tile_bounds_world, TileId, WorldBounds};
#[derive(Debug, Clone, PartialEq)]
pub struct PlaceholderStyle {
pub background_color: [f32; 4],
pub skeleton_line_color: [f32; 4],
pub animate: bool,
pub shimmer_speed: f32,
pub shimmer_amplitude: f32,
}
impl Default for PlaceholderStyle {
fn default() -> Self {
Self {
background_color: [0.90, 0.90, 0.90, 1.0],
skeleton_line_color: [0.82, 0.82, 0.82, 1.0],
animate: true,
shimmer_speed: 1.2,
shimmer_amplitude: 0.15,
}
}
}
impl PlaceholderStyle {
pub fn new() -> Self {
Self::default()
}
pub fn with_background_color(mut self, color: [f32; 4]) -> Self {
self.background_color = color;
self
}
pub fn with_skeleton_line_color(mut self, color: [f32; 4]) -> Self {
self.skeleton_line_color = color;
self
}
pub fn with_animate(mut self, animate: bool) -> Self {
self.animate = animate;
self
}
pub fn with_shimmer_speed(mut self, speed: f32) -> Self {
self.shimmer_speed = speed;
self
}
pub fn with_shimmer_amplitude(mut self, amplitude: f32) -> Self {
self.shimmer_amplitude = amplitude;
self
}
#[inline]
pub fn shimmer_opacity(&self, phase: f32) -> f32 {
if !self.animate {
return 1.0;
}
let t = (phase * std::f32::consts::TAU).cos(); 1.0 - self.shimmer_amplitude * 0.5 * (1.0 - t)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LoadingPlaceholder {
pub tile: TileId,
pub bounds: WorldBounds,
pub animation_phase: f32,
}
pub struct PlaceholderGenerator;
impl PlaceholderGenerator {
pub fn generate(
visible_tiles: &[VisibleTile],
style: &PlaceholderStyle,
time_seconds: f64,
) -> Vec<LoadingPlaceholder> {
let phase = if style.animate {
((time_seconds * style.shimmer_speed as f64) % 1.0) as f32
} else {
0.0
};
visible_tiles
.iter()
.filter(|t| t.data.is_none())
.map(|t| LoadingPlaceholder {
tile: t.target,
bounds: tile_bounds_world(&t.target),
animation_phase: phase,
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tile_source::{DecodedImage, TileData};
use rustial_math::TileId;
use std::sync::Arc;
fn missing_tile(zoom: u8, x: u32, y: u32) -> VisibleTile {
let id = TileId::new(zoom, x, y);
VisibleTile {
target: id,
actual: id,
data: None,
fade_opacity: 1.0,
}
}
fn loaded_tile(zoom: u8, x: u32, y: u32) -> VisibleTile {
let id = TileId::new(zoom, x, y);
VisibleTile {
target: id,
actual: id,
data: Some(TileData::Raster(DecodedImage {
width: 1,
height: 1,
data: Arc::new(vec![0u8; 4]),
})),
fade_opacity: 1.0,
}
}
#[test]
fn default_style_has_expected_values() {
let s = PlaceholderStyle::default();
assert_eq!(s.background_color, [0.90, 0.90, 0.90, 1.0]);
assert!(s.animate);
assert!((s.shimmer_speed - 1.2).abs() < 1e-5);
}
#[test]
fn shimmer_opacity_is_one_when_disabled() {
let s = PlaceholderStyle::new().with_animate(false);
assert!((s.shimmer_opacity(0.0) - 1.0).abs() < 1e-6);
assert!((s.shimmer_opacity(0.5) - 1.0).abs() < 1e-6);
}
#[test]
fn shimmer_opacity_peaks_at_phase_zero() {
let s = PlaceholderStyle::new()
.with_animate(true)
.with_shimmer_amplitude(0.2);
assert!((s.shimmer_opacity(0.0) - 1.0).abs() < 1e-6);
assert!((s.shimmer_opacity(0.5) - 0.8).abs() < 1e-5);
}
#[test]
fn builder_overrides_defaults() {
let s = PlaceholderStyle::new()
.with_background_color([1.0, 0.0, 0.0, 1.0])
.with_skeleton_line_color([0.0, 1.0, 0.0, 1.0])
.with_shimmer_speed(2.0)
.with_shimmer_amplitude(0.3);
assert_eq!(s.background_color, [1.0, 0.0, 0.0, 1.0]);
assert_eq!(s.skeleton_line_color, [0.0, 1.0, 0.0, 1.0]);
assert!((s.shimmer_speed - 2.0).abs() < 1e-6);
assert!((s.shimmer_amplitude - 0.3).abs() < 1e-6);
}
#[test]
fn generates_placeholders_for_missing_tiles_only() {
let tiles = vec![
missing_tile(10, 512, 512),
loaded_tile(10, 513, 512),
missing_tile(10, 514, 512),
];
let style = PlaceholderStyle::default();
let placeholders = PlaceholderGenerator::generate(&tiles, &style, 0.0);
assert_eq!(placeholders.len(), 2);
assert_eq!(placeholders[0].tile, TileId::new(10, 512, 512));
assert_eq!(placeholders[1].tile, TileId::new(10, 514, 512));
}
#[test]
fn generates_nothing_when_all_loaded() {
let tiles = vec![loaded_tile(5, 10, 10), loaded_tile(5, 11, 10)];
let style = PlaceholderStyle::default();
let placeholders = PlaceholderGenerator::generate(&tiles, &style, 1.0);
assert!(placeholders.is_empty());
}
#[test]
fn generates_nothing_for_empty_tile_set() {
let style = PlaceholderStyle::default();
let placeholders = PlaceholderGenerator::generate(&[], &style, 0.0);
assert!(placeholders.is_empty());
}
#[test]
fn animation_phase_is_zero_when_disabled() {
let tiles = vec![missing_tile(5, 0, 0)];
let style = PlaceholderStyle::new().with_animate(false);
let placeholders = PlaceholderGenerator::generate(&tiles, &style, 99.9);
assert!((placeholders[0].animation_phase).abs() < 1e-6);
}
#[test]
fn animation_phase_wraps_around_one() {
let tiles = vec![missing_tile(5, 0, 0)];
let style = PlaceholderStyle::new()
.with_animate(true)
.with_shimmer_speed(1.0);
let placeholders = PlaceholderGenerator::generate(&tiles, &style, 2.7);
assert!((placeholders[0].animation_phase - 0.7).abs() < 1e-5);
}
#[test]
fn bounds_match_tile_bounds_world() {
let tiles = vec![missing_tile(3, 4, 2)];
let style = PlaceholderStyle::default();
let placeholders = PlaceholderGenerator::generate(&tiles, &style, 0.0);
let expected = tile_bounds_world(&TileId::new(3, 4, 2));
let actual = &placeholders[0].bounds;
assert!((actual.min.position.x - expected.min.position.x).abs() < 1e-3);
assert!((actual.min.position.y - expected.min.position.y).abs() < 1e-3);
assert!((actual.max.position.x - expected.max.position.x).abs() < 1e-3);
assert!((actual.max.position.y - expected.max.position.y).abs() < 1e-3);
}
#[test]
fn all_placeholders_share_same_phase() {
let tiles = vec![
missing_tile(5, 0, 0),
missing_tile(5, 1, 0),
missing_tile(5, 2, 0),
];
let style = PlaceholderStyle::default();
let placeholders = PlaceholderGenerator::generate(&tiles, &style, 0.42);
let p0 = placeholders[0].animation_phase;
for ph in &placeholders {
assert!((ph.animation_phase - p0).abs() < 1e-6);
}
}
}