Jelajahi Sumber

app/ui/vector_art: add inset box shadow

x 2 minggu lalu
induk
melakukan
ccf6019fae

+ 1 - 0
bin/app/src/ui/vector_art/mod.rs

@@ -36,6 +36,7 @@ use crate::{
 
 use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
+mod shadow;
 pub mod shape;
 
 pub type VectorArtPtr = Arc<VectorArt>;

+ 81 - 0
bin/app/src/ui/vector_art/shadow.rs

@@ -0,0 +1,81 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use super::shape::sexpr_add;
+use crate::expr::{Op, SExprCode};
+
+/// Gaussian CDF `0.5 * (1 + erf(dist * sqrt(2) / blur))`: coverage of the
+/// box interior at `dist` px from an edge.
+fn edge_cdf(dist: f32, blur: f32) -> f32 {
+    if blur <= 0. {
+        return if dist >= 0. { 1. } else { 0. };
+    }
+    let z = dist * std::f32::consts::SQRT_2 / blur;
+    // This magic is Abramowitz & Stegun 7.1.26, max abs error 1.5e-7
+    let t = 1. / (1. + 0.327_591_1 * z.abs());
+    let approx = 1. -
+        t * (0.254_829_592 +
+            t * (-0.284_496_736 +
+                t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429)))) *
+            (-z * z).exp();
+    let erf = if z < 0. { -approx } else { approx };
+    0.5 * (1. + erf)
+}
+
+/// Knot offsets from one edge, one ring every `blur / 4` px. The fade is
+/// gone by ~2 * blur, so 8 rings cover it.
+pub(crate) fn ring_knots(blur: f32) -> Vec<f32> {
+    let n = 8;
+    let spacing = blur / 4.;
+    (0..=n).map(|k| k as f32 * spacing).collect()
+}
+
+/// Inset shadow alpha at a knot.
+pub(crate) fn inset_alpha(dx: f32, dy: f32, blur: f32, base: f32) -> f32 {
+    let a = base * (1. - edge_cdf(dx, blur) * edge_cdf(dy, blur));
+    if a < 1e-4 * base {
+        0.
+    } else {
+        a
+    }
+}
+
+/// Symbolic position for every knot on one axis. Returns `(position, off)`
+/// so callers know each knot's distance from its edge.
+pub(crate) fn knot_sexprs(offs: &[f32], e1: &SExprCode, e2: &SExprCode) -> Vec<(SExprCode, f32)> {
+    let center = sexpr_add(
+        e1.clone(),
+        Op::Mul((
+            Box::new(Op::Sub((Box::new(e2[0].clone()), Box::new(e1[0].clone())))),
+            Box::new(Op::ConstFloat32(0.5)),
+        )),
+    )
+    .unwrap();
+    let n = offs.len();
+    let near = (0..n).map(|k| {
+        let raw = sexpr_add(e1.clone(), Op::ConstFloat32(offs[k])).unwrap();
+        let head = raw.into_iter().next().unwrap();
+        (vec![Op::Min((Box::new(head), Box::new(center[0].clone())))], offs[k])
+    });
+    let far = (0..n).rev().map(|k| {
+        let raw = sexpr_add(e2.clone(), Op::ConstFloat32(-offs[k])).unwrap();
+        let head = raw.into_iter().next().unwrap();
+        (vec![Op::Max((Box::new(head), Box::new(center[0].clone())))], offs[k])
+    });
+    near.chain(far).collect()
+}

+ 44 - 1
bin/app/src/ui/vector_art/shape.rs

@@ -16,6 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use super::shadow;
 use crate::{
     error::Result,
     expr::{Op, SExprCode, SExprMachine, SExprVal},
@@ -64,7 +65,7 @@ impl ShapeVertex {
 // This is bullshit. We need something in expr to support joining exprs somehow. Subroutines.
 // Hacky temp workaround for now.
 // s-expr surgery
-fn sexpr_add(mut x: SExprCode, op: Op) -> Option<SExprCode> {
+pub(crate) fn sexpr_add(mut x: SExprCode, op: Op) -> Option<SExprCode> {
     let eqn = x.pop()?;
     x.push(Op::Add((Box::new(eqn), Box::new(op))));
     Some(x)
@@ -221,6 +222,48 @@ impl VectorShape {
         }
     }
 
+    /// Inset box shadow: Gaussian fade (50% alpha at the edge, gone by
+    /// ~`2 * blur`).
+    pub fn add_box_shadow_inset(
+        &mut self,
+        x1: SExprCode,
+        y1: SExprCode,
+        x2: SExprCode,
+        y2: SExprCode,
+        blur: f32,
+        color: Color,
+    ) {
+        let offs = shadow::ring_knots(blur);
+        let xs = shadow::knot_sexprs(&offs, &x1, &x2);
+        let ys = shadow::knot_sexprs(&offs, &y1, &y2);
+
+        let knot_alpha = |i: usize, j: usize| shadow::inset_alpha(xs[i].1, ys[j].1, blur, color[3]);
+
+        // Vertex grid
+        let stride = ys.len() as u16;
+        for (i, (x, _)) in xs.iter().enumerate() {
+            for (j, (y, _)) in ys.iter().enumerate() {
+                let mut c = color;
+                c[3] = knot_alpha(i, j);
+                self.verts.push(ShapeVertex::new(x.clone(), y.clone(), c));
+            }
+        }
+        // Skip fully transparent interior quads
+        for i in 0..xs.len() - 1 {
+            for j in 0..ys.len() - 1 {
+                if knot_alpha(i, j) == 0. &&
+                    knot_alpha(i + 1, j) == 0. &&
+                    knot_alpha(i, j + 1) == 0. &&
+                    knot_alpha(i + 1, j + 1) == 0.
+                {
+                    continue
+                }
+                let v = (i * stride as usize + j) as u16;
+                self.indices.extend([v, v + stride, v + 1, v + 1, v + stride, v + stride + 1]);
+            }
+        }
+    }
+
     pub fn add_outline(
         &mut self,
         x1: SExprCode,