Skip to main content

effects_preview/
preview.rs

1//! Build the effects preview page.
2//!
3//! Embeds the deformation engine — compiled to WebAssembly — into
4//! `preview/index.html`, and writes the result. The page calls it directly, so
5//! the mesh you watch bend in a browser is the one `warp::mesh_part` builds for
6//! a real window. A reimplementation in JavaScript would drift the first time
7//! either side changed, and the drift would be invisible because the page would
8//! still bend something plausible.
9//!
10//! Driven by `dev/preview`, which builds the wasm first.
11//!
12//! ```sh
13//! dev/preview && xdg-open crates/effects/preview/preview.html
14//! ```
15//!
16//! The binary is `effects-preview` rather than `preview`; `Cargo.toml` says
17//! why.
18
19use std::{io::Write as _, path::PathBuf};
20
21use solium_effects::{Axis, Deform};
22
23fn main() {
24    let Some(wasm) = std::env::args_os().nth(1).map(PathBuf::from) else {
25        eprintln!("usage: effects-preview <effects.wasm>");
26        eprintln!("       run dev/preview instead, which builds it first");
27        std::process::exit(2);
28    };
29
30    let bytes = match std::fs::read(&wasm) {
31        Ok(bytes) => bytes,
32        Err(err) => {
33            eprintln!("error: could not read {}: {err}", wasm.display());
34            std::process::exit(1);
35        }
36    };
37
38    // Both lists come from the engine, in the order its `all()` gives them,
39    // which is the order the exported functions index by. A list written out
40    // here would be a second copy of a vocabulary, and the page would select
41    // the wrong effect the first time one was inserted rather than appended.
42    let names = |list: Vec<&str>| {
43        list.into_iter()
44            .map(|name| format!("{name:?}"))
45            .collect::<Vec<_>>()
46            .join(", ")
47    };
48    let effects = names(Deform::all().iter().map(|(name, _)| *name).collect());
49    let axes = names(Axis::all().iter().map(|(name, _)| *name).collect());
50
51    const PAGE: &str = include_str!("../../preview/index.html");
52    let page = PAGE
53        .replace("/*EFFECTS*/ null", &format!("[{effects}]"))
54        .replace("/*AXES*/ null", &format!("[{axes}]"))
55        .replace("/*WASM*/ \"\"", &format!("{:?}", base64(&bytes)));
56
57    let mut stdout = std::io::stdout().lock();
58    if let Err(err) = stdout.write_all(page.as_bytes()) {
59        eprintln!("error: could not write the page: {err}");
60        std::process::exit(1);
61    }
62}
63
64/// Base64, so the wasm can live inside the page.
65///
66/// The same twenty lines as `solium-animation`'s preview builder, and a
67/// deliberate copy rather than a shared helper: sharing it would mean this
68/// crate depending on that one, and the empty `[dependencies]` is what keeps
69/// an effect testable without a compositor. Twenty lines with RFC 4648's own
70/// vectors under them is the cheaper half of that trade.
71fn base64(bytes: &[u8]) -> String {
72    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
73
74    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
75    for chunk in bytes.chunks(3) {
76        let mut block = [0_u8; 3];
77        block[..chunk.len()].copy_from_slice(chunk);
78        let packed = (u32::from(block[0]) << 16) | (u32::from(block[1]) << 8) | u32::from(block[2]);
79
80        for slot in 0..4 {
81            if slot <= chunk.len() {
82                let index = ((packed >> (18 - slot * 6)) & 0x3F) as usize;
83                out.push(char::from(ALPHABET[index]));
84            } else {
85                out.push('=');
86            }
87        }
88    }
89    out
90}
91
92#[cfg(test)]
93mod tests {
94    use super::base64;
95
96    #[test]
97    fn base64_matches_the_standard_including_padding() {
98        // The vectors from RFC 4648, because getting the padding wrong produces
99        // a page that loads and then fails in the browser.
100        assert_eq!(base64(b""), "");
101        assert_eq!(base64(b"f"), "Zg==");
102        assert_eq!(base64(b"fo"), "Zm8=");
103        assert_eq!(base64(b"foo"), "Zm9v");
104        assert_eq!(base64(b"foob"), "Zm9vYg==");
105        assert_eq!(base64(b"fooba"), "Zm9vYmE=");
106        assert_eq!(base64(b"foobar"), "Zm9vYmFy");
107    }
108
109    #[test]
110    fn high_bytes_survive() {
111        assert_eq!(base64(&[0x00, 0x10, 0x83, 0xFF]), "ABCD/w==");
112    }
113
114    /// Every placeholder the page carries is one this binary fills in.
115    ///
116    /// A renamed marker is otherwise a page that loads, shows an empty effect
117    /// list and blames the engine.
118    #[test]
119    fn the_page_has_somewhere_to_put_everything() {
120        const PAGE: &str = include_str!("../../preview/index.html");
121        for marker in ["/*EFFECTS*/ null", "/*AXES*/ null", "/*WASM*/ \"\""] {
122            assert!(PAGE.contains(marker), "the page has no {marker} to fill in");
123        }
124    }
125}