Skip to main content

dna/
lib.rs

1//! Embeddable DNA helix preview (character grid + RGB) for other TUIs.
2//! The full-screen CLI remains in `main.rs`.
3
4use std::f32::consts::TAU;
5use std::time::Duration;
6
7/// Target frame interval for helix mode in `main.rs` (`RenderState::draw` → helix branch).
8const HELIX_FRAME_MS: f32 = 33.0;
9
10pub type HelixCell = Option<(char, u8, u8, u8)>;
11pub type HelixGrid = Vec<Vec<HelixCell>>;
12
13#[allow(dead_code)]
14const DNA: &[u8; 4] = b"ATCG";
15const DNA_CODONS: &[&[u8; 3]] = &[
16    b"ATG", b"GCT", b"GCC", b"GCA", b"GCG", b"TGT", b"TGC", b"GAT", b"GAC", b"GAA", b"GAG", b"TTT",
17    b"TTC", b"GGT", b"GGC", b"GGA", b"GGG", b"CAT", b"CAC", b"ATT", b"ATC", b"ATA", b"AAA", b"AAG",
18    b"TTA", b"TTG", b"CTT", b"CTC", b"CTA", b"CTG", b"AAT", b"AAC", b"CCT", b"CCC", b"CCA", b"CCG",
19    b"CAA", b"CAG", b"CGT", b"CGC", b"CGA", b"CGG", b"AGA", b"AGG", b"TCT", b"TCC", b"TCA", b"TCG",
20    b"AGT", b"AGC", b"ACT", b"ACC", b"ACA", b"ACG", b"GTT", b"GTC", b"GTA", b"GTG", b"TGG", b"TAT",
21    b"TAC",
22];
23
24struct Rng(u64);
25
26impl Rng {
27    fn new(seed: u64) -> Self {
28        Self(seed)
29    }
30
31    fn range(&mut self, max: u32) -> u32 {
32        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
33        ((self.0 >> 32) as u32) % max.max(1)
34    }
35}
36
37fn put_cell(
38    row: &mut [Option<(char, u8, u8, u8)>],
39    width: usize,
40    gx: i32,
41    ch: u8,
42    rgb: (u8, u8, u8),
43) {
44    if gx >= 1 && gx <= width as i32 {
45        let ux = (gx - 1) as usize;
46        if ux < width {
47            row[ux] = Some((ch as char, rgb.0, rgb.1, rgb.2));
48        }
49    }
50}
51
52fn complement_dna(base: u8) -> u8 {
53    match base {
54        b'A' => b'T',
55        b'T' => b'A',
56        b'C' => b'G',
57        b'G' => b'C',
58        _ => base,
59    }
60}
61
62fn coding_sequence_dna(codon_count: usize, rng: &mut Rng) -> Vec<u8> {
63    let mut sequence = Vec::with_capacity(codon_count * 3);
64    sequence.extend_from_slice(b"ATG");
65    for _ in 1..codon_count {
66        sequence.extend_from_slice(DNA_CODONS[rng.range(DNA_CODONS.len() as u32) as usize]);
67    }
68    sequence
69}
70
71/// Classic-palette style colors (approximate ANSI mapping).
72fn palette_front() -> (u8, u8, u8) {
73    (0, 255, 255)
74}
75
76fn palette_back() -> (u8, u8, u8) {
77    (0, 100, 255)
78}
79
80/// Stateful mini helix for embedding in a fixed character rectangle.
81pub struct HelixMini {
82    theta: f32,
83    tick: usize,
84    sequence: Vec<u8>,
85}
86
87impl HelixMini {
88    pub fn new(seed: u64) -> Self {
89        let mut rng = Rng::new(seed ^ 0xC0D0_5EED_B10C_0D0A);
90        Self {
91            theta: 0.0,
92            tick: 0,
93            // Same codon count as `HelixState` in `main.rs` for matching scroll length.
94            sequence: coding_sequence_dna(720, &mut rng),
95        }
96    }
97
98    /// Advance by one nominal helix frame (~33 ms at `speed` 1.0), matching `main.rs` timing.
99    pub fn tick(&mut self, speed: f32) {
100        self.tick_elapsed(speed, Duration::from_secs_f32(HELIX_FRAME_MS / 1000.0));
101    }
102
103    /// Advance using wall-clock time between draws so motion matches the CLI when the host
104    /// UI redraws slowly. Same model as `main.rs`: per frame `theta += 0.16 * speed`,
105    /// `tick += speed.ceil()`, frame sleep `33ms / speed`.
106    pub fn tick_elapsed(&mut self, speed: f32, elapsed: Duration) {
107        if speed <= 0.0 {
108            return;
109        }
110        let elapsed_sec = elapsed.as_secs_f32().min(0.25);
111        let frame_sec = (HELIX_FRAME_MS / 1000.0) / speed.max(0.01);
112        let num_frames = elapsed_sec / frame_sec;
113        self.theta += 0.16 * speed * num_frames;
114        let tick_step = (speed.ceil() * num_frames).floor() as usize;
115        self.tick = self.tick.wrapping_add(tick_step);
116    }
117
118    /// `grid[y][x]` = optional (base char, rgb). Unoccupied cells are `None`.
119    pub fn render_grid(&self, width: usize, height: usize, scale: f32) -> HelixGrid {
120        let mut grid: HelixGrid = vec![vec![None; width]; height];
121        if width == 0 || height == 0 {
122            return grid;
123        }
124
125        let mid_x = (width / 2).max(1) as i32;
126        let rows = height.max(1);
127        // Match `HelixState::draw` in `main.rs` (amplitude in terminal cells).
128        let amp = (((width as f32) * 0.22) * scale).clamp(4.0, 48.0);
129
130        for (yi, y) in (1_i32..=rows as i32).enumerate() {
131            if yi >= height {
132                break;
133            }
134            let t = y as f32 * 0.38 + self.theta;
135            let z = t.sin();
136            let x1 = mid_x + (t.cos() * amp) as i32;
137            let x2 = mid_x + ((t + TAU / 2.0).cos() * amp) as i32;
138            let base = self.sequence[(self.tick / 4 + y as usize) % self.sequence.len()];
139            let pair = complement_dna(base);
140            let (front_x, back_x, front_base, back_base) = if z >= 0.0 {
141                (x1, x2, base, pair)
142            } else {
143                (x2, x1, pair, base)
144            };
145
146            let row = &mut grid[yi];
147
148            let lo = back_x.min(front_x) + 1;
149            let hi = back_x.max(front_x);
150            for x in lo..hi {
151                if x >= 1 && x <= width as i32 {
152                    let ux = (x - 1) as usize;
153                    if ux < width {
154                        row[ux] = Some(('-', 90, 90, 90));
155                    }
156                }
157            }
158
159            put_cell(row, width, back_x, back_base, palette_back());
160            put_cell(row, width, front_x, front_base, palette_front());
161        }
162
163        grid
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn helix_grid_has_reasonable_extent() {
173        let h = HelixMini::new(42);
174        let g = h.render_grid(40, 10, 1.0);
175        assert_eq!(g.len(), 10);
176        assert!(g[0].len() == 40);
177        let filled = g.iter().flatten().filter(|c| c.is_some()).count();
178        assert!(filled > 5, "expected some helix cells");
179    }
180}