data.rsannotateddata.rssource205 lines · 8.4 KB · raw

The art as one text file the app reads (android/app/src/main/resources/art.txt, written by bin/gen-art, a build output): every motion as its frames, and every character in every mood (and every shape of the mouth) as the rectangles and groups it is, with the parts that repeat (the shadow, the weather) written once. The app has one interpreter for it (art/ArtData.kt) and no drawing or formula of its own, so a character added here is in the app at the next build with no Kotlin written.

Format, one item to a line, numbers as Rust prints them (they read back exactly):

characters ID ID ...                    every character, in the order the chooser steps through them
moods NAME ...                          the core's moods, and levels NAME ...: the four shapes of a mouth
grid view=X,Y,W,H center=X,Y half=W gaze=RX,RY,STEP box=X,Y,SIDE   the drawing area, the middle touch is measured from, how far the pupils go, and the square a body stands in
vowels all=aeiouy wide=a round=ou open_until=0.7   the vowels, which opens the mouth how, and how long it stays open in a gap
track NAME PERIOD STEPS(0|1) FRAMES      then FRAMES lines: PERCENT TX TY ROTATE SX SY OPACITY
def N                                    nodes, then `end`: a part a scene uses
scene CHARACTER MOOD SHAPE               nodes, then `end`: SHAPE is `-`, `flap` or a level
nodes:  r X Y W H RRGGBB [ALPHA]  |  g TRACK OX OY  ...  e  |  eyes follow|fixed GX GY  ...  e
        slot  ...  e  |  u N
21use std::collections::{BTreeMap, HashMap};
22use std::fmt::Write;
24use whiskers_pet::Mood;
25
26use crate::character::Character;
27use crate::motion::Motion;
28use crate::node::{Color, Group, Look, Node, Role};
29use crate::scene::{MOODS, Mouth};
30
31fn f(v: f32) -> String {
32    format!("{v:?}")
33}
34
35fn rgb(c: Color) -> String {
36    format!("{:02x}{:02x}{:02x}", c.0, c.1, c.2)
37}
38
39fn node(n: &Node, out: &mut String, motions: &mut Vec<Motion>) {
40    match n {
41        Node::Rect(r) => {
42            let alpha = if r.alpha == 1.0 { String::new() } else { format!(" {}", f(r.alpha)) };
43            let _ = writeln!(out, "r {} {} {} {} {}{alpha}", f(r.x), f(r.y), f(r.w), f(r.h), rgb(r.color));
44        }
45        Node::Group(g) => group(g, out, motions),
46        Node::Slot(over) => {
47            out.push_str("slot\n");
48            for child in over {
49                node(child, out, motions);
50            }
51            out.push_str("e\n");
52        }
53    }
54}
55
56fn group(g: &Group, out: &mut String, motions: &mut Vec<Motion>) {
57    match g.role {
58        Role::Eyes(Look::Follow) => out.push_str("eyes follow\n"),
59        Role::Eyes(Look::Fixed(x, y)) => {
60            let _ = writeln!(out, "eyes fixed {} {}", f(x), f(y));
61        }
62        Role::Plain => {
63            if !motions.contains(&g.motion) {
64                motions.push(g.motion);
65            }
66            let _ = writeln!(out, "g {} {} {}", g.motion.name(), f(g.origin.0), f(g.origin.1));
67        }
68    }
69    for child in &g.children {
70        node(child, out, motions);
71    }
72    out.push_str("e\n");
73}

The shapes a mood is written in: the mouth's four levels and its beat, for speaking; one for the rest.

76fn variants(mood: Mood) -> Vec<(&'static str, Option<Mouth>)> {
77    if mood == Mood::Speaking {
78        let mut v = vec![("flap", None)];
79        v.extend(Mouth::ALL.map(|m| (level_name(m), Some(m))));
80        v
81    } else {
82        vec![("-", None)]
83    }
84}

The name a level has in the file and in the app's MouthShape.

87pub fn level_name(m: Mouth) -> &'static str {
88    match m {
89        Mouth::Closed => "Closed",
90        Mouth::Small => "Small",
91        Mouth::Wide => "Wide",
92        Mouth::Round => "Round",
93    }
94}

The whole file.

97pub fn generate() -> String {
98    let mut motions: Vec<Motion> = Vec::new();
99    // Each scene's top-level parts as text; a part that two scenes share is written once as a `def`.
100    let mut scenes: Vec<(String, Vec<String>)> = Vec::new();
101    for character in Character::all() {
102        for mood in MOODS {
103            for (shape, mouth) in variants(mood) {
104                let parts: Vec<String> = character
105                    .scene(mood, mouth)
106                    .iter()
107                    .map(|n| {
108                        let mut s = String::new();
109                        node(n, &mut s, &mut motions);
110                        s
111                    })
112                    .collect();
113                scenes.push((format!("scene {} {mood:?} {shape}", character.id()), parts));
114            }
115        }
116    }
117    let mut uses: HashMap<&str, usize> = HashMap::new();
118    for (_, parts) in &scenes {
119        for p in parts {
120            *uses.entry(p.as_str()).or_default() += 1;
121        }
122    }
123    let mut ids: BTreeMap<&str, usize> = BTreeMap::new();
124    let mut defs = String::new();
125    for (_, parts) in &scenes {
126        for p in parts {
127            if uses[p.as_str()] > 1 && !ids.contains_key(p.as_str()) {
128                let id = ids.len();
129                ids.insert(p.as_str(), id);
130                let _ = write!(defs, "def {id}\n{p}end\n");
131            }
132        }
133    }
134    let mut out = String::new();
135    out.push_str("# GENERATED by `cargo run -p whiskers-art --bin gen-art` from crates/whiskers-art. Do not edit.\n");
136    let names: Vec<&str> = Character::all().iter().map(|c| c.id()).collect();
137    let _ = writeln!(out, "characters {}", names.join(" "));
138    let moods: Vec<String> = MOODS.iter().map(|m| format!("{m:?}")).collect();
139    let _ = writeln!(out, "moods {}", moods.join(" "));
140    let levels: Vec<&str> = Mouth::ALL.iter().map(|m| level_name(*m)).collect();
141    let _ = writeln!(out, "levels {}", levels.join(" "));
142    let (vx, vy, vw, vh) = crate::scene::VIEW;
143    let _ = writeln!(
144        out,
145        "grid view={},{},{},{} center={},{} half={} gaze={},{},{} box={},{},{}",
146        f(vx), f(vy), f(vw), f(vh),
147        f(crate::scene::CENTER.0), f(crate::scene::CENTER.1),
148        f(crate::scene::HALF_WIDTH),
149        f(crate::scene::GAZE_REACH.0), f(crate::scene::GAZE_REACH.1), f(crate::scene::GAZE_STEP),
150        f(crate::body::BOX.0), f(crate::body::BOX.1), f(crate::body::BOX.2),
151    );
152    let _ = writeln!(out, "vowels all={} wide={} round={} open_until={}", pixel_mouth::VOWELS, pixel_mouth::WIDE_VOWELS, pixel_mouth::ROUND_VOWELS, f(pixel_mouth::OPEN_UNTIL));
153    motions.sort_by_key(|m| m.name());
154    for m in &motions {
155        let t = m.played();
156        let _ = writeln!(out, "track {} {} {} {}", m.name(), f(t.period), u8::from(t.steps), t.frames.len());
157        for (percent, xf) in &t.frames {
158            let _ = writeln!(out, "{} {} {} {} {} {} {}", f(*percent), f(xf.tx), f(xf.ty), f(xf.rotate), f(xf.sx), f(xf.sy), f(xf.opacity));
159        }
160    }
161    out.push_str(&defs);
162    for (head, parts) in &scenes {
163        let _ = writeln!(out, "{head}");
164        for p in parts {
165            match ids.get(p.as_str()) {
166                Some(id) => {
167                    let _ = writeln!(out, "u {id}");
168                }
169                None => out.push_str(p),
170            }
171        }
172        out.push_str("end\n");
173    }
174    out
175}
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn every_character_mood_and_shape_is_written_and_every_group_is_closed_and_has_its_track() {
183        let text = generate();
184        let heads = text.lines().filter(|l| l.starts_with("scene ")).count();
185        assert_eq!(heads, 13 * (10 + 5));
186        let opens = text.lines().filter(|l| l.starts_with("g ") || l.starts_with("eyes ") || *l == "slot").count();
187        let closes = text.lines().filter(|l| *l == "e").count();
188        assert_eq!(opens, closes);
189        let tracks: Vec<&str> = text.lines().filter_map(|l| l.strip_prefix("track ")).filter_map(|l| l.split(' ').next()).collect();
190        for line in text.lines().filter_map(|l| l.strip_prefix("g ")) {
191            assert!(tracks.contains(&line.split(' ').next().unwrap()), "{line}");
192        }
193        assert!(!text.contains("NaN") && !text.contains("inf"));
194    }
195
196    #[test]
197    fn a_part_two_scenes_share_is_written_once() {
198        let text = generate();
199        assert!(text.lines().filter(|l| l.starts_with("def ")).count() > 10);
200        // The 13 characters' idle scenes all start with the same shadow, as a use of one def.
201        let shadow = text.lines().find(|l| l.starts_with("r 3.0 15.0 9.0 1.0 000000")).map(|_| ()).is_some();
202        assert!(shadow, "the shadow is written once, inside a def");
203        assert!(text.len() < 400_000, "{} bytes", text.len());
204    }
205}