Skip to main content

oxihuman_morph/
eyebrow_shape_library.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Library of eyebrow shape presets (flat, arched, peaked, etc.).
5
6/// Eyebrow shape preset names.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum BrowShapePreset {
10    Flat,
11    Arched,
12    Peaked,
13    Rounded,
14    Straight,
15    Angled,
16    SoftArch,
17    Bushy,
18}
19
20/// Eyebrow shape control parameters.
21#[allow(dead_code)]
22#[derive(Debug, Clone, PartialEq)]
23pub struct BrowShapeParams {
24    /// Overall arch height 0..=1.
25    pub arch_height: f32,
26    /// Peak position along brow 0..=1 (0 = inner, 1 = outer).
27    pub peak_position: f32,
28    /// Brow thickness 0..=1.
29    pub thickness: f32,
30    /// Tail angle (outer end lift/drop) -1..=1.
31    pub tail_angle: f32,
32    /// Inner corner angle -1..=1.
33    pub inner_angle: f32,
34    /// Width scale 0..=2.
35    pub width: f32,
36}
37
38impl Default for BrowShapeParams {
39    fn default() -> Self {
40        Self {
41            arch_height: 0.4,
42            peak_position: 0.6,
43            thickness: 0.5,
44            tail_angle: 0.0,
45            inner_angle: 0.0,
46            width: 1.0,
47        }
48    }
49}
50
51/// Return preset params for a given preset.
52#[allow(dead_code)]
53pub fn preset_params(preset: BrowShapePreset) -> BrowShapeParams {
54    match preset {
55        BrowShapePreset::Flat => BrowShapeParams {
56            arch_height: 0.1,
57            peak_position: 0.5,
58            thickness: 0.5,
59            tail_angle: 0.0,
60            inner_angle: 0.0,
61            width: 1.0,
62        },
63        BrowShapePreset::Arched => BrowShapeParams {
64            arch_height: 0.7,
65            peak_position: 0.6,
66            thickness: 0.4,
67            tail_angle: -0.2,
68            inner_angle: 0.1,
69            width: 1.0,
70        },
71        BrowShapePreset::Peaked => BrowShapeParams {
72            arch_height: 0.9,
73            peak_position: 0.65,
74            thickness: 0.35,
75            tail_angle: -0.3,
76            inner_angle: 0.0,
77            width: 1.0,
78        },
79        BrowShapePreset::Rounded => BrowShapeParams {
80            arch_height: 0.5,
81            peak_position: 0.5,
82            thickness: 0.6,
83            tail_angle: -0.1,
84            inner_angle: -0.1,
85            width: 1.05,
86        },
87        BrowShapePreset::Straight => BrowShapeParams {
88            arch_height: 0.05,
89            peak_position: 0.5,
90            thickness: 0.55,
91            tail_angle: 0.0,
92            inner_angle: 0.0,
93            width: 1.0,
94        },
95        BrowShapePreset::Angled => BrowShapeParams {
96            arch_height: 0.6,
97            peak_position: 0.7,
98            thickness: 0.4,
99            tail_angle: -0.4,
100            inner_angle: 0.2,
101            width: 1.0,
102        },
103        BrowShapePreset::SoftArch => BrowShapeParams {
104            arch_height: 0.45,
105            peak_position: 0.58,
106            thickness: 0.5,
107            tail_angle: -0.1,
108            inner_angle: 0.0,
109            width: 1.0,
110        },
111        BrowShapePreset::Bushy => BrowShapeParams {
112            arch_height: 0.35,
113            peak_position: 0.5,
114            thickness: 0.85,
115            tail_angle: 0.0,
116            inner_angle: 0.0,
117            width: 1.1,
118        },
119    }
120}
121
122/// List all preset names.
123#[allow(dead_code)]
124pub fn all_presets() -> [BrowShapePreset; 8] {
125    [
126        BrowShapePreset::Flat,
127        BrowShapePreset::Arched,
128        BrowShapePreset::Peaked,
129        BrowShapePreset::Rounded,
130        BrowShapePreset::Straight,
131        BrowShapePreset::Angled,
132        BrowShapePreset::SoftArch,
133        BrowShapePreset::Bushy,
134    ]
135}
136
137/// Blend two brow shape params.
138#[allow(dead_code)]
139pub fn blend_brow_shape(a: &BrowShapeParams, b: &BrowShapeParams, t: f32) -> BrowShapeParams {
140    let t = t.clamp(0.0, 1.0);
141    let inv = 1.0 - t;
142    BrowShapeParams {
143        arch_height: a.arch_height * inv + b.arch_height * t,
144        peak_position: a.peak_position * inv + b.peak_position * t,
145        thickness: a.thickness * inv + b.thickness * t,
146        tail_angle: a.tail_angle * inv + b.tail_angle * t,
147        inner_angle: a.inner_angle * inv + b.inner_angle * t,
148        width: a.width * inv + b.width * t,
149    }
150}
151
152/// Evaluate brow height at a normalized x position.
153#[allow(dead_code)]
154pub fn brow_height_at(x: f32, params: &BrowShapeParams) -> f32 {
155    let x = x.clamp(0.0, 1.0);
156    let peak = params.peak_position.clamp(0.0, 1.0);
157    let arch = params.arch_height.clamp(0.0, 1.0);
158    let sigma = 0.25f32;
159    let gaussian = (-(x - peak).powi(2) / (2.0 * sigma * sigma)).exp();
160    arch * gaussian + params.tail_angle * (x - 0.5) + params.inner_angle * (0.5 - x).max(0.0)
161}
162
163/// Reset to default.
164#[allow(dead_code)]
165pub fn reset_brow_shape(params: &mut BrowShapeParams) {
166    *params = BrowShapeParams::default();
167}
168
169/// Serialize to JSON.
170#[allow(dead_code)]
171pub fn brow_shape_to_json(params: &BrowShapeParams) -> String {
172    format!(
173        r#"{{"arch_height":{:.4},"peak_position":{:.4},"thickness":{:.4},"tail_angle":{:.4},"inner_angle":{:.4},"width":{:.4}}}"#,
174        params.arch_height,
175        params.peak_position,
176        params.thickness,
177        params.tail_angle,
178        params.inner_angle,
179        params.width
180    )
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_default() {
189        let p = BrowShapeParams::default();
190        assert!((0.0..=1.0).contains(&p.arch_height));
191    }
192
193    #[test]
194    fn test_preset_flat_low_arch() {
195        let p = preset_params(BrowShapePreset::Flat);
196        assert!(p.arch_height < 0.2);
197    }
198
199    #[test]
200    fn test_preset_peaked_high_arch() {
201        let p = preset_params(BrowShapePreset::Peaked);
202        assert!(p.arch_height > 0.8);
203    }
204
205    #[test]
206    fn test_all_presets_count() {
207        assert_eq!(all_presets().len(), 8);
208    }
209
210    #[test]
211    fn test_blend_midpoint() {
212        let a = BrowShapeParams {
213            arch_height: 0.0,
214            ..Default::default()
215        };
216        let b = BrowShapeParams {
217            arch_height: 1.0,
218            ..Default::default()
219        };
220        let r = blend_brow_shape(&a, &b, 0.5);
221        assert!((r.arch_height - 0.5).abs() < 1e-6);
222    }
223
224    #[test]
225    fn test_brow_height_at_peak() {
226        let p = BrowShapeParams {
227            arch_height: 1.0,
228            peak_position: 0.5,
229            tail_angle: 0.0,
230            inner_angle: 0.0,
231            ..Default::default()
232        };
233        let h_peak = brow_height_at(0.5, &p);
234        let h_edge = brow_height_at(0.0, &p);
235        assert!(h_peak > h_edge);
236    }
237
238    #[test]
239    fn test_brow_height_clamped_x() {
240        let p = BrowShapeParams::default();
241        let h1 = brow_height_at(-1.0, &p);
242        let h2 = brow_height_at(0.0, &p);
243        assert!((h1 - h2).abs() < 1e-6);
244    }
245
246    #[test]
247    fn test_reset() {
248        let mut p = BrowShapeParams {
249            arch_height: 0.99,
250            ..Default::default()
251        };
252        reset_brow_shape(&mut p);
253        assert!((p.arch_height - 0.4).abs() < 1e-6);
254    }
255
256    #[test]
257    fn test_to_json() {
258        let j = brow_shape_to_json(&BrowShapeParams::default());
259        assert!(j.contains("arch_height"));
260        assert!(j.contains("thickness"));
261    }
262
263    #[test]
264    fn test_preset_bushy_thick() {
265        let p = preset_params(BrowShapePreset::Bushy);
266        assert!(p.thickness > 0.8);
267    }
268}