git.lucas.co / cce-core
GUI-free half of the cce toolkit: config, input, IPC, spec parsers
git clone https://git.lucas.co/cce-core.git

src/ramp.rs (4.4K)

  1 //! The DE's ramp spec — `"smooth;0.000:0.500,0.200:1.000,…"` — and the curve it draws:
  2 //! what cce-ui's Ramp widget writes, its relief profiles read, and the window manager's
  3 //! camera transitions evaluate. `cce_ui::widget` and `cce_ui::layout` re-export these.
  4 
  5 /// Serialize ramp keys + line type as the DE's ramp spec string:
  6 /// `"smooth;0.000:0.500,0.200:1.000,…"` (`"linear;…"` for straight segments) —
  7 /// the format ramp-valued params travel in (`ParametersBg` "ramp" rows,
  8 /// project files, `cce_ui::layout::set_bevel_profile_keys` consumers).
  9 pub fn format_ramp_spec(keys: &[(f32, f32)], smooth: bool) -> String {
 10     let body: Vec<String> =
 11         keys.iter().map(|(p, v)| format!("{:.3}:{:.3}", p, v)).collect();
 12     format!("{};{}", if smooth { "smooth" } else { "linear" }, body.join(","))
 13 }
 14 
 15 /// Parse a ramp spec string ([`format_ramp_spec`]) into `(keys, smooth)`.
 16 /// `None` for anything that doesn't yield at least two keys.
 17 pub fn parse_ramp_spec(spec: &str) -> Option<(Vec<(f32, f32)>, bool)> {
 18     let (head, body) = spec.split_once(';')?;
 19     let smooth = head.trim() == "smooth";
 20     let mut keys = Vec::new();
 21     for part in body.split(',') {
 22         let (p, v) = part.split_once(':')?;
 23         keys.push((
 24             p.trim().parse::<f32>().ok()?.clamp(0.0, 1.0),
 25             v.trim().parse::<f32>().ok()?.clamp(0.0, 1.0),
 26         ));
 27     }
 28     if keys.len() < 2 {
 29         return None;
 30     }
 31     keys.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
 32     Some((keys, smooth))
 33 }
 34 
 35 /// Evaluate a ramp key list at `t` — THE ramp interpolation of the DE.
 36 /// `cce_ui::widget::Ramp` draws it, `RampPreview` previews it, the relief
 37 /// profile LUTs sample it, and cce-window-manager's camera speed ramp mirrors
 38 /// it verbatim (that crate stays dependency-minimal), so a curve sculpted in
 39 /// the widget is exactly the curve every consumer evaluates. Keys are
 40 /// `(pos, value)` sorted by pos; outside the key range the end values hold.
 41 ///
 42 /// `smooth` is the widget's curved line type: a **monotone cubic** through
 43 /// the keys (Fritsch–Butland tangents, cubic Hermite segments) — C1, passes
 44 /// through every key, never overshoots a key, and flattens only at the ends
 45 /// and at genuine local extrema. It used to be a smoothstep blend PER
 46 /// SEGMENT, which forces zero slope at every key: a curve with more than two
 47 /// keys came out as a chain of little bumps, and a wall profile built from
 48 /// it read as jagged and uneven where a smooth slope was drawn. A two-key
 49 /// ramp is unchanged — zero tangents at both ends make the single Hermite
 50 /// segment exactly the old smoothstep — so the identity sentinel and every
 51 /// simple ease keep their look. `false` is straight segments.
 52 pub fn sample_ramp_keys(keys: &[(f32, f32)], smooth: bool, t: f32) -> f32 {
 53     let Some(first) = keys.first() else { return 0.0 };
 54     let last = keys.last().unwrap();
 55     if t <= first.0 {
 56         return first.1;
 57     }
 58     if t >= last.0 {
 59         return last.1;
 60     }
 61     for i in 0..keys.len() - 1 {
 62         let ((x0, y0), (x1, y1)) = (keys[i], keys[i + 1]);
 63         if t < x0 || t > x1 {
 64             continue;
 65         }
 66         let h = x1 - x0;
 67         if h.abs() < 0.0001 {
 68             return y0;
 69         }
 70         let s = (t - x0) / h;
 71         if !smooth {
 72             return y0 + (y1 - y0) * s;
 73         }
 74         let (m0, m1) = (ramp_key_tangent(keys, i), ramp_key_tangent(keys, i + 1));
 75         let (s2, s3) = (s * s, s * s * s);
 76         let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
 77         let h10 = s3 - 2.0 * s2 + s;
 78         let h01 = -2.0 * s3 + 3.0 * s2;
 79         let h11 = s3 - s2;
 80         return h00 * y0 + h10 * h * m0 + h01 * y1 + h11 * h * m1;
 81     }
 82     first.1
 83 }
 84 
 85 /// The monotone cubic's tangent (dy/dpos) at key `i`: zero at either end and
 86 /// at any local extremum (so the curve never overshoots a key), otherwise the
 87 /// Fritsch–Butland weighted harmonic mean of the two neighbouring secants —
 88 /// the shape-preserving choice, which keeps every segment monotone whenever
 89 /// its keys are.
 90 fn ramp_key_tangent(keys: &[(f32, f32)], i: usize) -> f32 {
 91     if i == 0 || i + 1 >= keys.len() {
 92         return 0.0;
 93     }
 94     let ((xp, yp), (x, y), (xn, yn)) = (keys[i - 1], keys[i], keys[i + 1]);
 95     let (h0, h1) = (x - xp, xn - x);
 96     if h0 <= 0.0001 || h1 <= 0.0001 {
 97         return 0.0;
 98     }
 99     let (d0, d1) = ((y - yp) / h0, (yn - y) / h1);
100     if d0 * d1 <= 0.0 {
101         return 0.0;
102     }
103     let (w0, w1) = (2.0 * h1 + h0, h1 + 2.0 * h0);
104     (w0 + w1) / (w0 / d0 + w1 / d1)
105 }