Skip to main content

termal_alignment/
rgb.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025-2026 Thomas Junier
3
4use std::fmt::{Display, Formatter};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct Rgb {
8    pub r: u8,
9    pub g: u8,
10    pub b: u8,
11}
12
13impl Rgb {
14    // The simplest case is when R, G, and B are known; if the colour is passed as a hex integer,
15    // then the following constructor can be used instead. This function is marked 'const' so it
16    // can be used to initialize maps at compile time.
17    pub const fn from_u32(h: u32) -> Self {
18        // Ignore bits > 16
19        Self {
20            r: ((h >> 16) & 0xFF) as u8,
21            g: ((h >> 8) & 0xFF) as u8,
22            b: (h & 0xFF) as u8,
23        }
24    }
25
26    // The simplest case is when R, G, and B are known; if the colour is passed as a hex string,
27    // then the following constructor can be used instead.
28    pub fn from_hex(s: &str) -> Result<Self, &'static str> {
29        let s = s.trim();
30
31        // Strip common prefixes
32        let s = s
33            .strip_prefix("0x")
34            .or_else(|| s.strip_prefix("#"))
35            .unwrap_or(s);
36
37        match s.len() {
38            6 => {
39                // RRGGBB
40                let value = u32::from_str_radix(s, 16).map_err(|_| "invalid hex color")?;
41
42                Ok(Self {
43                    r: ((value >> 16) & 0xFF) as u8,
44                    g: ((value >> 8) & 0xFF) as u8,
45                    b: (value & 0xFF) as u8,
46                })
47            }
48            8 => {
49                // Assume AARRGGBB, ignore alpha
50                let value = u32::from_str_radix(s, 16).map_err(|_| "invalid hex color")?;
51
52                Ok(Self {
53                    r: ((value >> 16) & 0xFF) as u8,
54                    g: ((value >> 8) & 0xFF) as u8,
55                    b: (value & 0xFF) as u8,
56                })
57            }
58            _ => Err("hex color must have 6 or 8 digits"),
59        }
60    }
61
62    /// Returns a CSS hex color, e.g. "#00ff7f".
63    pub fn to_hex(self) -> String {
64        format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
65    }
66}
67
68pub const RGB_RED: Rgb = Rgb { r: 255, g: 0, b: 0 };
69pub const RGB_GRAY: Rgb = Rgb {
70    r: 127,
71    g: 127,
72    b: 127,
73};
74pub const RGB_LIGHT_GRAY: Rgb = Rgb {
75    r: 224,
76    g: 224,
77    b: 224,
78};
79pub const RGB_WHITE: Rgb = Rgb {
80    r: 255,
81    g: 255,
82    b: 255,
83};
84
85pub const GAP_COLOR: Rgb = RGB_LIGHT_GRAY;
86
87// In-house colors
88pub const TERMAL_ORANGE: Rgb = Rgb {
89    r: 255,
90    g: 165,
91    b: 0,
92};
93pub const TERMAL_SALMON: Rgb = Rgb {
94    r: 250,
95    g: 128,
96    b: 114,
97};
98
99// ASCII 8-Color palette colors
100pub const ASCII_8_COLOR_GREEN: Rgb = Rgb { r: 0, g: 128, b: 0 };
101pub const ASCII_8_COLOR_MAGENTA: Rgb = Rgb {
102    r: 128,
103    g: 0,
104    b: 128,
105};
106pub const ASCII_8_COLOR_RED: Rgb = Rgb { r: 128, g: 0, b: 0 };
107pub const ASCII_8_COLOR_BLUE: Rgb = Rgb { r: 0, g: 0, b: 128 };
108
109// Lesk aa colors (source?)
110pub const LESK_ORANGE: Rgb = TERMAL_ORANGE;
111pub const LESK_GREEN: Rgb = ASCII_8_COLOR_GREEN;
112pub const LESK_BLUE: Rgb = ASCII_8_COLOR_BLUE;
113pub const LESK_MAGENTA: Rgb = ASCII_8_COLOR_MAGENTA;
114pub const LESK_RED: Rgb = ASCII_8_COLOR_RED;
115
116// ClustalX aa colors (source:
117// https://www.cgl.ucsf.edu/chimera/1.2065/docs/ContributedSoftware/multalignviewer/colprot.par)
118pub const CLUSTALX_RED: Rgb = Rgb {
119    r: 229,
120    g: 51,
121    b: 25,
122};
123pub const CLUSTALX_BLUE: Rgb = Rgb {
124    r: 25,
125    g: 127,
126    b: 229,
127};
128pub const CLUSTALX_GREEN: Rgb = Rgb {
129    r: 25,
130    g: 204,
131    b: 25,
132};
133pub const CLUSTALX_CYAN: Rgb = Rgb {
134    r: 25,
135    g: 178,
136    b: 178,
137};
138pub const CLUSTALX_PINK: Rgb = Rgb {
139    r: 229,
140    g: 127,
141    b: 127,
142};
143pub const CLUSTALX_MAGENTA: Rgb = Rgb {
144    r: 204,
145    g: 76,
146    b: 204,
147};
148pub const CLUSTALX_YELLOW: Rgb = Rgb {
149    r: 204,
150    g: 204,
151    b: 0,
152};
153pub const CLUSTALX_ORANGE: Rgb = Rgb {
154    r: 229,
155    g: 153,
156    b: 76,
157};
158
159// JalView Nucleotide Colors
160
161// Contrary to the Clustal colors, I found these as hex.
162pub const JALVIEW_NUCLEOTIDE_A: Rgb = Rgb::from_u32(0x0064F73F);
163pub const JALVIEW_NUCLEOTIDE_C: Rgb = Rgb::from_u32(0x00FFB340);
164pub const JALVIEW_NUCLEOTIDE_G: Rgb = Rgb::from_u32(0x00EB413C);
165pub const JALVIEW_NUCLEOTIDE_T: Rgb = Rgb::from_u32(0x003C88EE);
166pub const JALVIEW_NUCLEOTIDE_U: Rgb = Rgb::from_u32(0x003C88EE);
167pub const JALVIEW_NUCLEOTIDE_I: Rgb = Rgb::from_u32(0x00ffffff);
168pub const JALVIEW_NUCLEOTIDE_X: Rgb = Rgb::from_u32(0x004f6f6f);
169pub const JALVIEW_NUCLEOTIDE_R: Rgb = Rgb::from_u32(0x00CD5C5C);
170pub const JALVIEW_NUCLEOTIDE_Y: Rgb = Rgb::from_u32(0x00008000);
171pub const JALVIEW_NUCLEOTIDE_W: Rgb = Rgb::from_u32(0x004682B4);
172pub const JALVIEW_NUCLEOTIDE_S: Rgb = Rgb::from_u32(0x00FF8C00);
173pub const JALVIEW_NUCLEOTIDE_M: Rgb = Rgb::from_u32(0x009ACD32);
174pub const JALVIEW_NUCLEOTIDE_K: Rgb = Rgb::from_u32(0x009932CC);
175pub const JALVIEW_NUCLEOTIDE_B: Rgb = Rgb::from_u32(0x008b4513);
176pub const JALVIEW_NUCLEOTIDE_H: Rgb = Rgb::from_u32(0x00808080);
177pub const JALVIEW_NUCLEOTIDE_D: Rgb = Rgb::from_u32(0x00483D8B);
178pub const JALVIEW_NUCLEOTIDE_V: Rgb = Rgb::from_u32(0x00b8860b);
179pub const JALVIEW_NUCLEOTIDE_N: Rgb = Rgb::from_u32(0x002f4f4f);
180
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum ColorMapName {
183    AALesk,
184    AAClustalX,
185    DNAJalView,
186    Monochrome,
187}
188
189#[derive(Clone, Debug)]
190pub struct ResidueColorMap {
191    table: [Rgb; 256],
192}
193
194impl ResidueColorMap {
195    pub fn with_default(gap_color: Rgb, default: Rgb) -> Self {
196        let mut tbl = [default; 256];
197        tbl[b'-' as usize] = gap_color;
198        tbl[b'.' as usize] = gap_color;
199        Self { table: tbl }
200    }
201
202    pub fn by_name(name: ColorMapName) -> Self {
203        match name {
204            ColorMapName::AALesk => Self::aa_lesk(),
205            ColorMapName::AAClustalX => Self::aa_clustalx(),
206            ColorMapName::DNAJalView => Self::dna_jalview(),
207            ColorMapName::Monochrome => Self::monochrome(),
208        }
209    }
210
211    #[inline]
212    pub fn rgb(&self, b: u8) -> Rgb {
213        self.table[b as usize]
214    }
215
216    fn set(&mut self, b: u8, color: Rgb) {
217        self.table[b as usize] = color;
218    }
219
220    pub fn set_pair(&mut self, b: u8, color: Rgb) {
221        self.set(b.to_ascii_lowercase(), color);
222        self.set(b.to_ascii_uppercase(), color);
223    }
224
225    pub fn monochrome() -> Self {
226        Self::with_default(GAP_COLOR, RGB_WHITE)
227    }
228
229    pub fn dna_basic() -> Self {
230        let mut map = Self::with_default(
231            GAP_COLOR,
232            Rgb {
233                r: 160,
234                g: 160,
235                b: 160,
236            },
237        );
238        map.set_pair(b'A', Rgb { r: 0, g: 200, b: 0 });
239        map.set_pair(b'T', Rgb { r: 200, g: 0, b: 0 });
240        map.set_pair(
241            b'G',
242            Rgb {
243                r: 255,
244                g: 165,
245                b: 0,
246            },
247        );
248        map.set_pair(b'C', Rgb { r: 0, g: 0, b: 200 });
249        map
250    }
251
252    pub fn dna_jalview() -> Self {
253        let mut map = Self::with_default(GAP_COLOR, RGB_WHITE);
254        map.set_pair(b'A', JALVIEW_NUCLEOTIDE_A);
255        map.set_pair(b'C', JALVIEW_NUCLEOTIDE_C);
256        map.set_pair(b'G', JALVIEW_NUCLEOTIDE_G);
257        map.set_pair(b'T', JALVIEW_NUCLEOTIDE_T);
258        map.set_pair(b'U', JALVIEW_NUCLEOTIDE_U);
259        map.set_pair(b'I', JALVIEW_NUCLEOTIDE_I);
260        map.set_pair(b'X', JALVIEW_NUCLEOTIDE_X);
261        map.set_pair(b'R', JALVIEW_NUCLEOTIDE_R);
262        map.set_pair(b'Y', JALVIEW_NUCLEOTIDE_Y);
263        map.set_pair(b'W', JALVIEW_NUCLEOTIDE_W);
264        map.set_pair(b'S', JALVIEW_NUCLEOTIDE_S);
265        map.set_pair(b'M', JALVIEW_NUCLEOTIDE_M);
266        map.set_pair(b'K', JALVIEW_NUCLEOTIDE_K);
267        map.set_pair(b'B', JALVIEW_NUCLEOTIDE_B);
268        map.set_pair(b'H', JALVIEW_NUCLEOTIDE_H);
269        map.set_pair(b'D', JALVIEW_NUCLEOTIDE_D);
270        map.set_pair(b'V', JALVIEW_NUCLEOTIDE_V);
271        map.set_pair(b'N', JALVIEW_NUCLEOTIDE_N);
272        map
273    }
274
275    pub fn aa_clustalx() -> Self {
276        let mut map = Self::with_default(GAP_COLOR, RGB_WHITE);
277        map.set_pair(b'G', CLUSTALX_ORANGE);
278        map.set_pair(b'A', CLUSTALX_BLUE);
279        map.set_pair(b'S', CLUSTALX_GREEN);
280        map.set_pair(b'T', CLUSTALX_GREEN);
281        map.set_pair(b'C', CLUSTALX_PINK);
282        map.set_pair(b'V', CLUSTALX_BLUE);
283        map.set_pair(b'I', CLUSTALX_BLUE);
284        map.set_pair(b'L', CLUSTALX_BLUE);
285        map.set_pair(b'P', CLUSTALX_YELLOW);
286        map.set_pair(b'F', CLUSTALX_BLUE);
287        map.set_pair(b'Y', CLUSTALX_CYAN);
288        map.set_pair(b'M', CLUSTALX_BLUE);
289        map.set_pair(b'W', CLUSTALX_BLUE);
290        map.set_pair(b'N', CLUSTALX_GREEN);
291        map.set_pair(b'Q', CLUSTALX_GREEN);
292        map.set_pair(b'H', CLUSTALX_CYAN);
293        map.set_pair(b'D', CLUSTALX_MAGENTA);
294        map.set_pair(b'E', CLUSTALX_MAGENTA);
295        map.set_pair(b'K', CLUSTALX_RED);
296        map.set_pair(b'R', CLUSTALX_RED);
297        map
298    }
299
300    pub fn aa_lesk() -> Self {
301        let mut map = Self::with_default(GAP_COLOR, RGB_WHITE);
302        map.set_pair(b'G', TERMAL_ORANGE);
303        map.set_pair(b'A', TERMAL_ORANGE);
304        map.set_pair(b'S', TERMAL_ORANGE);
305        map.set_pair(b'T', TERMAL_ORANGE);
306        map.set_pair(b'C', ASCII_8_COLOR_GREEN);
307        map.set_pair(b'V', ASCII_8_COLOR_GREEN);
308        map.set_pair(b'I', ASCII_8_COLOR_GREEN);
309        map.set_pair(b'L', ASCII_8_COLOR_GREEN);
310        map.set_pair(b'P', ASCII_8_COLOR_GREEN);
311        map.set_pair(b'F', ASCII_8_COLOR_GREEN);
312        map.set_pair(b'Y', ASCII_8_COLOR_GREEN);
313        map.set_pair(b'M', ASCII_8_COLOR_GREEN);
314        map.set_pair(b'W', ASCII_8_COLOR_GREEN);
315        map.set_pair(b'N', ASCII_8_COLOR_MAGENTA);
316        map.set_pair(b'Q', ASCII_8_COLOR_MAGENTA);
317        map.set_pair(b'H', ASCII_8_COLOR_MAGENTA);
318        map.set_pair(b'D', ASCII_8_COLOR_RED);
319        map.set_pair(b'E', ASCII_8_COLOR_RED);
320        map.set_pair(b'K', ASCII_8_COLOR_BLUE);
321        map.set_pair(b'R', ASCII_8_COLOR_BLUE);
322        map.set_pair(b'X', RGB_WHITE);
323        map
324    }
325}
326
327impl Display for ResidueColorMap {
328    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
329        write!(f, "{{")?;
330        let residues = "ABCDEFGHIKLMNPQRSTUVWXY";
331        for c in residues.as_bytes().iter() {
332            write!(f, "{}: {}, ", *c as char, self.rgb(*c).to_hex())?;
333        }
334        write!(f, "}}")?;
335        Ok(())
336    }
337}
338
339#[cfg(test)]
340mod test {
341
342    use super::{ColorMapName, ResidueColorMap, Rgb, CLUSTALX_MAGENTA, GAP_COLOR, RGB_RED};
343
344    #[test]
345    fn test_default_colormap() {
346        let cmap = ResidueColorMap::with_default(GAP_COLOR, RGB_RED);
347        assert_eq!(RGB_RED, cmap.rgb(b'a'));
348    }
349
350    #[test]
351    fn test_simple_colormap() {
352        let cmap = ResidueColorMap::dna_basic();
353        assert_eq!(
354            Rgb {
355                r: 160,
356                g: 160,
357                b: 160
358            },
359            cmap.rgb(b'%')
360        );
361        assert_eq!(Rgb { r: 0, g: 200, b: 0 }, cmap.rgb(b'A'));
362    }
363
364    #[test]
365    fn test_aa_clustalx() {
366        let cmap = ResidueColorMap::aa_clustalx();
367        assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'D'));
368        assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'd'));
369        assert_eq!(GAP_COLOR, cmap.rgb(b'-'));
370        assert_eq!(GAP_COLOR, cmap.rgb(b'.'));
371    }
372
373    #[test]
374    fn test_from_u32() {
375        let rgb = Rgb::from_u32(0xFF7700);
376        assert_eq!(255, rgb.r);
377        assert_eq!(119, rgb.g);
378        assert_eq!(0, rgb.b);
379    }
380
381    #[test]
382    fn test_colormap_name() {
383        let cmap = ResidueColorMap::by_name(ColorMapName::AAClustalX);
384        assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'D'));
385        assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'd'));
386        assert_eq!(GAP_COLOR, cmap.rgb(b'-'));
387        assert_eq!(GAP_COLOR, cmap.rgb(b'.'));
388    }
389}