1pub const W: usize = 320;
12pub const H: usize = 180;
13pub const FRAMES: u32 = 96;
15const RALLY_END: u32 = 36;
16const MORPH_END: u32 = 60;
17const NAME_END: u32 = 72;
18const FADE_START: u32 = 84;
19pub const HOLD: u32 = FADE_START - 1;
21
22const BG: [u8; 3] = [0x07, 0x09, 0x0c];
23const TILE: [u8; 3] = [0x0e, 0x11, 0x16];
24const NET: [u8; 3] = [0x3a, 0x42, 0x50];
25const RED: [u8; 3] = [0xe8, 0x55, 0x4e];
26const BLUE: [u8; 3] = [0x3b, 0x7f, 0xf5];
27const WHITE: [u8; 3] = [0xf2, 0xf4, 0xf8];
28
29type Rect = [i32; 4];
31
32const LOGO_X: i32 = (W as i32 - 128) / 2;
34const LOGO_Y: i32 = 10;
35const fn logo(x: i32, y: i32, w: i32, h: i32) -> Rect {
36 [LOGO_X + x * 2, LOGO_Y + y * 2, w * 2, h * 2]
37}
38const LOGO_TILE: Rect = logo(2, 2, 60, 60);
39const LOGO_RADIUS: i32 = 26;
40const LOGO_LEFT: Rect = logo(9, 15, 5, 18);
41const LOGO_RIGHT: Rect = logo(50, 29, 5, 18);
42const LOGO_BALL: Rect = logo(38, 22, 6, 6);
43const fn logo_dash(i: i32) -> Rect {
45 [LOGO_X + 61, LOGO_Y + (9 + 9 * i) * 2, 6, 10]
46}
47
48const COURT: Rect = [0, 0, W as i32, H as i32];
50const PADDLE_W: i32 = 10;
51const PADDLE_H: i32 = 36;
52const LEFT_X: i32 = 18;
53const RIGHT_X: i32 = W as i32 - 18 - PADDLE_W;
54const BALL: i32 = 12;
55const fn court_dash(i: i32) -> Rect {
56 [W as i32 / 2 - 3, 8 + 36 * i, 6, 20]
57}
58
59const LEFT_FACE: i32 = LEFT_X + PADDLE_W;
62const RIGHT_FACE: i32 = RIGHT_X - BALL;
63const VX: i32 = 9;
64const CROSS: i32 = (RIGHT_FACE - LEFT_FACE) / VX; const RIGHT_HIT: i32 = RALLY_END as i32 - CROSS;
68const VY: i32 = 5;
69
70fn bounce(p: i32, lo: i32, hi: i32) -> i32 {
71 let span = hi - lo;
72 let m = (p - lo).rem_euclid(2 * span);
73 lo + if m <= span { m } else { 2 * span - m }
74}
75
76fn ball_at(t: i32) -> (i32, i32) {
78 (bounce(RIGHT_FACE - (RIGHT_HIT - t) * VX, LEFT_FACE, RIGHT_FACE), bounce(40 + t * VY, 0, H as i32 - BALL))
79}
80
81fn paddle_y(t: i32, first: i32, offset: i32) -> i32 {
85 let period = 2 * CROSS;
86 let prev = first + (t - first).div_euclid(period) * period;
87 let next = prev + period;
88 let at = |h: i32| ball_at(h).1 + BALL / 2 - PADDLE_H / 2 + offset;
89 let y = at(prev) + (at(next) - at(prev)) * (t - prev) / period;
90 y.clamp(0, H as i32 - PADDLE_H)
91}
92
93fn ease(t: u32, n: u32) -> i64 {
95 let t = t.min(n) as i64;
96 let n = n as i64;
97 (4096 * (3 * t * t * n - 2 * t * t * t)) / (n * n * n)
98}
99
100fn lerp(a: i32, b: i32, e: i64) -> i32 {
101 a + ((b - a) as i64 * e / 4096) as i32
102}
103
104fn lerp_rect(a: Rect, b: Rect, e: i64) -> Rect {
105 [lerp(a[0], b[0], e), lerp(a[1], b[1], e), lerp(a[2], b[2], e), lerp(a[3], b[3], e)]
106}
107
108fn put(px: &mut [u8], x: i32, y: i32, c: [u8; 3]) {
109 if x >= 0 && y >= 0 && (x as usize) < W && (y as usize) < H {
110 let i = (y as usize * W + x as usize) * 4;
111 px[i..i + 4].copy_from_slice(&[c[0], c[1], c[2], 255]);
112 }
113}
114
115fn fill(px: &mut [u8], r: Rect, c: [u8; 3]) {
116 for y in r[1]..r[1] + r[3] {
117 for x in r[0]..r[0] + r[2] {
118 put(px, x, y, c);
119 }
120 }
121}
122
123fn fill_round(px: &mut [u8], r: Rect, radius: i32, c: [u8; 3]) {
125 let rad = radius.min(r[2] / 2).min(r[3] / 2);
126 for y in r[1]..r[1] + r[3] {
127 for x in r[0]..r[0] + r[2] {
128 let cx = (2 * x + 1).clamp(2 * (r[0] + rad), 2 * (r[0] + r[2] - rad));
130 let cy = (2 * y + 1).clamp(2 * (r[1] + rad), 2 * (r[1] + r[3] - rad));
131 let (dx, dy) = (2 * x + 1 - cx, 2 * y + 1 - cy);
132 if dx * dx + dy * dy <= 4 * rad * rad {
133 put(px, x, y, c);
134 }
135 }
136 }
137}
138
139const GLYPHS: [[&str; 7]; 4] = [
141 [".####", "#...#", "#...#", ".####", "....#", "....#", ".###."],
142 [".###.", "....#", ".####", "#...#", ".####", ".....", "....."],
143 [".####", "#....", ".###.", "....#", "####.", ".....", "....."],
144 ["##.#.", "#.#.#", "#.#.#", "#.#.#", "#.#.#", ".....", "....."],
145];
146const SCALE: i32 = 3;
147const NAME_Y: i32 = LOGO_Y + 128 + 12;
148const NAME_X: i32 = (W as i32 - (4 * 5 + 3 * 1) * SCALE) / 2;
149
150fn name(px: &mut [u8], letters: usize) {
151 for (g, glyph) in GLYPHS.iter().enumerate().take(letters) {
152 for (row, bits) in glyph.iter().enumerate() {
153 for (col, b) in bits.bytes().enumerate() {
154 if b == b'#' {
155 let x = NAME_X + (g as i32 * 6 + col as i32) * SCALE;
156 fill(px, [x, NAME_Y + row as i32 * SCALE, SCALE, SCALE], WHITE);
157 }
158 }
159 }
160 }
161}
162
163pub fn frame(f: u32) -> Vec<u8> {
165 let f = f.min(FRAMES - 1);
166 let mut px = vec![0u8; W * H * 4];
167 fill(&mut px, COURT, BG);
168 let r = f.min(RALLY_END);
170 let e = if f <= RALLY_END { 0 } else { ease(f - RALLY_END, MORPH_END - RALLY_END) };
171 let (bx, by) = ball_at(r as i32);
172 let tile = lerp_rect(COURT, LOGO_TILE, e);
173 fill_round(&mut px, tile, lerp(0, LOGO_RADIUS, e), TILE);
174 for i in 0..5 {
175 fill(&mut px, lerp_rect(court_dash(i), logo_dash(i), e), NET);
176 }
177 fill(&mut px, lerp_rect([LEFT_X, paddle_y(r as i32, RIGHT_HIT + CROSS, -7), PADDLE_W, PADDLE_H], LOGO_LEFT, e), RED);
178 fill(&mut px, lerp_rect([RIGHT_X, paddle_y(r as i32, RIGHT_HIT, 8), PADDLE_W, PADDLE_H], LOGO_RIGHT, e), BLUE);
179 fill(&mut px, lerp_rect([bx, by, BALL, BALL], LOGO_BALL, e), WHITE);
180 if f > MORPH_END {
181 let letters = 1 + (f - MORPH_END) as usize * 4 / (NAME_END - MORPH_END) as usize;
182 name(&mut px, letters.min(4));
183 }
184 if f >= FADE_START {
185 let k = 16 - (16 * (f - FADE_START + 1) / (FRAMES - FADE_START)).min(16);
187 for p in px.chunks_exact_mut(4) {
188 for c in &mut p[..3] {
189 *c = (*c as u32 * k / 16) as u8;
190 }
191 }
192 }
193 px
194}
195
196pub const SPLASH_HASH: u32 = 0xab6bc49a;
199
200pub fn splash_hash() -> u32 {
202 (0..FRAMES).flat_map(frame).fold(0x811c_9dc5u32, |h, b| (h ^ b as u32).wrapping_mul(0x0100_0193))
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn frames() {
211 if let Ok(dir) = std::env::var("SPLASH_DIR") {
213 std::fs::create_dir_all(&dir).unwrap();
214 for f in 0..FRAMES {
215 let file = std::fs::File::create(format!("{dir}/{f:03}.png")).unwrap();
216 let mut enc = png::Encoder::new(std::io::BufWriter::new(file), W as u32, H as u32);
217 enc.set_color(png::ColorType::Rgba);
218 enc.write_header().unwrap().write_image_data(&frame(f)).unwrap();
219 }
220 }
221 let last = frame(MORPH_END);
223 let i = ((LOGO_BALL[1] + 1) as usize * W + (LOGO_BALL[0] + 1) as usize) * 4;
224 assert_eq!(&last[i..i + 3], &WHITE);
225 assert_eq!(splash_hash(), SPLASH_HASH, "the frames changed: SPLASH_HASH = {:#010x}", splash_hash());
226 }
227}