Skip to main content

night_shift/
night_shift.rs

1//! Original pixel art, authored as palette-key grids and rendered with spritebox.
2//! cargo run --release --example night_shift
3use spritebox::{Document, Image, png};
4use std::{fmt::Write, path::PathBuf};
5
6const W: i32 = 480;
7const H: i32 = 300;
8const FRAMES: i32 = 96;
9const PALETTE: &[(char, &str)] = &[
10    ('.', "transparent"),
11    ('0', "#080d1c"),
12    ('1', "#10162b"),
13    ('2', "#181d36"),
14    ('3', "#262640"),
15    ('4', "#37304b"),
16    ('5', "#49344f"),
17    ('6', "#65425a"),
18    ('7', "#8c5262"),
19    ('8', "#ba666e"),
20    ('9', "#ed8e7b"),
21    ('a', "#ffb08a"),
22    ('b', "#ffe3b2"),
23    ('c', "#fc725a"),
24    ('d', "#c94756"),
25    ('e', "#7c304a"),
26    ('f', "#153442"),
27    ('g', "#20505b"),
28    ('h', "#2a7277"),
29    ('i', "#3e9e9c"),
30    ('j', "#78d9c4"),
31    ('k', "#b9f4d5"),
32    ('l', "#f7f2d0"),
33    ('m', "#273a50"),
34    ('n', "#42516b"),
35    ('o', "#6a7793"),
36    ('p', "#9cacc0"),
37    ('q', "#dd995b"),
38    ('r', "#b46b47"),
39    ('s', "#203c5570"),
40    ('t', "#7cbacb58"),
41    ('u', "#b9f4d530"),
42    ('v', "#ff9b6b38"),
43    ('w', "#78d9c418"),
44    ('x', "#3e9e9c50"),
45    ('y', "#3e9e9c34"),
46    ('z', "#3e9e9c20"),
47];
48
49#[derive(Clone)]
50struct Canvas {
51    w: i32,
52    h: i32,
53    pixels: Vec<char>,
54}
55impl Canvas {
56    fn new(w: i32, h: i32, key: char) -> Self {
57        Self {
58            w,
59            h,
60            pixels: vec![key; (w * h) as usize],
61        }
62    }
63    fn put(&mut self, x: i32, y: i32, c: char) {
64        if x >= 0 && y >= 0 && x < self.w && y < self.h {
65            self.pixels[(y * self.w + x) as usize] = c;
66        }
67    }
68    fn get(&self, x: i32, y: i32) -> char {
69        if x >= 0 && y >= 0 && x < self.w && y < self.h {
70            self.pixels[(y * self.w + x) as usize]
71        } else {
72            '.'
73        }
74    }
75    fn rect(&mut self, x: i32, y: i32, w: i32, h: i32, c: char) {
76        for yy in y.max(0)..(y + h).min(self.h) {
77            for xx in x.max(0)..(x + w).min(self.w) {
78                self.put(xx, yy, c);
79            }
80        }
81    }
82    fn line(&mut self, mut x: i32, mut y: i32, x1: i32, y1: i32, c: char) {
83        let dx = (x1 - x).abs();
84        let sx = if x < x1 { 1 } else { -1 };
85        let dy = -(y1 - y).abs();
86        let sy = if y < y1 { 1 } else { -1 };
87        let mut e = dx + dy;
88        loop {
89            self.put(x, y, c);
90            if x == x1 && y == y1 {
91                break;
92            }
93            let e2 = e * 2;
94            if e2 >= dy {
95                e += dy;
96                x += sx;
97            }
98            if e2 <= dx {
99                e += dx;
100                y += sy;
101            }
102        }
103    }
104    fn poly(&mut self, points: &[(i32, i32)], c: char) {
105        let lo = points.iter().map(|p| p.1).min().unwrap();
106        let hi = points.iter().map(|p| p.1).max().unwrap();
107        for y in lo.max(0)..hi.min(self.h) {
108            let mut cuts = Vec::new();
109            for i in 0..points.len() {
110                let (x0, y0) = points[i];
111                let (x1, y1) = points[(i + 1) % points.len()];
112                if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
113                    cuts.push(x0 + (y - y0) * (x1 - x0) / (y1 - y0));
114                }
115            }
116            cuts.sort_unstable();
117            for pair in cuts.chunks_exact(2) {
118                self.rect(pair[0], y, pair[1] - pair[0] + 1, 1, c);
119            }
120        }
121    }
122    fn ellipse(&mut self, cx: i32, cy: i32, rx: i32, ry: i32, c: char) {
123        for y in -ry..=ry {
124            for x in -rx..=rx {
125                if x * x * ry * ry + y * y * rx * rx <= rx * rx * ry * ry {
126                    self.put(cx + x, cy + y, c);
127                }
128            }
129        }
130    }
131    fn ring(&mut self, cx: i32, cy: i32, r: i32, c: char) {
132        let mut x = r;
133        let mut y = 0;
134        let mut e = 1 - r;
135        while x >= y {
136            for (a, b) in [
137                (x, y),
138                (y, x),
139                (-y, x),
140                (-x, y),
141                (-x, -y),
142                (-y, -x),
143                (y, -x),
144                (x, -y),
145            ] {
146                self.put(cx + a, cy + b, c);
147            }
148            y += 1;
149            if e < 0 {
150                e += 2 * y + 1;
151            } else {
152                x -= 1;
153                e += 2 * (y - x) + 1;
154            }
155        }
156    }
157    fn text(&mut self, x: i32, y: i32, text: &str, c: char, scale: i32) {
158        let mut at = x;
159        for ch in text.chars() {
160            for (yy, row) in glyph(ch).iter().enumerate() {
161                for xx in 0..5 {
162                    if row & (1 << (4 - xx)) != 0 {
163                        self.rect(at + xx * scale, y + yy as i32 * scale, scale, scale, c);
164                    }
165                }
166            }
167            at += 6 * scale;
168        }
169    }
170    fn source(&self, name: &str, out: &mut String) {
171        writeln!(out, "\nsprite {name} {}x{} palette=night", self.w, self.h).unwrap();
172        for row in self.pixels.chunks_exact(self.w as usize) {
173            out.push_str("  ");
174            out.extend(row);
175            out.push('\n');
176        }
177    }
178}
179fn glyph(c: char) -> [u8; 7] {
180    match c {
181        'A' => [14, 17, 17, 31, 17, 17, 17],
182        'B' => [30, 17, 17, 30, 17, 17, 30],
183        'C' => [14, 17, 16, 16, 16, 17, 14],
184        'D' => [30, 17, 17, 17, 17, 17, 30],
185        'E' => [31, 16, 16, 30, 16, 16, 31],
186        'F' => [31, 16, 16, 30, 16, 16, 16],
187        'G' => [14, 17, 16, 23, 17, 17, 15],
188        'H' => [17, 17, 17, 31, 17, 17, 17],
189        'I' => [31, 4, 4, 4, 4, 4, 31],
190        'J' => [7, 2, 2, 2, 18, 18, 12],
191        'K' => [17, 18, 20, 24, 20, 18, 17],
192        'L' => [16, 16, 16, 16, 16, 16, 31],
193        'M' => [17, 27, 21, 21, 17, 17, 17],
194        'N' => [17, 25, 21, 19, 17, 17, 17],
195        'O' => [14, 17, 17, 17, 17, 17, 14],
196        'P' => [30, 17, 17, 30, 16, 16, 16],
197        'Q' => [14, 17, 17, 17, 21, 18, 13],
198        'R' => [30, 17, 17, 30, 20, 18, 17],
199        'S' => [15, 16, 16, 14, 1, 1, 30],
200        'T' => [31, 4, 4, 4, 4, 4, 4],
201        'U' => [17, 17, 17, 17, 17, 17, 14],
202        'V' => [17, 17, 17, 17, 17, 10, 4],
203        'W' => [17, 17, 17, 21, 21, 21, 10],
204        'X' => [17, 17, 10, 4, 10, 17, 17],
205        'Y' => [17, 17, 10, 4, 4, 4, 4],
206        'Z' => [31, 1, 2, 4, 8, 16, 31],
207        '0' => [14, 17, 19, 21, 25, 17, 14],
208        '1' => [4, 12, 4, 4, 4, 4, 14],
209        '2' => [14, 17, 1, 2, 4, 8, 31],
210        '3' => [30, 1, 1, 14, 1, 1, 30],
211        '4' => [2, 6, 10, 18, 31, 2, 2],
212        '5' => [31, 16, 16, 30, 1, 1, 30],
213        '6' => [6, 8, 16, 30, 17, 17, 14],
214        '7' => [31, 1, 2, 4, 8, 8, 8],
215        '8' => [14, 17, 17, 14, 17, 17, 14],
216        '9' => [14, 17, 17, 15, 1, 2, 12],
217        ':' => [0, 4, 4, 0, 4, 4, 0],
218        '-' => [0, 0, 0, 31, 0, 0, 0],
219        '/' => [1, 2, 2, 4, 8, 8, 16],
220        '.' => [0, 0, 0, 0, 0, 12, 12],
221        '+' => [0, 4, 4, 31, 4, 4, 0],
222        _ => [0; 7],
223    }
224}
225fn hash(mut n: u32) -> u32 {
226    n ^= n >> 16;
227    n = n.wrapping_mul(0x7feb352d);
228    n ^= n >> 15;
229    n = n.wrapping_mul(0x846ca68b);
230    n ^ (n >> 16)
231}
232fn pick(seed: u32, n: i32) -> i32 {
233    (hash(seed) % n as u32) as i32
234}
235
236fn background() -> Canvas {
237    let mut c = Canvas::new(W, H, '1');
238    // A restrained, stepped atmosphere; the moon is the single warm focal mass.
239    for (y, h, k) in [
240        (0, 35, '1'),
241        (35, 32, '2'),
242        (67, 28, '3'),
243        (95, 31, '4'),
244        (126, 39, '5'),
245    ] {
246        c.rect(0, y, W, h, k);
247    }
248    for i in 0..165u32 {
249        let x = pick(i * 41 + 1, W);
250        let y = pick(i * 41 + 2, 151);
251        if pick(i + 3, 5) == 0 {
252            c.put(x, y, '6');
253        } else {
254            c.put(x, y, '4');
255        }
256    }
257    c.ellipse(215, 78, 57, 57, '7');
258    c.ellipse(215, 77, 54, 54, 'c');
259    for y in 25..132 {
260        for x in 160..271 {
261            let old = c.get(x, y);
262            if old == 'c' {
263                let k = if y < 51 {
264                    'a'
265                } else if y < 82 {
266                    '9'
267                } else if y < 103 {
268                    'c'
269                } else {
270                    'd'
271                };
272                c.put(x, y, k);
273            }
274        }
275    }
276    // Horizontal interruptions preserve the pixel texture of the moon.
277    for (y, w) in [(102, 103), (109, 98), (117, 86), (124, 67)] {
278        c.rect(215 - w / 2, y, w, 2, '5');
279    }
280    c.rect(141, 43, 31, 2, '3');
281    c.rect(124, 46, 61, 1, '3');
282    c.rect(251, 64, 63, 2, '4');
283    c.rect(268, 67, 63, 1, '4');
284    // Far skyline: deliberately varied roofs, setbacks and window rhythms.
285    for i in 0..30u32 {
286        let x = i as i32 * 18 - 15;
287        let w = 13 + pick(i + 80, 13);
288        let y = 78 + pick(i + 90, 65);
289        c.rect(x, y, w, 172 - y, '3');
290        c.rect(x + 2, y - 3, w - 5, 3, '3');
291        if i % 3 == 0 {
292            c.line(x + w / 2, y - 12, x + w / 2, y, '3');
293        }
294        for yy in (y + 7..166).step_by(7) {
295            for xx in (x + 3..x + w - 2).step_by(5) {
296                if pick((xx * 31 + yy * 7).unsigned_abs(), 4) == 0 {
297                    c.rect(xx, yy, 2, 2, if i % 4 == 0 { '8' } else { '6' });
298                }
299            }
300        }
301    }
302    // Central blade tower, silhouetted against the moon.
303    c.poly(
304        &[
305            (199, 137),
306            (201, 59),
307            (207, 49),
308            (208, 20),
309            (211, 13),
310            (214, 51),
311            (221, 59),
312            (225, 137),
313        ],
314        '2',
315    );
316    c.rect(206, 58, 10, 82, '1');
317    c.line(210, 29, 210, 115, '5');
318    c.line(211, 30, 211, 52, '8');
319    c.rect(209, 15, 3, 2, 'c');
320    c.rect(199, 65, 23, 3, '4');
321    c.rect(199, 96, 25, 3, '4');
322    for y in (62..135).step_by(7) {
323        c.rect(203, y, 2, 2, '7');
324        c.rect(217, y + 2, 2, 3, '6');
325    }
326    c.rect(241, 87, 25, 89, '2');
327    c.rect(245, 80, 17, 7, '2');
328    c.line(254, 64, 254, 80, '3');
329    for y in (94..169).step_by(8) {
330        c.rect(245, y, 4, 2, 'g');
331        c.rect(254, y, 7, 2, '5');
332    }
333    // Mid-ground buildings frame a long, narrow street.
334    for (x, y, w, h) in [
335        (91, 102, 39, 86),
336        (134, 116, 38, 74),
337        (279, 95, 39, 97),
338        (322, 119, 39, 73),
339        (368, 87, 48, 108),
340    ] {
341        c.rect(x, y, w, h, '2');
342        c.rect(x, y, w, 3, '4');
343        c.rect(x + w - 5, y + 3, 5, h - 3, '1');
344        for yy in (y + 9..y + h - 4).step_by(9) {
345            for xx in (x + 5..x + w - 7).step_by(8) {
346                let r = pick((xx * 73 + yy * 43) as u32, 10);
347                c.rect(
348                    xx,
349                    yy,
350                    4,
351                    3,
352                    if r < 2 {
353                        'q'
354                    } else if r < 5 {
355                        'g'
356                    } else {
357                        '3'
358                    },
359                );
360            }
361        }
362    }
363    // Low cables and a distant neon torii.
364    c.line(91, 128, 173, 142, '0');
365    c.line(173, 142, 321, 119, '0');
366    c.rect(283, 126, 27, 2, 'h');
367    c.rect(287, 129, 2, 18, 'g');
368    c.rect(304, 129, 2, 18, 'g');
369    c.rect(0, 187, W, 113, '0');
370    c.poly(&[(188, 188), (283, 188), (416, 300), (60, 300)], '1');
371    c.poly(&[(207, 190), (260, 190), (322, 300), (116, 300)], '2');
372    c.poly(&[(252, 191), (257, 191), (289, 300), (266, 300)], 'f');
373    // The wet street is painted with broken, widening horizontal reflections.
374    for i in 0..230u32 {
375        let y = 195 + pick(i * 17 + 2, 91);
376        let d = y - 184;
377        let cx = if i % 3 == 0 {
378            348
379        } else if i % 3 == 1 {
380            150
381        } else {
382            250
383        };
384        let spread = d / 2 + 4;
385        let x = cx + pick(i * 17 + 3, spread * 2) - spread;
386        let len = 2 + pick(i * 17 + 4, (d / 5).max(3));
387        let col = if i % 3 == 0 {
388            ['r', 'e', 'q', '5'][(i / 3 % 4) as usize]
389        } else {
390            ['f', 'g', 'h', 'm'][(i / 3 % 4) as usize]
391        };
392        c.rect(x, y, len, 1, col);
393    }
394    for (x, y, w, k) in [
395        (207, 223, 12, 'h'),
396        (216, 242, 20, 'g'),
397        (225, 258, 30, 'h'),
398        (201, 278, 53, 'g'),
399        (335, 272, 29, 'q'),
400        (325, 280, 47, 'r'),
401        (146, 275, 23, 'i'),
402    ] {
403        c.rect(x, y, w, 1, k);
404    }
405    // Pavement perspective, drain and scattered leaves.
406    for x in [-50, 49, 137, 325, 414, 533] {
407        c.line(235 + (x - 235) / 8, 190, x, 299, '3');
408    }
409    for y in [206, 224, 252, 285] {
410        c.line(0, y, W - 1, y, '1');
411    }
412    c.poly(&[(65, 273), (107, 273), (100, 278), (57, 278)], '0');
413    for x in (65..105).step_by(5) {
414        c.line(x, 274, x - 4, 277, '3');
415    }
416    for i in 0..30u32 {
417        let x = pick(i * 13, W);
418        let y = 245 + pick(i * 13 + 1, 37);
419        c.rect(x, y, 2, 1, if i % 2 == 0 { '5' } else { 'g' });
420    }
421    c
422}
423
424fn train() -> Canvas {
425    let mut c = Canvas::new(280, 31, '.');
426    c.poly(
427        &[
428            (0, 9),
429            (6, 3),
430            (260, 3),
431            (271, 7),
432            (279, 19),
433            (274, 27),
434            (0, 27),
435        ],
436        '1',
437    );
438    c.poly(
439        &[(3, 8), (8, 4), (259, 4), (269, 8), (274, 15), (3, 15)],
440        'o',
441    );
442    c.rect(3, 8, 258, 15, 'p');
443    c.rect(3, 10, 258, 10, 'f');
444    for i in 0..16 {
445        let x = 7 + i * 16;
446        c.rect(x, 11, 12, 7, 'j');
447        c.rect(x + 1, 11, 11, 2, 'k');
448        c.rect(x + 1, 16, 11, 2, 'h');
449        if i % 3 != 0 {
450            c.rect(x + 4, 13, 3, 5, 'g');
451            c.put(x + 5, 12, 'f');
452        }
453        c.rect(x + 12, 10, 2, 11, 'm');
454    }
455    for x in [69, 137, 205] {
456        c.rect(x, 5, 3, 22, '0');
457        c.line(x + 1, 8, x + 1, 25, 'n');
458    }
459    c.rect(1, 22, 270, 3, 'g');
460    c.line(2, 22, 270, 22, 'j');
461    c.line(3, 26, 271, 26, 'm');
462    c.poly(&[(263, 10), (270, 10), (275, 18), (263, 18)], '0');
463    c.line(265, 11, 270, 11, 'j');
464    c.rect(274, 20, 4, 2, 'l');
465    c.rect(2, 20, 2, 2, 'c');
466    for x in [17, 50, 85, 118, 154, 187, 221, 253] {
467        c.rect(x, 27, 9, 3, '0');
468        c.rect(x + 1, 27, 7, 1, 'n');
469    }
470    c.rect(12, 5, 30, 3, '3');
471    c.rect(15, 6, 23, 1, 'j');
472    c.rect(220, 5, 25, 3, '3');
473    c.rect(223, 6, 19, 1, 'a');
474    c
475}
476
477fn frontage() -> Canvas {
478    let mut c = Canvas::new(W, H, '.');
479    // Track, steel ribs and columns. The moving train passes behind this layer.
480    c.rect(0, 176, W, 4, '0');
481    c.rect(0, 177, W, 1, 'n');
482    c.rect(0, 181, W, 9, '2');
483    c.line(0, 181, W - 1, 181, 'g');
484    c.line(0, 189, W - 1, 189, '0');
485    for x in (0..W).step_by(24) {
486        c.line(x, 184, x + 12, 188, '4');
487        c.line(x + 12, 188, x + 24, 184, '4');
488    }
489    for x in [111, 306, 397] {
490        c.rect(x, 190, 8, 37, '1');
491        c.rect(x + 1, 190, 2, 36, '3');
492        c.rect(x - 2, 221, 12, 4, '0');
493    }
494    for x in (93..405).step_by(14) {
495        c.rect(x, 169, 1, 7, '3');
496    }
497    c.line(92, 169, 411, 169, 'n');
498    // Left brick apartment, pipes, warm windows and fire escape.
499    c.rect(0, 51, 77, 218, '1');
500    c.rect(0, 51, 73, 3, '4');
501    c.rect(72, 54, 9, 215, '0');
502    c.rect(2, 57, 5, 200, '3');
503    for y in (60..260).step_by(8) {
504        c.line(9, y, 68, y, '2');
505        for x in (12..68).step_by(17) {
506            let shift = if y % 16 == 12 { 8 } else { 0 };
507            c.line(x + shift, y, x + shift, y + 6, '2');
508        }
509    }
510    for y in [64, 99, 136, 174] {
511        for x in [15, 44] {
512            c.rect(x, y, 20, 25, '0');
513            c.rect(x + 2, y + 2, 15, 19, 'r');
514            c.rect(x + 3, y + 2, 13, 5, 'q');
515            c.rect(
516                x + 3,
517                y + 8,
518                13,
519                12,
520                if (x + y) % 3 == 0 { '5' } else { '8' },
521            );
522            c.rect(x + 9, y + 2, 2, 20, '2');
523            c.rect(x + 2, y + 12, 15, 2, '2');
524            c.rect(x - 1, y + 24, 22, 2, '4');
525        }
526    }
527    c.rect(63, 62, 3, 180, '3');
528    c.line(64, 63, 64, 238, 'n');
529    c.rect(59, 234, 9, 5, '2');
530    c.rect(8, 193, 48, 5, '0');
531    c.line(8, 193, 55, 193, 'n');
532    for x in [10, 21, 32, 43, 54] {
533        c.line(x, 187, x, 193, 'm');
534    }
535    c.line(10, 187, 54, 187, 'm');
536    c.line(20, 199, 51, 229, '0');
537    c.line(16, 199, 47, 229, '0');
538    for i in 0..10 {
539        c.line(16 + i * 3, 199 + i * 3, 22 + i * 3, 199 + i * 3, 'm');
540    }
541    // Large cyan sign is a second focal point, balancing the warm restaurant.
542    c.rect(75, 86, 23, 86, '0');
543    c.rect(77, 87, 21, 83, 'g');
544    c.rect(79, 89, 17, 79, '1');
545    c.line(78, 89, 78, 167, 'j');
546    c.line(96, 89, 96, 167, 'i');
547    c.text(82, 94, "N", 'k', 2);
548    c.text(82, 112, "0", 'k', 2);
549    c.text(82, 130, "9", 'k', 2);
550    c.rect(82, 151, 10, 2, 'j');
551    c.rect(85, 156, 4, 3, 'j');
552    c.line(72, 83, 90, 83, '3');
553    c.line(90, 83, 90, 86, '3');
554    c.rect(14, 219, 46, 31, '0');
555    c.rect(16, 221, 42, 27, '2');
556    c.text(20, 226, "OPEN", 'j', 1);
557    c.text(22, 238, "24 H", 'o', 1);
558    c.rect(0, 260, 83, 8, '0');
559    c.line(0, 260, 82, 260, '4');
560    c.rect(0, 267, 88, 4, '2');
561    // Right apartment tower: a cutaway of dense, lived-in details.
562    c.rect(421, 27, 59, 251, '0');
563    c.rect(424, 30, 56, 244, '2');
564    c.rect(424, 30, 56, 3, '5');
565    c.rect(424, 33, 4, 242, '3');
566    for y in (38..176).step_by(25) {
567        for x in [432, 457] {
568            c.rect(x, y, 17, 17, '0');
569            c.rect(x + 2, y + 2, 12, 12, if y % 3 == 0 { '8' } else { 'g' });
570            c.rect(x + 7, y + 2, 1, 12, '1');
571            c.rect(x + 2, y + 8, 12, 1, '1');
572            c.rect(x - 2, y + 17, 21, 2, '4');
573        }
574    }
575    c.rect(433, 179, 46, 10, '3');
576    for x in (436..477).step_by(6) {
577        c.line(x, 181, x, 186, '0');
578    }
579    c.rect(462, 16, 3, 14, '3');
580    c.line(454, 16, 476, 16, '4');
581    c.rect(407, 56, 16, 104, 'e');
582    c.rect(409, 58, 12, 100, '1');
583    for (i, ch) in "HOTEL".chars().enumerate() {
584        c.text(412, 65 + i as i32 * 18, &ch.to_string(), 'c', 1);
585    }
586    c.rect(411, 153, 8, 2, 'a');
587    // Ramen stall. A broad warm pool of light sits low on the right.
588    c.rect(326, 195, 99, 69, '0');
589    c.rect(329, 202, 91, 53, 'r');
590    c.rect(332, 205, 86, 46, '8');
591    c.rect(337, 207, 77, 36, 'q');
592    c.rect(340, 209, 70, 31, 'a');
593    c.rect(343, 211, 64, 27, '9');
594    c.rect(345, 212, 3, 25, 'r');
595    c.rect(371, 210, 3, 29, 'r');
596    c.rect(400, 210, 3, 29, 'r');
597    // Shelves, bowls, hanging utensils, seated customers, and the cook.
598    c.line(344, 218, 404, 218, 'q');
599    for x in (350..399).step_by(9) {
600        c.rect(x, 214, 4, 3, 'b');
601        c.line(x, 213, x + 3, 213, 'r');
602    }
603    for x in [352, 360, 385, 393] {
604        c.line(x, 220, x, 225, 'r');
605        c.rect(x - 1, 225, 3, 2, 'r');
606    }
607    c.rect(382, 225, 8, 14, 'l');
608    c.rect(381, 222, 9, 3, 'b');
609    c.rect(384, 225, 5, 5, '9');
610    c.rect(381, 232, 10, 7, 'b');
611    c.rect(389, 231, 5, 2, 'l');
612    c.ellipse(352, 234, 4, 4, '2');
613    c.rect(347, 238, 10, 9, '2');
614    c.rect(346, 245, 13, 2, '0');
615    c.ellipse(402, 234, 4, 4, '4');
616    c.rect(398, 238, 9, 9, '3');
617    c.rect(396, 245, 13, 2, '0');
618    c.rect(334, 240, 81, 4, 'b');
619    c.rect(332, 244, 85, 3, 'r');
620    c.rect(335, 247, 78, 11, '5');
621    c.line(336, 247, 411, 247, '8');
622    for x in [362, 392] {
623        c.rect(x, 238, 7, 2, 'l');
624        c.rect(x + 1, 240, 5, 1, 'c');
625    }
626    for x in [347, 399] {
627        c.rect(x, 251, 12, 3, '0');
628        c.rect(x + 3, 254, 2, 10, '3');
629        c.rect(x + 8, 254, 2, 10, '3');
630    }
631    c.poly(&[(319, 201), (327, 186), (420, 186), (432, 201)], '0');
632    c.poly(&[(322, 198), (329, 188), (418, 188), (428, 198)], 'd');
633    for x in (328..420).step_by(14) {
634        c.poly(&[(x, 188), (x + 6, 188), (x + 10, 198), (x + 2, 198)], '9');
635    }
636    c.line(323, 199, 429, 199, 'r');
637    c.rect(325, 200, 102, 3, 'e');
638    for x in (325..426).step_by(7) {
639        c.rect(x, 202, 5, 3, 'e');
640    }
641    // The sign has its own metal frame and brackets.
642    c.rect(337, 170, 75, 15, '0');
643    c.rect(339, 171, 71, 12, 'e');
644    c.rect(340, 172, 69, 10, 'd');
645    c.text(345, 174, "RAMEN 09", 'b', 1);
646    c.rect(344, 185, 2, 3, '0');
647    c.rect(403, 185, 2, 3, '0');
648    // Lantern, curbside vending machine and little planters.
649    c.line(321, 204, 321, 213, '3');
650    c.ellipse(321, 219, 6, 8, 'd');
651    c.ellipse(320, 218, 4, 6, 'c');
652    c.line(321, 213, 321, 225, '9');
653    c.rect(318, 211, 6, 2, '0');
654    c.rect(318, 226, 6, 2, '0');
655    c.rect(434, 210, 26, 50, '0');
656    c.rect(436, 212, 22, 45, '4');
657    c.rect(438, 214, 18, 27, 'g');
658    c.rect(439, 215, 16, 3, 'k');
659    for y in [220, 227, 234] {
660        for x in [440, 446, 452] {
661            c.rect(x, y, 3, 4, if (x + y) % 3 == 0 { 'c' } else { 'j' });
662            c.rect(x, y, 3, 1, 'l');
663        }
664    }
665    c.rect(439, 244, 11, 7, '1');
666    c.rect(452, 244, 3, 2, 'c');
667    c.rect(439, 253, 16, 2, '0');
668    c.rect(416, 252, 12, 11, 'r');
669    c.rect(415, 251, 14, 3, 'q');
670    for (x, y) in [(419, 247), (425, 245), (416, 244), (424, 240)] {
671        c.line(422, 253, x, y, 'g');
672        c.ellipse(x, y, 3, 2, 'h');
673    }
674    // A bicycle and a human-scale detail in the quieter side of the composition.
675    c.ring(127, 263, 9, '0');
676    c.ring(127, 263, 7, 'n');
677    c.ring(153, 263, 9, '0');
678    c.ring(153, 263, 7, 'n');
679    for (x, y, x1, y1) in [
680        (127, 263, 137, 249),
681        (137, 249, 145, 263),
682        (145, 263, 127, 263),
683        (137, 249, 149, 249),
684        (149, 249, 153, 263),
685        (149, 249, 145, 263),
686    ] {
687        c.line(x, y, x1, y1, 'd');
688    }
689    c.line(137, 249, 136, 245, 'n');
690    c.rect(132, 244, 9, 2, '0');
691    c.line(149, 249, 150, 242, 'n');
692    c.line(150, 242, 155, 242, 'n');
693    // Courier with a coral umbrella: the eye's resting point in the street.
694    c.ellipse(236, 270, 12, 2, '0');
695    c.line(235, 244, 232, 268, '0');
696    c.line(238, 246, 242, 268, '0');
697    c.rect(230, 266, 5, 3, '0');
698    c.rect(241, 267, 6, 2, '0');
699    c.poly(&[(231, 233), (240, 233), (242, 249), (230, 249)], '2');
700    c.rect(231, 236, 3, 11, 'm');
701    c.rect(233, 230, 5, 5, 'q');
702    c.rect(232, 228, 7, 4, '0');
703    c.line(241, 236, 245, 239, '3');
704    c.line(245, 222, 245, 242, 'n');
705    c.line(245, 242, 242, 244, 'n');
706    c.poly(
707        &[(213, 228), (219, 220), (232, 213), (244, 216), (258, 228)],
708        '0',
709    );
710    c.poly(
711        &[(215, 226), (221, 220), (232, 215), (242, 218), (255, 226)],
712        'd',
713    );
714    c.poly(&[(215, 226), (231, 216), (229, 226)], 'c');
715    c.poly(&[(230, 226), (233, 216), (242, 218), (246, 226)], '9');
716    c.line(215, 227, 228, 227, 'c');
717    c.line(231, 227, 244, 227, 'd');
718    c.line(247, 227, 255, 227, 'c');
719    c.put(232, 213, 'a');
720    // A small cat waits in the restaurant's reflected light.
721    c.ellipse(368, 269, 6, 3, '0');
722    c.ellipse(368, 265, 4, 4, '2');
723    c.poly(
724        &[(365, 263), (364, 258), (368, 261), (372, 258), (372, 265)],
725        '2',
726    );
727    c.put(366, 263, 'q');
728    c.put(370, 263, 'q');
729    c.line(372, 267, 377, 267, '2');
730    c.line(377, 267, 379, 264, '2');
731    // Fine aerial wires cross open sky, staying out of the moon's centre.
732    for (x, y, x1, y1) in [
733        (0, 30, 94, 61),
734        (94, 61, 170, 49),
735        (307, 41, 420, 19),
736        (317, 44, 479, 34),
737        (0, 78, 69, 84),
738    ] {
739        c.line(x, y, x1, y1, '0');
740    }
741    for x in [28, 51, 367, 391] {
742        c.line(x, 0, x, if x < 100 { 39 } else { 30 }, '0');
743    }
744    c
745}
746
747fn overlay(image: &mut Image, source: &Image, x: i32, y: i32) {
748    for sy in 0..source.height() as i32 {
749        let yy = sy + y;
750        if yy < 0 || yy >= image.height() as i32 {
751            continue;
752        }
753        for sx in 0..source.width() as i32 {
754            let xx = sx + x;
755            if xx < 0 || xx >= image.width() as i32 {
756                continue;
757            }
758            let col = source.pixel(sx as u32, sy as u32).unwrap();
759            if col.a > 0 {
760                let under = image.pixel(xx as u32, yy as u32).unwrap();
761                image.set_pixel(xx as u32, yy as u32, col.over(under));
762            }
763        }
764    }
765}
766fn scale(image: &Image, n: u32) -> spritebox::Result<Image> {
767    let mut out = Image::new(image.width() * n, image.height() * n)?;
768    for y in 0..out.height() {
769        for x in 0..out.width() {
770            out.set_pixel(x, y, image.pixel(x / n, y / n).unwrap());
771        }
772    }
773    Ok(out)
774}
775fn effects(frame: i32) -> Canvas {
776    let mut c = Canvas::new(W, H, '.');
777    // Periodic rain; its entire state returns after exactly 96 frames.
778    for i in 0..225u32 {
779        let y = (pick(i * 29 + 2, 384) + frame * 4) % 384 - 42;
780        let x = (pick(i * 29 + 1, W) - y / 3).rem_euclid(W);
781        let len = 3 + pick(i * 29 + 3, 6);
782        c.line(
783            x,
784            y,
785            x - len / 3,
786            y + len,
787            if i % 7 == 0 { 't' } else { 's' },
788        );
789    }
790    // Broken puddle rings rather than a full, distracting reflection mirror.
791    for i in 0..27u32 {
792        let phase = (frame + pick(i + 401, 32)) % 32;
793        let x = 96 + pick(i + 500, 326);
794        let y = 225 + pick(i + 600, 56);
795        if phase < 16 {
796            let r = phase / 4 + 1;
797            c.line(x - r, y, x + r, y, if phase < 8 { 'g' } else { 'f' });
798            c.put(x - r - 1, y - 1, 'f');
799            c.put(x + r + 1, y - 1, 'f');
800        }
801    }
802    // Slow wisps escaping the restaurant pot; offsets wrap out of sight.
803    for i in 0..5 {
804        let t = (frame + i * 19) % FRAMES;
805        let y = 234 - t / 5;
806        let x = 389 + ((t as f64 * 0.15).sin() * 3.) as i32;
807        c.ellipse(x, y, 1 + t / 32, 2, 'u');
808    }
809    // Lantern light and a subtle heartbeat in the cyan sign.
810    c.ellipse(321, 219, 10, 13, 'v');
811    if frame % 48 < 43 {
812        c.rect(76, 91, 1, 75, 'w');
813        c.rect(98, 91, 2, 75, 'w');
814    }
815    c.rect(12, 284, 112, 1, 'g');
816    c.text(13, 289, "NIGHT SHIFT", 'p', 1);
817    c.text(407, 289, "02:17", 'h', 1);
818    c
819}
820
821fn reflection(frame: i32) -> Canvas {
822    let mut c = Canvas::new(W, H, '.');
823    let x = (frame * 8 + 396) % 768 - 280;
824    for row in 0..7 {
825        let yy = 197 + row * 7;
826        for xx in (x.max(98)..(x + 260).min(417)).step_by(11) {
827            c.rect(
828                xx,
829                yy,
830                3 + row,
831                1,
832                if row < 2 {
833                    'x'
834                } else if row < 4 {
835                    'y'
836                } else {
837                    'z'
838                },
839            );
840        }
841    }
842    c
843}
844
845fn render_frame(
846    frame: i32,
847    city: &Image,
848    train: &Image,
849    street: &Image,
850    palette: &str,
851) -> Result<Image, Box<dyn std::error::Error>> {
852    let mut image = city.clone();
853    overlay(
854        &mut image,
855        &render_canvas(&reflection(frame), palette)?,
856        0,
857        0,
858    );
859    overlay(&mut image, train, (frame * 8 + 396) % 768 - 280, 145);
860    overlay(&mut image, street, 0, 0);
861    overlay(&mut image, &render_canvas(&effects(frame), palette)?, 0, 0);
862    Ok(image)
863}
864fn source_header() -> String {
865    let mut s = String::from(
866        "; NIGHT SHIFT / an original spritebox scene.\n; All shapes are authored procedurally, without a reference bitmap.\npalette night\n",
867    );
868    for (key, value) in PALETTE {
869        writeln!(s, "  {key} {value}").unwrap();
870    }
871    s
872}
873fn render_canvas(canvas: &Canvas, palette: &str) -> Result<Image, Box<dyn std::error::Error>> {
874    let mut s = palette.to_string();
875    canvas.source("effect", &mut s);
876    let d = Document::parse(&s)?;
877    Ok(d.sprite("effect").unwrap().render())
878}
879
880fn main() -> Result<(), Box<dyn std::error::Error>> {
881    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
882    let out = root.join("out/night-shift");
883    let frames = out.join("frames");
884    std::fs::create_dir_all(&frames)?;
885    std::fs::create_dir_all(root.join("examples/night_shift"))?;
886    let palette = source_header();
887    let mut source = palette.clone();
888    background().source("city", &mut source);
889    train().source("last_train", &mut source);
890    frontage().source("street", &mut source);
891    // A clean, editable hero frame uses spritebox's native layer composition.
892    let hero_x = 116;
893    let mut train_hero = Canvas::new(W, H, '.');
894    let train_pixels = train();
895    for y in 0..train_pixels.h {
896        for x in 0..train_pixels.w {
897            train_hero.put(x + hero_x, y + 145, train_pixels.get(x, y));
898        }
899    }
900    train_hero.source("train_at_station", &mut source);
901    reflection(0).source("reflections", &mut source);
902    effects(0).source("rain", &mut source);
903    source.push_str("\nsprite night_shift 480x300 palette=night\n  layer city = city\n  layer reflections = reflections\n  layer train = train_at_station\n  layer street = street\n  layer atmosphere = rain\n");
904    let art_path = root.join("examples/night_shift/night_shift.spritebox");
905    std::fs::write(&art_path, &source)?;
906    let doc = Document::parse(&source)?;
907    let city = doc.sprite("city").unwrap().render();
908    let train = doc.sprite("last_train").unwrap().render();
909    let street = doc.sprite("street").unwrap().render();
910    let hero = doc.sprite("night_shift").unwrap().render();
911    let first = render_frame(0, &city, &train, &street, &palette)?;
912    if hero != first {
913        return Err("The editable layered sprite differs from animation frame zero".into());
914    }
915    if first != render_frame(FRAMES, &city, &train, &street, &palette)? {
916        return Err("The animation does not close seamlessly".into());
917    }
918    png::write_file(out.join("night-shift-native.png"), &hero)?;
919    png::write_file(out.join("night-shift.png"), &scale(&hero, 3)?)?;
920    let mut sheet = Image::new(W as u32 * 2, H as u32 * 2)?;
921    for f in 0..FRAMES {
922        let image = render_frame(f, &city, &train, &street, &palette)?;
923        png::write_file(frames.join(format!("{f:03}.png")), &scale(&image, 3)?)?;
924        if f == 0 {
925            png::write_file(out.join("night-shift.png"), &scale(&image, 3)?)?;
926            png::write_file(out.join("night-shift-native.png"), &image)?;
927        }
928        if f % 24 == 0 {
929            sheet.copy_from(&image, i64::from((f / 24) % 2 * W), i64::from((f / 48) * H));
930            println!("Rendered {f}/{FRAMES} frames");
931        }
932    }
933    png::write_file(out.join("contact-sheet.png"), &sheet)?;
934    if png::read_file(out.join("night-shift-native.png"))? != hero {
935        return Err("PNG round-trip mismatch".into());
936    }
937    // Keep a transparent sheet of useful original assets as well as the scene.
938    png::write_file(out.join("train.png"), &scale(&train, 4)?)?;
939    println!("Art: {}\nFrames: {}", art_path.display(), frames.display());
940    Ok(())
941}