The demo's stage: a black window the cat stands in, so what is drawn round it (white speech and thought bubbles, pale z's, stars, hearts, sweat) can be seen. The colour is here, once; the page's stylesheet takes it from here (a test holds them together), and the tests read the scenes against it.
6use crate::node::Color;
The stage's floor and backdrop.
9pub const STAGE: Color = Color::hex(0x000000);
Graphical objects must stand out by at least 3 to 1 (WCAG 1.4.11).
12pub const MIN_CONTRAST: f64 = 3.0;
WCAG relative luminance.
color laid over under at alpha, as the screen mixes it (in sRGB, as a browser does).
WCAG contrast ratio, 1 to 21.
Every rectangle of a scene in paint order (a group's children after the group's turn).
52 fn painted(mascot: Mascot, mood: Mood) -> Vec<Rect> { 53 let mut out = Vec::new(); 54 rects(&scene(mascot, mood).nodes, &mut out); 55 out 56 } 57 58 fn on_stage(r: &Rect) -> Color { 59 over(r.color, r.alpha, STAGE) 60 } 61 62 fn inside(outer: &Rect, inner: &Rect) -> bool { 63 let (cx, cy) = (inner.x + inner.w / 2.0, inner.y + inner.h / 2.0); 64 cx >= outer.x && cx <= outer.x + outer.w && cy >= outer.y && cy <= outer.y + outer.h 65 } 66 67 #[test] 68 fn the_ratio_is_the_wcag_one() { 69 assert!((contrast(Color::hex(0xFFFFFF), Color::hex(0x000000)) - 21.0).abs() < 1e-9); 70 assert!((contrast(Color::hex(0x777777), Color::hex(0x777777)) - 1.0).abs() < 1e-9); 71 assert_eq!(over(Color::hex(0xFFFFFF), 0.5, STAGE), Color(128, 128, 128)); 72 } 73 74 #[test] 75 fn the_stage_is_black() { 76 assert_eq!(STAGE.css(), "#000000"); 77 } 78 79 // The figure is drawn light on the stage, and so are the things round it. A dark mark (a 80 // pupil, a mouth, a cloud's dots) is fine when it lies on a light shape painted before it. 81 // The one dark mark on the bare stage is the ground shadow, which the black swallows: it 82 // is the first thing painted, it is half-transparent black, and nothing depends on seeing it. 83 #[test] 84 fn nothing_is_dark_on_the_dark_stage_in_any_mood_of_any_mascot() { 85 for &mascot in Mascot::ALL { 86 for mood in Mood::ALL { 87 let all = painted(mascot, mood); 88 for (i, r) in all.iter().enumerate() { 89 if contrast(on_stage(r), STAGE) >= MIN_CONTRAST { 90 continue; 91 } 92 let shadow = i == 0 && r.color == Color::hex(0x000000) && r.alpha < 1.0; 93 let on_light = all[..i].iter().any(|under| contrast(on_stage(under), STAGE) >= MIN_CONTRAST && under.alpha >= 0.85 && inside(under, r)); 94 assert!(shadow || on_light, "{mascot:?} {mood:?}: {r:?} is dark and lies on the bare stage"); 95 } 96 } 97 } 98 } 99 100 #[test] 101 fn every_colour_drawn_round_the_figure_stands_out_from_the_stage() { 102 // The weather is what `decor::around` and `zs` add: a colour a mood draws that the 103 // same mascot's idle scene does not. 104 let mut weather = Vec::new(); 105 for &mascot in Mascot::ALL { 106 let idle: Vec<Color> = painted(mascot, Mood::Idle).iter().map(|r| r.color).collect(); 107 for mood in Mood::ALL { 108 for r in painted(mascot, mood) { 109 if !idle.contains(&r.color) { 110 weather.push((mascot, mood, r)); 111 } 112 } 113 } 114 } 115 assert!(!weather.is_empty()); 116 for (mascot, mood, r) in weather { 117 assert!(contrast(on_stage(&r), STAGE) >= MIN_CONTRAST, "{mascot:?} {mood:?}: {r:?} is {:.2} to 1", contrast(on_stage(&r), STAGE)); 118 } 119 } 120 121 #[test] 122 fn each_named_effect_colour_stands_out_from_the_stage() { 123 // The effects the mood row names, by colour, each against the stage. 124 for (name, color, alpha) in [ 125 ("a speech wave and a thought cloud", Color::hex(0xFFFFFF), 0.9_f32), 126 ("the z's", Color::hex(0xD6E6FF), 1.0), 127 ("the stars", Color::hex(0xFFD84A), 1.0), 128 ("a heart", Color::hex(0xFF8FA3), 1.0), 129 ("a drop of sweat", Color::hex(0x6EC1FF), 1.0), 130 ("the offline cross", Color::hex(0xFF5A5A), 1.0), 131 ] { 132 assert!(contrast(over(color, alpha, STAGE), STAGE) >= MIN_CONTRAST, "{name}"); 133 } 134 } 135}