1use std::f32::consts::TAU;
5use std::time::Duration;
6
7const 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
71fn palette_front() -> (u8, u8, u8) {
73 (0, 255, 255)
74}
75
76fn palette_back() -> (u8, u8, u8) {
77 (0, 100, 255)
78}
79
80pub 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 sequence: coding_sequence_dna(720, &mut rng),
95 }
96 }
97
98 pub fn tick(&mut self, speed: f32) {
100 self.tick_elapsed(speed, Duration::from_secs_f32(HELIX_FRAME_MS / 1000.0));
101 }
102
103 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 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 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}