Skip to main content

oxihuman_viewer/
pixel_sort_effect.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Pixel sort effect — pixel sort glitch effect parameters.
5
6/// Sort direction.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum SortDirection {
10    #[default]
11    Horizontal,
12    Vertical,
13    Diagonal,
14}
15
16/// Sort key channel.
17#[allow(dead_code)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum SortKey {
20    #[default]
21    Luminance,
22    Hue,
23    Saturation,
24    Red,
25    Green,
26    Blue,
27}
28
29/// Config.
30#[allow(dead_code)]
31#[derive(Debug, Clone, PartialEq)]
32pub struct PixelSortEffectConfig {
33    pub direction: SortDirection,
34    pub sort_key: SortKey,
35    /// Luminance threshold below which pixels are sorted 0..=1.
36    pub threshold_low: f32,
37    /// Luminance threshold above which pixels are sorted 0..=1.
38    pub threshold_high: f32,
39    /// Intensity of the effect 0..=1.
40    pub intensity: f32,
41    /// Seed for row/column selection.
42    pub seed: u64,
43    pub enabled: bool,
44}
45
46impl Default for PixelSortEffectConfig {
47    fn default() -> Self {
48        Self {
49            direction: SortDirection::Horizontal,
50            sort_key: SortKey::Luminance,
51            threshold_low: 0.2,
52            threshold_high: 0.8,
53            intensity: 0.7,
54            seed: 12345,
55            enabled: true,
56        }
57    }
58}
59
60#[allow(dead_code)]
61pub fn new_pixel_sort_effect_config() -> PixelSortEffectConfig {
62    PixelSortEffectConfig::default()
63}
64
65#[allow(dead_code)]
66pub fn ps_set_threshold_low(cfg: &mut PixelSortEffectConfig, v: f32) {
67    cfg.threshold_low = v.clamp(0.0, 1.0);
68}
69
70#[allow(dead_code)]
71pub fn ps_set_threshold_high(cfg: &mut PixelSortEffectConfig, v: f32) {
72    cfg.threshold_high = v.clamp(0.0, 1.0);
73}
74
75#[allow(dead_code)]
76pub fn ps_set_intensity(cfg: &mut PixelSortEffectConfig, v: f32) {
77    cfg.intensity = v.clamp(0.0, 1.0);
78}
79
80/// Effective sort range (high - low).
81#[allow(dead_code)]
82pub fn ps_sort_range(cfg: &PixelSortEffectConfig) -> f32 {
83    (cfg.threshold_high - cfg.threshold_low).max(0.0)
84}
85
86/// Returns true if a pixel with given luminance is in the sort range.
87#[allow(dead_code)]
88pub fn ps_should_sort(luminance: f32, cfg: &PixelSortEffectConfig) -> bool {
89    luminance >= cfg.threshold_low && luminance <= cfg.threshold_high
90}
91
92/// Sort a row of luminance values in-place (ascending).  Simulates pixel sorting on that row.
93#[allow(dead_code)]
94pub fn ps_sort_row(row: &mut [f32], cfg: &PixelSortEffectConfig) {
95    if !cfg.enabled {
96        return;
97    }
98    // Find sortable segments and sort them
99    let n = row.len();
100    let mut i = 0;
101    while i < n {
102        if ps_should_sort(row[i], cfg) {
103            let start = i;
104            while i < n && ps_should_sort(row[i], cfg) {
105                i += 1;
106            }
107            row[start..i].sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
108        } else {
109            i += 1;
110        }
111    }
112}
113
114#[allow(dead_code)]
115pub fn ps_to_json(cfg: &PixelSortEffectConfig) -> String {
116    format!(
117        "{{\"threshold_low\":{:.3},\"threshold_high\":{:.3},\"intensity\":{:.3},\"enabled\":{}}}",
118        cfg.threshold_low, cfg.threshold_high, cfg.intensity, cfg.enabled
119    )
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn should_sort_in_range() {
128        let cfg = new_pixel_sort_effect_config();
129        assert!(ps_should_sort(0.5, &cfg));
130    }
131
132    #[test]
133    fn should_not_sort_below() {
134        let cfg = new_pixel_sort_effect_config();
135        assert!(!ps_should_sort(0.1, &cfg));
136    }
137
138    #[test]
139    fn should_not_sort_above() {
140        let cfg = new_pixel_sort_effect_config();
141        assert!(!ps_should_sort(0.9, &cfg));
142    }
143
144    #[test]
145    fn sort_range_positive() {
146        let cfg = new_pixel_sort_effect_config();
147        assert!(ps_sort_range(&cfg) > 0.0);
148    }
149
150    #[test]
151    fn sort_row_ascending() {
152        let cfg = new_pixel_sort_effect_config();
153        let mut row = vec![0.7f32, 0.3, 0.5, 0.6, 0.4];
154        ps_sort_row(&mut row, &cfg);
155        for i in 0..row.len() - 1 {
156            assert!(row[i] <= row[i + 1] || !ps_should_sort(row[i], &cfg));
157        }
158    }
159
160    #[test]
161    fn disabled_does_not_sort() {
162        let mut cfg = new_pixel_sort_effect_config();
163        cfg.enabled = false;
164        let mut row = vec![0.7f32, 0.3, 0.5];
165        let orig = row.clone();
166        ps_sort_row(&mut row, &cfg);
167        assert_eq!(row, orig);
168    }
169
170    #[test]
171    fn threshold_low_clamps() {
172        let mut cfg = new_pixel_sort_effect_config();
173        ps_set_threshold_low(&mut cfg, -1.0);
174        assert!(cfg.threshold_low < 1e-6);
175    }
176
177    #[test]
178    fn threshold_high_clamps() {
179        let mut cfg = new_pixel_sort_effect_config();
180        ps_set_threshold_high(&mut cfg, 5.0);
181        assert!((cfg.threshold_high - 1.0).abs() < 1e-6);
182    }
183
184    #[test]
185    fn intensity_clamps() {
186        let mut cfg = new_pixel_sort_effect_config();
187        ps_set_intensity(&mut cfg, 2.0);
188        assert!((cfg.intensity - 1.0).abs() < 1e-6);
189    }
190
191    #[test]
192    fn json_has_keys() {
193        let j = ps_to_json(&new_pixel_sort_effect_config());
194        assert!(j.contains("threshold_low") && j.contains("enabled"));
195    }
196}