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 _pad0: f32,
108 _pad1: f32,
109};
110
111@group(0) @binding(0) var t_screen: texture_2d<f32>;
112@group(0) @binding(1) var s_screen: sampler;
113@group(0) @binding(2) var<uniform> cb: ColorBlindUniforms;
114
115struct VertexOutput {
116 @builtin(position) pos: vec4<f32>,
117 @location(0) uv: vec2<f32>,
118};
119
120@vertex
121fn fs_main_vs(@builtin(vertex_index) vid: u32) -> VertexOutput {
122 // Full-screen triangle
123 let pos = vec4<f32>(
124 select(vec2<f32>(-1.0, -1.0), vec2<f32>(3.0, -1.0), vid == 1u),
125 0.0,
126 1.0
127 );
128 let uv = vec2<f32>(
129 select(0.0, 2.0, vid == 1u),
130 select(0.0, 2.0, vid > 0u),
131 );
132 return VertexOutput(pos, uv);
133}
134
135@fragment
136fn fs_color_blind(in: VertexOutput) -> @location(0) vec4<f32> {
137 // the 3x3 matrix in the uniform is the simulation matrix
138 // see ColorBlindMode::matrix() for the algorithm
139 let screen_uv = vec2<f32>(in.uv.x, 1.0 - in.uv.y);
140 let color = textureSample(t_screen, s_screen, screen_uv);
141 let rgb = color.rgb;
142
143 let mat = mat3x3<f32>(cb.matrix_0, cb.matrix_1, cb.matrix_2);
144 let simulated = mat * rgb;
145 let result = mix(rgb, simulated, cb.intensity);
146
147 return vec4<f32>(result, color.a);
148}
149"#
150}
151
152#[repr(C)]
154#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
155pub struct ColorBlindUniforms {
156 pub matrix_0: [f32; 3],
158 _pad_m0: f32, pub matrix_1: [f32; 3],
161 _pad_m1: f32,
162 pub matrix_2: [f32; 3],
164 _pad_m2: f32,
165 pub mode: u32,
167 pub intensity: f32,
169 _pad0: f32,
170 _pad1: f32,
171}
172
173impl ColorBlindUniforms {
174 pub fn new(mode: ColorBlindMode, intensity: f32) -> Self {
176 let m = mode.matrix();
177 Self {
178 matrix_0: [m[0], m[3], m[6]],
179 _pad_m0: 0.0,
180 matrix_1: [m[1], m[4], m[7]],
181 _pad_m1: 0.0,
182 matrix_2: [m[2], m[5], m[8]],
183 _pad_m2: 0.0,
184 mode: mode as u32,
185 intensity: intensity.clamp(0.0, 1.0),
186 _pad0: 0.0,
187 _pad1: 0.0,
188 }
189 }
190}
191
192pub const ALL_MODES: &[ColorBlindMode] = &[
194 ColorBlindMode::Normal,
195 ColorBlindMode::Protanopia,
196 ColorBlindMode::Protanomaly,
197 ColorBlindMode::Deuteranopia,
198 ColorBlindMode::Deuteranomaly,
199 ColorBlindMode::Tritanopia,
200];
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_normal_matrix_is_identity() {
208 let m = ColorBlindMode::Normal.matrix();
209 assert_eq!(m[0], 1.0);
210 assert_eq!(m[4], 1.0);
211 assert_eq!(m[8], 1.0);
212 assert_eq!(m[1], 0.0);
213 }
214
215 #[test]
216 fn test_protanopia_blue_input_isolated() {
217 let m = ColorBlindMode::Protanopia.matrix();
218 assert_eq!(m[2], 0.0);
220 assert_eq!(m[5], 0.0);
221 }
222
223 #[test]
224 fn test_uniforms_creation() {
225 let u = ColorBlindUniforms::new(ColorBlindMode::Deuteranopia, 0.8);
226 assert_eq!(u.intensity, 0.8);
227 assert_eq!(u.mode, 2); }
229
230 #[test]
231 fn test_intensity_clamping() {
232 let u = ColorBlindUniforms::new(ColorBlindMode::Normal, 999.0);
233 assert_eq!(u.intensity, 1.0);
234 let u2 = ColorBlindUniforms::new(ColorBlindMode::Normal, -1.0);
235 assert_eq!(u2.intensity, 0.0);
236 }
237
238 #[test]
239 fn test_all_modes_have_names() {
240 for mode in ALL_MODES {
241 let name = mode.display_name();
242 assert!(!name.is_empty());
243 }
244 }
245}