1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[repr(u32)]
23pub enum ColorBlindMode {
24 Normal,
26 Protanopia,
28 Deuteranopia,
30 Tritanopia,
32 Protanomaly,
34 Deuteranomaly,
36}
37
38impl ColorBlindMode {
39 pub fn matrix(&self) -> [f32; 9] {
44 match self {
45 ColorBlindMode::Normal => [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
47 ColorBlindMode::Protanopia => [
50 0.567, 0.433, 0.000, 0.558, 0.442, 0.000, 0.000, 0.242, 0.758, ],
54 ColorBlindMode::Deuteranopia => [
56 0.625, 0.375, 0.000, 0.700, 0.300, 0.000, 0.000, 0.300, 0.700, ],
60 ColorBlindMode::Tritanopia => [
62 0.950, 0.050, 0.000, 0.000, 0.433, 0.567, 0.000, 0.475, 0.525, ],
66 ColorBlindMode::Protanomaly => [
68 0.817, 0.183, 0.000, 0.333, 0.667, 0.000, 0.000, 0.125, 0.875,
69 ],
70 ColorBlindMode::Deuteranomaly => [
72 0.800, 0.200, 0.000, 0.258, 0.742, 0.000, 0.000, 0.142, 0.858,
73 ],
74 }
75 }
76
77 pub fn display_name(&self) -> &'static str {
79 match self {
80 ColorBlindMode::Normal => "Normal Vision",
81 ColorBlindMode::Protanopia => "Protanopia (no red)",
82 ColorBlindMode::Deuteranopia => "Deuteranopia (no green)",
83 ColorBlindMode::Tritanopia => "Tritanopia (no blue)",
84 ColorBlindMode::Protanomaly => "Protanomaly (reduced red)",
85 ColorBlindMode::Deuteranomaly => "Deuteranomaly (reduced green)",
86 }
87 }
88
89 pub fn is_identity(&self) -> bool {
91 matches!(self, ColorBlindMode::Normal)
92 }
93}
94
95pub fn shader_source() -> &'static str {
100 r#"
101struct ColorBlindUniforms {
102 matrix_0: vec3<f32>,
103 matrix_1: vec3<f32>,
104 matrix_2: vec3<f32>,
105 mode: u32,
106 intensity: f32, // 0.0 = no effect, 1.0 = full simulation
107 color_space: u32,
108 _pad0: f32,
109 _pad1: f32,
110};
111
112@group(0) @binding(0) var t_screen: texture_2d<f32>;
113@group(0) @binding(1) var s_screen: sampler;
114@group(0) @binding(2) var<uniform> cb: ColorBlindUniforms;
115
116struct VertexOutput {
117 @builtin(position) pos: vec4<f32>,
118 @location(0) uv: vec2<f32>,
119};
120
121@vertex
122fn fs_main_vs(@builtin(vertex_index) vid: u32) -> VertexOutput {
123 // Full-screen triangle
124 let pos = vec4<f32>(
125 select(vec2<f32>(-1.0, -1.0), vec2<f32>(3.0, -1.0), vid == 1u),
126 0.0,
127 1.0
128 );
129 let uv = vec2<f32>(
130 select(0.0, 2.0, vid == 1u),
131 select(0.0, 2.0, vid > 0u),
132 );
133 return VertexOutput(pos, uv);
134}
135
136@fragment
137fn fs_color_blind(in: VertexOutput) -> @location(0) vec4<f32> {
138 // the 3x3 matrix in the uniform is the simulation matrix
139 // see ColorBlindMode::matrix() for the algorithm
140 let screen_uv = vec2<f32>(in.uv.x, 1.0 - in.uv.y);
141 let color = textureSample(t_screen, s_screen, screen_uv);
142 let rgb = color.rgb;
143
144 let mat = mat3x3<f32>(cb.matrix_0, cb.matrix_1, cb.matrix_2);
145 let simulated = mat * rgb;
146 let result = mix(rgb, simulated, cb.intensity);
147
148 return vec4<f32>(result, color.a);
149}
150"#
151}
152
153#[repr(C)]
155#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
156pub struct ColorBlindUniforms {
157 pub matrix_0: [f32; 3],
159 _pad_m0: f32, pub matrix_1: [f32; 3],
162 _pad_m1: f32,
163 pub matrix_2: [f32; 3],
165 _pad_m2: f32,
166 pub mode: u32,
168 pub intensity: f32,
170 color_space: u32,
171 _pad0: f32,
172 _pad1: f32,
173}
174
175impl ColorBlindUniforms {
176 pub fn new(mode: ColorBlindMode, intensity: f32) -> Self {
178 let m = mode.matrix();
179 Self {
180 matrix_0: [m[0], m[1], m[2]],
181 _pad_m0: 0.0,
182 matrix_1: [m[3], m[4], m[5]],
183 _pad_m1: 0.0,
184 matrix_2: [m[6], m[7], m[8]],
185 _pad_m2: 0.0,
186 mode: mode as u32,
187 intensity: intensity.clamp(0.0, 1.0),
188 color_space: 0,
189 _pad0: 0.0,
190 _pad1: 0.0,
191 }
192 }
193}
194
195pub const ALL_MODES: &[ColorBlindMode] = &[
197 ColorBlindMode::Normal,
198 ColorBlindMode::Protanopia,
199 ColorBlindMode::Protanomaly,
200 ColorBlindMode::Deuteranopia,
201 ColorBlindMode::Deuteranomaly,
202 ColorBlindMode::Tritanopia,
203];
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_normal_matrix_is_identity() {
211 let m = ColorBlindMode::Normal.matrix();
212 assert_eq!(m[0], 1.0);
213 assert_eq!(m[4], 1.0);
214 assert_eq!(m[8], 1.0);
215 assert_eq!(m[1], 0.0);
216 }
217
218 #[test]
219 fn test_protanopia_blue_input_isolated() {
220 let m = ColorBlindMode::Protanopia.matrix();
221 assert_eq!(m[2], 0.0);
223 assert_eq!(m[5], 0.0);
224 }
225
226 #[test]
227 fn test_uniforms_creation() {
228 let u = ColorBlindUniforms::new(ColorBlindMode::Deuteranopia, 0.8);
229 assert_eq!(u.intensity, 0.8);
230 assert_eq!(u.mode, 2); }
232
233 #[test]
234 fn test_intensity_clamping() {
235 let u = ColorBlindUniforms::new(ColorBlindMode::Normal, 999.0);
236 assert_eq!(u.intensity, 1.0);
237 let u2 = ColorBlindUniforms::new(ColorBlindMode::Normal, -1.0);
238 assert_eq!(u2.intensity, 0.0);
239 }
240
241 #[test]
242 fn test_all_modes_have_names() {
243 for mode in ALL_MODES {
244 let name = mode.display_name();
245 assert!(!name.is_empty());
246 }
247 }
248}