Skip to main content

preview/
preview.rs

1//! Build the animation preview page.
2//!
3//! Embeds two engines — the animation curves and the window arrangements, both
4//! compiled to WebAssembly — into `preview/index.html`, and writes the result.
5//! The page calls them directly, so what is tuned in a browser is the code that
6//! moves and arranges real windows. A reimplementation in JavaScript would
7//! drift the first time either side changed, and the drift would be invisible
8//! because the page would still look plausible.
9//!
10//! Driven by `dev/preview`, which builds the wasm first.
11//!
12//! ```sh
13//! dev/preview && xdg-open crates/animation/preview/preview.html
14//! ```
15
16use std::{io::Write as _, path::PathBuf};
17
18use solium_animation::Curve;
19
20fn main() {
21    let mut args = std::env::args_os().skip(1).map(PathBuf::from);
22    let (Some(animation), Some(layout)) = (args.next(), args.next()) else {
23        eprintln!("usage: preview <animation.wasm> <layout.wasm>");
24        eprintln!("       run dev/preview instead, which builds them first");
25        std::process::exit(2);
26    };
27
28    let read = |path: &PathBuf| match std::fs::read(path) {
29        Ok(bytes) => bytes,
30        Err(err) => {
31            eprintln!("error: could not read {}: {err}", path.display());
32            std::process::exit(1);
33        }
34    };
35    let bytes = read(&animation);
36    let layout_bytes = read(&layout);
37
38    let names: Vec<String> = Curve::all()
39        .into_iter()
40        .map(|(name, _)| format!("{name:?}"))
41        .collect();
42
43    const PAGE: &str = include_str!("../../preview/index.html");
44    let page = PAGE
45        .replace("/*CURVES*/ null", &format!("[{}]", names.join(", ")))
46        .replace("/*WASM*/ \"\"", &format!("{:?}", base64(&bytes)))
47        .replace("/*LAYOUT*/ \"\"", &format!("{:?}", base64(&layout_bytes)));
48
49    let mut stdout = std::io::stdout().lock();
50    if let Err(err) = stdout.write_all(page.as_bytes()) {
51        eprintln!("error: could not write the page: {err}");
52        std::process::exit(1);
53    }
54}
55
56/// Base64, so the wasm can live inside the page.
57///
58/// Hand-rolled rather than pulled in: this crate must stay dependency-free so
59/// it keeps building for the browser, and this is twenty lines.
60fn base64(bytes: &[u8]) -> String {
61    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
62
63    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
64    for chunk in bytes.chunks(3) {
65        let mut block = [0_u8; 3];
66        block[..chunk.len()].copy_from_slice(chunk);
67        let packed = (u32::from(block[0]) << 16) | (u32::from(block[1]) << 8) | u32::from(block[2]);
68
69        for slot in 0..4 {
70            if slot <= chunk.len() {
71                let index = ((packed >> (18 - slot * 6)) & 0x3F) as usize;
72                out.push(char::from(ALPHABET[index]));
73            } else {
74                out.push('=');
75            }
76        }
77    }
78    out
79}
80
81#[cfg(test)]
82mod tests {
83    use super::base64;
84
85    #[test]
86    fn base64_matches_the_standard_including_padding() {
87        // The vectors from RFC 4648, because getting the padding wrong produces
88        // a page that loads and then fails in the browser.
89        assert_eq!(base64(b""), "");
90        assert_eq!(base64(b"f"), "Zg==");
91        assert_eq!(base64(b"fo"), "Zm8=");
92        assert_eq!(base64(b"foo"), "Zm9v");
93        assert_eq!(base64(b"foob"), "Zm9vYg==");
94        assert_eq!(base64(b"fooba"), "Zm9vYmE=");
95        assert_eq!(base64(b"foobar"), "Zm9vYmFy");
96    }
97
98    #[test]
99    fn high_bytes_survive() {
100        assert_eq!(base64(&[0x00, 0x10, 0x83, 0xFF]), "ABCD/w==");
101    }
102}