1use 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 png::write_file(out.join("train.png"), &scale(&train, 4)?)?;
939 println!("Art: {}\nFrames: {}", art_path.display(), frames.display());
940 Ok(())
941}