Skip to main content

ravenrs_widgets/
quad_field.rs

1//! [`QuadField`]: a plane of small squares that waves like a floor and, as the page
2//! scrolls, stands up into a picture drawn square by square.
3
4use std::cell::Cell;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_core::{Color, Point, Quad, Rect, Scene};
9use ravenrs_layout::{Align, Axis, LayoutStyle};
10use ravenrs_style::Semantic;
11
12use crate::View;
13use crate::context::{BuildCx, NodeId, Paint};
14use crate::scroll_scene::ScrollRange;
15
16type Picture = Rc<dyn Fn(f32, f32, f64) -> Color>;
17
18/// A plane of `columns × rows` squares drawn on every frame. At rest it lies like a
19/// floor that waves, lit in the accent and lifting under the pointer; as the scroll
20/// progresses through its [`ScrollRange`] it stands up and becomes the picture its
21/// function paints, one color per square. Nothing is rebuilt or laid out while it
22/// moves. Under reduced motion the floor stands still.
23///
24/// ```
25/// use ravenrs_core::Color;
26/// use ravenrs_widgets::QuadField;
27///
28/// // A blue square on a dark screen.
29/// let field = QuadField::new(96, 54, |u, v, _time| {
30///     if (0.4..0.6).contains(&u) && (0.4..0.6).contains(&v) { Color::hex(0x4c66ee) } else { Color::hex(0x12151c) }
31/// })
32/// .height(720.0);
33/// ```
34#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
35pub struct QuadField {
36    columns: u16,
37    rows: u16,
38    picture: Picture,
39    height: f32,
40    range: ScrollRange,
41}
42
43impl fmt::Debug for QuadField {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.debug_struct("QuadField")
46            .field("columns", &self.columns)
47            .field("rows", &self.rows)
48            .finish_non_exhaustive()
49    }
50}
51
52#[cfg_attr(feature = "live", ravenrs_live::expose)]
53impl QuadField {
54    /// A field of `columns × rows` squares forming the picture `picture(u, v, seconds)`,
55    /// where `u` and `v` run from 0 to 1 across and down.
56    pub fn new(columns: u16, rows: u16, picture: impl Fn(f32, f32, f64) -> Color + 'static) -> Self {
57        Self {
58            columns: columns.max(1),
59            rows: rows.max(1),
60            picture: Rc::new(picture),
61            height: 600.0,
62            range: ScrollRange::Scene,
63        }
64    }
65
66    /// The height of the field (600 by default); it fills the width it is given.
67    pub fn height(mut self, height: f32) -> Self {
68        self.height = height;
69        self
70    }
71
72    /// The stretch of scrolling over which it stands up ([`ScrollRange::Scene`] by
73    /// default: the scene around it, or its own pass across the view outside one).
74    pub fn range(mut self, range: ScrollRange) -> Self {
75        self.range = range;
76        self
77    }
78}
79
80impl View for QuadField {
81    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
82        let theme = *cx.theme();
83        let accent = theme.color(Semantic::Accent);
84        let glow = theme.color(Semantic::FocusRing);
85        let pointer = cx.use_state(|| Rc::new(Cell::new(Point::new(-1e4, -1e4)))).get();
86        let progress = Rc::new(Cell::new(0.0_f32));
87        let shown = progress.clone();
88        let (columns, rows, picture) = (self.columns, self.rows, self.picture.clone());
89        let at = pointer.clone();
90        let paint = Paint::Live(Rc::new(move |frame: Rect, time: f64, scene: &mut Scene| {
91            let e = smooth(shown.get());
92            draw(scene, frame, time, e, at.get(), columns, rows, &picture, accent, glow);
93        }));
94        let node = cx.container(
95            LayoutStyle {
96                height: Some(self.height),
97                align_self: Some(Align::Stretch),
98                ..LayoutStyle::stack(Axis::Vertical)
99            },
100            &[],
101            paint,
102        );
103        cx.scroll_progress(node, self.range, progress);
104        cx.on_pointer_move(
105            node,
106            Rc::new(move |p, frame| pointer.set(Point::new(p.x - frame.x(), p.y - frame.y()))),
107        );
108        cx.clip(node);
109        node
110    }
111}
112
113fn smooth(t: f32) -> f32 {
114    let t = t.clamp(0.0, 1.0);
115    t * t * (3.0 - 2.0 * t)
116}
117
118/// Paints the field for `e`, from 0 (the floor) to 1 (the picture, facing the viewer).
119#[allow(clippy::too_many_arguments)]
120fn draw(
121    scene: &mut Scene,
122    frame: Rect,
123    time: f64,
124    e: f32,
125    pointer: Point,
126    columns: u16,
127    rows: u16,
128    picture: &Picture,
129    accent: Color,
130    glow: Color,
131) {
132    let (cw, ch) = (frame.width(), frame.height());
133    if cw <= 0.0 || ch <= 0.0 {
134        return;
135    }
136    let t = time as f32;
137    let (c, r) = (f32::from(columns), f32::from(rows));
138    let w = (cw * 0.9).min(1180.0) / 2.0;
139    let s = 2.3 - 1.3 * e;
140    let hh = w * r / c;
141    let a = 1.22 * (1.0 - e);
142    let (ca, sa) = (a.cos(), a.sin());
143    let d = 900.0;
144    // A screen taller than wide leaves more room below the text over the floor.
145    let base = if cw < ch { 0.86 } else { 0.7 };
146    let center = Point::new(cw * 0.5, ch * (base - (base - 0.5) * e));
147    let amp = 30.0 * (1.0 - e);
148    let cell = 2.0 * w / c;
149    let sweep = (t * 0.22) % 1.8 - 0.4;
150    for j in 0..rows {
151        for i in 0..columns {
152            let u = (f32::from(i) + 0.5) / c;
153            let v = (f32::from(j) + 0.5) / r;
154            let x = (u * 2.0 - 1.0) * w * s;
155            let py = (v * 2.0 - 1.0) * hh * s;
156            let h = amp
157                * ((x * 0.006 + t * 1.1).sin()
158                    + 0.6 * (py * 0.01 - t * 1.5).sin()
159                    + 0.4 * ((x + py) * 0.004 + t * 0.6).sin());
160            let dz = d + py * sa + h * ca;
161            if dz < 60.0 {
162                continue;
163            }
164            let k = d / dz;
165            let mut p = Point::new(center.x + x * k, center.y + (py * ca - h * sa) * k);
166            let (dx, dy) = (p.x - pointer.x, p.y - pointer.y);
167            let near = (-(dx * dx + dy * dy) / 9000.0).exp();
168            p.y -= near * 46.0 * (1.0 - e);
169            if p.x < -20.0 || p.x > cw + 20.0 || p.y < -20.0 || p.y > ch + 20.0 {
170                continue;
171            }
172            let fl = ((h / amp.max(0.001) + 1.4) / 2.8).clamp(0.0, 1.0);
173            let depth = (k * 0.9).clamp(0.0, 1.0);
174            let floor = accent.lerp(glow, 0.45 * fl);
175            let floor_alpha = ((0.15 + 0.85 * fl) * depth + near * 0.6).min(1.0);
176            let mut shown = picture(u, v, time);
177            let shine = (1.0 - (u - sweep - v * 0.3).abs() * 9.0).max(0.0) * 0.25 + near * 0.3;
178            shown = shown.lerp(Color::WHITE, shine * e);
179            let color = floor.with_alpha(floor_alpha).lerp(shown, e);
180            let size = (cell * s * k * (0.2 + 0.66 * e)).max(1.0);
181            let rect = Rect::new(frame.x() + p.x - size / 2.0, frame.y() + p.y - size / 2.0, size, size);
182            scene.push_quad(Quad::new(rect, color));
183        }
184    }
185}