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