1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use crate::geometry::{tile::ToRgb, Map, Point};
use rand::Rng;
use std::time::Duration;

/// How each tile gets rendered.
#[derive(Debug, Clone, Copy)]
pub enum Style {
    /// Fill the entire 4x4 area with the tile's color.
    Fill,
    /// Fill a 3x3 area with the tile's color, leaving a 1 pixel black grid pattern between.
    Grid,
    /// Fill a 3x3 cross with the tile's color, leaving black space between.
    Cross,
    /// Fill a 3x3 cross with the tile's color, plus up to 4 more chosen randomly,
    /// for a sparkling effect.
    SparkleCross,
}

impl Style {
    fn offsets(self) -> Box<dyn Iterator<Item = Point>> {
        match self {
            Style::Cross => Box::new(std::array::IntoIter::new([
                Point::new(1, 0),
                Point::new(0, 1),
                Point::new(1, 1),
                Point::new(2, 1),
                Point::new(1, 2),
            ])),
            Style::SparkleCross => {
                let mut rng = rand::thread_rng();

                let corners = std::array::IntoIter::new([
                    Point::new(0, 0),
                    Point::new(0, 2),
                    Point::new(2, 0),
                    Point::new(2, 2),
                ])
                .filter(move |_point| rng.gen());

                Box::new(Style::Cross.offsets().chain(corners))
            }
            Style::Grid => Box::new((0..3).flat_map(|y| (0..3).map(move |x| Point::new(x, y)))),
            Style::Fill => Box::new((0..4).flat_map(|y| (0..4).map(move |x| Point::new(x, y)))),
        }
    }
}

pub fn render_point<Tile: ToRgb>(
    position: Point,
    tile: &Tile,
    subpixels: &mut [u8],
    width: usize,
    style: Style,
) {
    let x = |point: Point| point.x as usize;
    let y = |point: Point| point.y as usize;

    let row_pixels = pixel_size(width) as usize;

    // the linear index of a position has the following components:
    //
    // - 2: offset from left edge
    // - 2 * row_pixels: offset from top
    // - x(position) * 4: x component of position
    // - y(position) * 4 * row_pixels: y component of position
    // - x(offset): x offset
    // - y(offset) * row_pixels: y offset
    //
    // It is multiplied by 3, because that is how many bytes each pixel takes
    //
    // Note: this requires that the offset be in the positive quadrant
    let linear_idx = |offset: Point| {
        (2 + (2 * row_pixels)
            + (x(position) * 4)
            + (y(position) * 4 * row_pixels)
            + x(offset)
            + (y(offset) * row_pixels))
            * 3
    };

    let rgb = tile.to_rgb();

    for offset in style.offsets() {
        let idx = linear_idx(offset);
        subpixels[idx..idx + 3].copy_from_slice(&rgb);
    }
}

/// Each tile is 4px high and wide, with a 2px margin on the outside edges of the image.
pub fn pixel_size(width: usize) -> u16 {
    ((width + 1) * 4) as u16
}

/// Total pixels in an image for a map
///
/// Each tile is 4px high and 4px wide, with a 2px margin on the outside
/// edges of the image.
pub fn n_pixels_for(width: usize, height: usize) -> usize {
    pixel_size(width) as usize * pixel_size(height) as usize
}

pub type Encoder = gif::Encoder<std::io::BufWriter<std::fs::File>>;
pub type EncodingError = gif::EncodingError;

/// An `Animation` holds a handle to an unfinished gif animation.
///
/// _Depends on the `map-render` feature._
///
/// It is created with [`Map::prepare_animation`].
///
/// The gif is finalized when this struct is dropped.
pub struct Animation {
    encoder: Encoder,
    style: Style,
}

impl Animation {
    /// Create a new animation from an encoder and frame duration.
    ///
    /// This animation will repeat infinitely, displaying each frame for
    /// `frame_duration`.
    pub(crate) fn new(
        mut encoder: Encoder,
        frame_duration: Duration,
        style: Style,
    ) -> Result<Animation, EncodingError> {
        encoder.set_repeat(gif::Repeat::Infinite)?;

        // delay is set in hundredths of a second
        encoder.write_extension(gif::ExtensionData::new_control_ext(
            (frame_duration.as_millis() / 10) as u16,
            gif::DisposalMethod::Any,
            false,
            None,
        ))?;

        Ok(Animation { encoder, style })
    }

    /// Write a frame to this animation.
    ///
    /// This frame will be visible for the duration specified at the animation's
    /// creation.
    pub fn write_frame<Tile: ToRgb>(&mut self, map: &Map<Tile>) -> Result<(), EncodingError> {
        self.encoder.write_frame(&map.render_frame(self.style))
    }
}