Skip to main content

oxihuman_morph/
age_progression_adv.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Advanced aging pipeline: piecewise-linear age stages, morph deltas, skin/face/body params.
5
6#[allow(dead_code)]
7/// One point on the aging curve.
8#[derive(Debug, Clone, PartialEq)]
9pub struct AgeStage {
10    pub years: f32,
11    pub fat_pct: f32,
12    pub muscle_pct: f32,
13    pub bone_density: f32,
14    pub skin_elasticity: f32,
15}
16
17#[allow(dead_code)]
18/// Piecewise-linear aging model.
19#[derive(Debug, Clone)]
20pub struct AgingCurve {
21    pub stages: Vec<AgeStage>,
22}
23
24#[allow(dead_code)]
25/// Person base profile.
26#[derive(Debug, Clone)]
27pub struct AgeProfile {
28    pub base_age: f32,
29    /// 0 = male, 1 = female
30    pub sex: u8,
31    /// 0..=3 ethnicity index
32    pub ethnicity: u8,
33}
34
35// ---------------------------------------------------------------------------
36// Core interpolation
37// ---------------------------------------------------------------------------
38
39/// Linear interpolation of two AgeStages.
40pub fn interpolate_age_stages(a: &AgeStage, b: &AgeStage, t: f32) -> AgeStage {
41    let t = t.clamp(0.0, 1.0);
42    AgeStage {
43        years: a.years + (b.years - a.years) * t,
44        fat_pct: a.fat_pct + (b.fat_pct - a.fat_pct) * t,
45        muscle_pct: a.muscle_pct + (b.muscle_pct - a.muscle_pct) * t,
46        bone_density: a.bone_density + (b.bone_density - a.bone_density) * t,
47        skin_elasticity: a.skin_elasticity + (b.skin_elasticity - a.skin_elasticity) * t,
48    }
49}
50
51/// Interpolate the aging curve at a given age in years.
52pub fn compute_age_stage(curve: &AgingCurve, years: f32) -> AgeStage {
53    let stages = &curve.stages;
54    if stages.is_empty() {
55        return AgeStage {
56            years,
57            fat_pct: 0.25,
58            muscle_pct: 0.40,
59            bone_density: 1.0,
60            skin_elasticity: 0.8,
61        };
62    }
63    if years <= stages[0].years {
64        return stages[0].clone();
65    }
66    let last = &stages[stages.len() - 1];
67    if years >= last.years {
68        return last.clone();
69    }
70    for i in 0..stages.len() - 1 {
71        let a = &stages[i];
72        let b = &stages[i + 1];
73        if years >= a.years && years <= b.years {
74            let t = (years - a.years) / (b.years - a.years);
75            return interpolate_age_stages(a, b, t);
76        }
77    }
78    stages[0].clone()
79}
80
81// ---------------------------------------------------------------------------
82// Default curves
83// ---------------------------------------------------------------------------
84
85/// Male aging curve — 10 stages from age 10 to 90.
86pub fn default_aging_curve_male() -> AgingCurve {
87    AgingCurve {
88        stages: vec![
89            AgeStage {
90                years: 10.0,
91                fat_pct: 0.15,
92                muscle_pct: 0.35,
93                bone_density: 0.70,
94                skin_elasticity: 0.98,
95            },
96            AgeStage {
97                years: 20.0,
98                fat_pct: 0.18,
99                muscle_pct: 0.45,
100                bone_density: 0.95,
101                skin_elasticity: 0.95,
102            },
103            AgeStage {
104                years: 30.0,
105                fat_pct: 0.22,
106                muscle_pct: 0.44,
107                bone_density: 1.00,
108                skin_elasticity: 0.90,
109            },
110            AgeStage {
111                years: 40.0,
112                fat_pct: 0.26,
113                muscle_pct: 0.42,
114                bone_density: 0.98,
115                skin_elasticity: 0.82,
116            },
117            AgeStage {
118                years: 50.0,
119                fat_pct: 0.30,
120                muscle_pct: 0.38,
121                bone_density: 0.93,
122                skin_elasticity: 0.72,
123            },
124            AgeStage {
125                years: 60.0,
126                fat_pct: 0.32,
127                muscle_pct: 0.33,
128                bone_density: 0.86,
129                skin_elasticity: 0.60,
130            },
131            AgeStage {
132                years: 70.0,
133                fat_pct: 0.33,
134                muscle_pct: 0.27,
135                bone_density: 0.76,
136                skin_elasticity: 0.48,
137            },
138            AgeStage {
139                years: 75.0,
140                fat_pct: 0.32,
141                muscle_pct: 0.23,
142                bone_density: 0.68,
143                skin_elasticity: 0.40,
144            },
145            AgeStage {
146                years: 80.0,
147                fat_pct: 0.30,
148                muscle_pct: 0.19,
149                bone_density: 0.60,
150                skin_elasticity: 0.32,
151            },
152            AgeStage {
153                years: 90.0,
154                fat_pct: 0.27,
155                muscle_pct: 0.14,
156                bone_density: 0.50,
157                skin_elasticity: 0.22,
158            },
159        ],
160    }
161}
162
163/// Female aging curve — 10 stages from age 10 to 90.
164pub fn default_aging_curve_female() -> AgingCurve {
165    AgingCurve {
166        stages: vec![
167            AgeStage {
168                years: 10.0,
169                fat_pct: 0.20,
170                muscle_pct: 0.30,
171                bone_density: 0.68,
172                skin_elasticity: 0.98,
173            },
174            AgeStage {
175                years: 20.0,
176                fat_pct: 0.24,
177                muscle_pct: 0.36,
178                bone_density: 0.92,
179                skin_elasticity: 0.96,
180            },
181            AgeStage {
182                years: 30.0,
183                fat_pct: 0.27,
184                muscle_pct: 0.35,
185                bone_density: 0.97,
186                skin_elasticity: 0.91,
187            },
188            AgeStage {
189                years: 40.0,
190                fat_pct: 0.30,
191                muscle_pct: 0.33,
192                bone_density: 0.94,
193                skin_elasticity: 0.82,
194            },
195            AgeStage {
196                years: 50.0,
197                fat_pct: 0.34,
198                muscle_pct: 0.29,
199                bone_density: 0.84,
200                skin_elasticity: 0.68,
201            },
202            AgeStage {
203                years: 60.0,
204                fat_pct: 0.36,
205                muscle_pct: 0.24,
206                bone_density: 0.74,
207                skin_elasticity: 0.54,
208            },
209            AgeStage {
210                years: 70.0,
211                fat_pct: 0.35,
212                muscle_pct: 0.19,
213                bone_density: 0.64,
214                skin_elasticity: 0.42,
215            },
216            AgeStage {
217                years: 75.0,
218                fat_pct: 0.33,
219                muscle_pct: 0.16,
220                bone_density: 0.57,
221                skin_elasticity: 0.35,
222            },
223            AgeStage {
224                years: 80.0,
225                fat_pct: 0.30,
226                muscle_pct: 0.13,
227                bone_density: 0.49,
228                skin_elasticity: 0.27,
229            },
230            AgeStage {
231                years: 90.0,
232                fat_pct: 0.26,
233                muscle_pct: 0.09,
234                bone_density: 0.38,
235                skin_elasticity: 0.18,
236            },
237        ],
238    }
239}
240
241// ---------------------------------------------------------------------------
242// Delta & param functions
243// ---------------------------------------------------------------------------
244
245/// Morph parameter changes between two age stages.
246pub fn age_progression_deltas(from: &AgeStage, to: &AgeStage) -> Vec<(String, f32)> {
247    vec![
248        ("delta_fat_pct".into(), to.fat_pct - from.fat_pct),
249        ("delta_muscle_pct".into(), to.muscle_pct - from.muscle_pct),
250        (
251            "delta_bone_density".into(),
252            to.bone_density - from.bone_density,
253        ),
254        (
255            "delta_skin_elasticity".into(),
256            to.skin_elasticity - from.skin_elasticity,
257        ),
258    ]
259}
260
261/// Skin aging parameters: wrinkles, sagging, age spots.
262pub fn skin_aging_params(years: f32, sex: u8) -> Vec<(String, f32)> {
263    let age_factor = ((years - 20.0) / 70.0).clamp(0.0, 1.0);
264    let sex_mul = if sex == 1 { 1.05f32 } else { 1.0f32 }; // females age skin slightly faster
265    vec![
266        ("wrinkle_forehead".into(), age_factor * 0.8 * sex_mul),
267        ("wrinkle_eyes".into(), age_factor * 0.9 * sex_mul),
268        ("wrinkle_mouth".into(), age_factor * 0.7 * sex_mul),
269        ("sag_cheeks".into(), age_factor * 0.6),
270        ("sag_jowl".into(), age_factor * 0.5),
271        ("sag_neck".into(), age_factor * 0.55),
272        ("age_spots".into(), (age_factor - 0.3).max(0.0) * 0.6),
273        ("pore_size".into(), age_factor * 0.4),
274    ]
275}
276
277/// Face aging parameters: brow, jaw, nose changes.
278pub fn face_aging_params(years: f32) -> Vec<(String, f32)> {
279    let af = ((years - 20.0) / 70.0).clamp(0.0, 1.0);
280    vec![
281        ("brow_droop".into(), af * 0.5),
282        ("jaw_resorption".into(), af * 0.35),
283        ("nose_tip_droop".into(), af * 0.3),
284        ("ear_growth".into(), af * 0.2),
285        ("lip_thinning".into(), af * 0.4),
286        ("nasolabial_depth".into(), af * 0.6),
287        ("eye_hollow".into(), af * 0.45),
288    ]
289}
290
291/// Body aging parameters: posture, fat redistribution, muscle loss.
292pub fn body_aging_params(years: f32, sex: u8) -> Vec<(String, f32)> {
293    let af = ((years - 20.0) / 70.0).clamp(0.0, 1.0);
294    let belly_mul = if sex == 0 { 1.2f32 } else { 1.0f32 }; // males more belly fat
295    vec![
296        ("posture_kyphosis".into(), af * 0.4),
297        ("belly_fat".into(), af * 0.6 * belly_mul),
298        ("muscle_loss_arms".into(), af * 0.5),
299        ("muscle_loss_legs".into(), af * 0.45),
300        ("height_loss".into(), af * 0.03), // up to ~3% height loss
301        ("hip_fat".into(), af * 0.35),
302        ("skin_looseness_body".into(), af * 0.5),
303    ]
304}
305
306/// Simulate full aging from base_age to target_years.
307pub fn simulate_aging(
308    profile: &AgeProfile,
309    target_years: f32,
310    curve: &AgingCurve,
311) -> Vec<(String, f32)> {
312    let from = compute_age_stage(curve, profile.base_age);
313    let to = compute_age_stage(curve, target_years);
314    let mut params = age_progression_deltas(&from, &to);
315    params.extend(skin_aging_params(target_years, profile.sex));
316    params.extend(face_aging_params(target_years));
317    params.extend(body_aging_params(target_years, profile.sex));
318    params
319}
320
321/// Negate all aging deltas (de-aging / reverse aging).
322pub fn reverse_aging(params: &[(String, f32)]) -> Vec<(String, f32)> {
323    params.iter().map(|(k, v)| (k.clone(), -v)).collect()
324}
325
326// ---------------------------------------------------------------------------
327// Tests
328// ---------------------------------------------------------------------------
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_interpolate_midpoint() {
336        let a = AgeStage {
337            years: 20.0,
338            fat_pct: 0.20,
339            muscle_pct: 0.40,
340            bone_density: 1.0,
341            skin_elasticity: 0.95,
342        };
343        let b = AgeStage {
344            years: 40.0,
345            fat_pct: 0.30,
346            muscle_pct: 0.35,
347            bone_density: 0.90,
348            skin_elasticity: 0.75,
349        };
350        let mid = interpolate_age_stages(&a, &b, 0.5);
351        assert!((mid.fat_pct - 0.25).abs() < 1e-5);
352        assert!((mid.muscle_pct - 0.375).abs() < 1e-5);
353    }
354
355    #[test]
356    fn test_interpolate_clamp_t() {
357        let a = AgeStage {
358            years: 0.0,
359            fat_pct: 0.1,
360            muscle_pct: 0.5,
361            bone_density: 1.0,
362            skin_elasticity: 1.0,
363        };
364        let b = AgeStage {
365            years: 100.0,
366            fat_pct: 0.4,
367            muscle_pct: 0.1,
368            bone_density: 0.4,
369            skin_elasticity: 0.2,
370        };
371        let clamped = interpolate_age_stages(&a, &b, 2.0);
372        assert!((clamped.fat_pct - b.fat_pct).abs() < 1e-5);
373    }
374
375    #[test]
376    fn test_compute_age_stage_clamps_low() {
377        let curve = default_aging_curve_male();
378        let stage = compute_age_stage(&curve, 5.0);
379        assert!((stage.years - 10.0).abs() < 1e-3);
380    }
381
382    #[test]
383    fn test_compute_age_stage_clamps_high() {
384        let curve = default_aging_curve_male();
385        let stage = compute_age_stage(&curve, 100.0);
386        assert!((stage.years - 90.0).abs() < 1e-3);
387    }
388
389    #[test]
390    fn test_bone_density_decreases_with_age() {
391        let curve = default_aging_curve_male();
392        let young = compute_age_stage(&curve, 30.0);
393        let old = compute_age_stage(&curve, 70.0);
394        assert!(old.bone_density < young.bone_density);
395    }
396
397    #[test]
398    fn test_skin_elasticity_decreases_with_age() {
399        let curve = default_aging_curve_female();
400        let young = compute_age_stage(&curve, 20.0);
401        let old = compute_age_stage(&curve, 80.0);
402        assert!(old.skin_elasticity < young.skin_elasticity);
403    }
404
405    #[test]
406    fn test_male_vs_female_differ() {
407        let male = default_aging_curve_male();
408        let female = default_aging_curve_female();
409        let m50 = compute_age_stage(&male, 50.0);
410        let f50 = compute_age_stage(&female, 50.0);
411        // females have higher fat_pct at 50
412        assert!(f50.fat_pct > m50.fat_pct);
413    }
414
415    #[test]
416    fn test_skin_aging_params_non_empty() {
417        let params = skin_aging_params(60.0, 0);
418        assert!(!params.is_empty());
419    }
420
421    #[test]
422    fn test_skin_aging_params_young_all_near_zero() {
423        let params = skin_aging_params(20.0, 0);
424        for (_, v) in &params {
425            assert!(*v >= 0.0);
426        }
427    }
428
429    #[test]
430    fn test_face_aging_params_non_empty() {
431        let params = face_aging_params(50.0);
432        assert!(!params.is_empty());
433        for (_, v) in &params {
434            assert!(*v >= 0.0);
435        }
436    }
437
438    #[test]
439    fn test_body_aging_params_non_empty() {
440        let params = body_aging_params(65.0, 0);
441        assert!(!params.is_empty());
442    }
443
444    #[test]
445    fn test_simulate_aging_non_empty() {
446        let curve = default_aging_curve_male();
447        let profile = AgeProfile {
448            base_age: 25.0,
449            sex: 0,
450            ethnicity: 0,
451        };
452        let params = simulate_aging(&profile, 65.0, &curve);
453        assert!(!params.is_empty());
454    }
455
456    #[test]
457    fn test_reverse_aging_negates() {
458        let params = vec![
459            ("wrinkle_forehead".into(), 0.5f32),
460            ("sag_cheeks".into(), 0.3f32),
461        ];
462        let reversed = reverse_aging(&params);
463        for (orig, rev) in params.iter().zip(reversed.iter()) {
464            assert!((orig.1 + rev.1).abs() < 1e-6);
465        }
466    }
467
468    #[test]
469    fn test_age_progression_deltas_count() {
470        let curve = default_aging_curve_male();
471        let from = compute_age_stage(&curve, 30.0);
472        let to = compute_age_stage(&curve, 70.0);
473        let deltas = age_progression_deltas(&from, &to);
474        assert_eq!(deltas.len(), 4);
475    }
476
477    #[test]
478    fn test_default_curves_have_10_stages() {
479        assert_eq!(default_aging_curve_male().stages.len(), 10);
480        assert_eq!(default_aging_curve_female().stages.len(), 10);
481    }
482
483    #[test]
484    fn test_simulate_aging_backward_gives_negatives() {
485        let curve = default_aging_curve_male();
486        let profile = AgeProfile {
487            base_age: 60.0,
488            sex: 0,
489            ethnicity: 0,
490        };
491        // aging backward (target < base) — delta_fat_pct should be negative
492        let params = simulate_aging(&profile, 30.0, &curve);
493        let fat_delta = params
494            .iter()
495            .find(|(k, _)| k == "delta_fat_pct")
496            .expect("should succeed");
497        assert!(fat_delta.1 < 0.0);
498    }
499}