Skip to main content

bc_lifehash/
lib.rs

1#![doc(html_root_url = "https://docs.rs/bc-lifehash/0.1.0")]
2#![warn(rust_2018_idioms)]
3
4//! # Introduction
5//!
6//! `bc-lifehash` is a method of hash visualization based on Conway's Game of
7//! Life that creates beautiful icons that are deterministic, yet distinct and
8//! unique given the input data.
9//!
10//! The basic concept is to take a SHA-256 hash of the input data (which can be
11//! any data including another hash) and then use the 256-bit digest as a 16x16
12//! pixel "seed" for running the cellular automata known as Conway's Game of
13//! Life. After the pattern becomes stable (or begins repeating) the resulting
14//! history is used to compile a grayscale image of all the states from the
15//! first to last generation. Using Game of Life provides visual structure to
16//! the resulting image, even though it was seeded with entropy. Some bits of
17//! the initial hash are then used to deterministically apply symmetry and color
18//! to the icon to add beauty and quick recognizability.
19//!
20//! This is the first-party Rust implementation. It produces byte-identical
21//! output to the [C++ reference implementation](https://github.com/BlockchainCommons/bc-lifehash).
22//!
23//! # Getting Started
24//!
25//! ```toml
26//! [dependencies]
27//! bc-lifehash = "0.1.0"
28//! ```
29//!
30//! # Versions
31//!
32//! Five LifeHash versions are supported via the [`Version`] enum:
33//!
34//! - **Version1** / **Version2** — 16x16 grid, up to 150 generations.
35//! - **Detailed** — 32x32 grid, up to 300 generations, richer color gradients.
36//! - **Fiducial** — 32x32, designed for use as fiducial markers.
37//! - **GrayscaleFiducial** — Same as Fiducial but rendered in grayscale.
38//!
39//! # Example
40//!
41//! ```rust
42//! let image = bc_lifehash::make_from_utf8(
43//!     "Hello",
44//!     bc_lifehash::Version::Version2,
45//!     1,
46//!     false,
47//! );
48//! assert_eq!(image.width, 32);
49//! assert_eq!(image.height, 32);
50//! // image.colors contains RGB bytes (width * height * 3)
51//! ```
52
53mod bit_enumerator;
54mod cell_grid;
55mod change_grid;
56mod color;
57mod color_func;
58mod color_grid;
59mod frac_grid;
60mod gradients;
61mod grid;
62mod hsb_color;
63mod patterns;
64
65use std::collections::BTreeSet;
66
67use bit_enumerator::BitEnumerator;
68use cell_grid::CellGrid;
69use change_grid::ChangeGrid;
70use color::{clamped, lerp_from};
71use color_grid::ColorGrid;
72use frac_grid::FracGrid;
73use gradients::select_gradient;
74use patterns::select_pattern;
75use sha2::{Digest, Sha256};
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum Version {
79    Version1,
80    Version2,
81    Detailed,
82    Fiducial,
83    GrayscaleFiducial,
84}
85
86pub struct Image {
87    pub width: usize,
88    pub height: usize,
89    pub colors: Vec<u8>,
90}
91
92fn sha256(data: &[u8]) -> Vec<u8> {
93    let mut hasher = Sha256::new();
94    hasher.update(data);
95    hasher.finalize().to_vec()
96}
97
98fn make_image(
99    width: usize,
100    height: usize,
101    float_colors: &[f64],
102    module_size: usize,
103    has_alpha: bool,
104) -> Image {
105    assert!(module_size > 0, "Invalid module size");
106
107    let scaled_width = width * module_size;
108    let scaled_height = height * module_size;
109    let result_components = if has_alpha { 4 } else { 3 };
110    let scaled_capacity = scaled_width * scaled_height * result_components;
111
112    let mut result_colors = vec![0u8; scaled_capacity];
113
114    // Match C++ loop order: outer loop uses scaled_width, inner uses
115    // scaled_height (they're swapped relative to the variable names, but
116    // since the image is always square this doesn't matter in practice)
117    for target_y in 0..scaled_width {
118        for target_x in 0..scaled_height {
119            let source_x = target_x / module_size;
120            let source_y = target_y / module_size;
121            let source_offset = (source_y * width + source_x) * 3;
122
123            let target_offset =
124                (target_y * scaled_width + target_x) * result_components;
125
126            result_colors[target_offset] =
127                (clamped(float_colors[source_offset]) * 255.0) as u8;
128            result_colors[target_offset + 1] =
129                (clamped(float_colors[source_offset + 1]) * 255.0) as u8;
130            result_colors[target_offset + 2] =
131                (clamped(float_colors[source_offset + 2]) * 255.0) as u8;
132            if has_alpha {
133                result_colors[target_offset + 3] = 255;
134            }
135        }
136    }
137
138    Image {
139        width: scaled_width,
140        height: scaled_height,
141        colors: result_colors,
142    }
143}
144
145pub fn make_from_utf8(
146    s: &str,
147    version: Version,
148    module_size: usize,
149    has_alpha: bool,
150) -> Image {
151    make_from_data(s.as_bytes(), version, module_size, has_alpha)
152}
153
154pub fn make_from_data(
155    data: &[u8],
156    version: Version,
157    module_size: usize,
158    has_alpha: bool,
159) -> Image {
160    let digest = sha256(data);
161    make_from_digest(&digest, version, module_size, has_alpha)
162}
163
164pub fn make_from_digest(
165    digest: &[u8],
166    version: Version,
167    module_size: usize,
168    has_alpha: bool,
169) -> Image {
170    assert_eq!(digest.len(), 32, "Digest must be 32 bytes");
171
172    let (length, max_generations): (usize, usize) = match version {
173        Version::Version1 | Version::Version2 => (16, 150),
174        Version::Detailed | Version::Fiducial | Version::GrayscaleFiducial => {
175            (32, 300)
176        }
177    };
178
179    let mut current_cell_grid = CellGrid::new(length, length);
180    let mut next_cell_grid = CellGrid::new(length, length);
181    let mut current_change_grid = ChangeGrid::new(length, length);
182    let mut next_change_grid = ChangeGrid::new(length, length);
183
184    match version {
185        Version::Version1 => {
186            next_cell_grid.set_data(digest);
187        }
188        Version::Version2 => {
189            let hashed = sha256(digest);
190            next_cell_grid.set_data(&hashed);
191        }
192        Version::Detailed | Version::Fiducial | Version::GrayscaleFiducial => {
193            let mut digest1 = digest.to_vec();
194            if version == Version::GrayscaleFiducial {
195                digest1 = sha256(&digest1);
196            }
197            let digest2 = sha256(&digest1);
198            let digest3 = sha256(&digest2);
199            let digest4 = sha256(&digest3);
200            let mut digest_final = digest1;
201            digest_final.extend_from_slice(&digest2);
202            digest_final.extend_from_slice(&digest3);
203            digest_final.extend_from_slice(&digest4);
204            next_cell_grid.set_data(&digest_final);
205        }
206    }
207
208    next_change_grid.grid.set_all(true);
209
210    let mut history_set: BTreeSet<Vec<u8>> = BTreeSet::new();
211    let mut history: Vec<Vec<u8>> = Vec::new();
212
213    while history.len() < max_generations {
214        std::mem::swap(&mut current_cell_grid, &mut next_cell_grid);
215        std::mem::swap(&mut current_change_grid, &mut next_change_grid);
216
217        let data = current_cell_grid.data();
218        let hash = sha256(&data);
219        if history_set.contains(&hash) {
220            break;
221        }
222        history_set.insert(hash);
223        history.push(data);
224
225        current_cell_grid.next_generation(
226            &current_change_grid,
227            &mut next_cell_grid,
228            &mut next_change_grid,
229        );
230    }
231
232    let mut frac_grid = FracGrid::new(length, length);
233    for (i, h) in history.iter().enumerate() {
234        current_cell_grid.set_data(h);
235        let frac =
236            clamped(lerp_from(0.0, history.len() as f64, (i + 1) as f64));
237        frac_grid.overlay(&current_cell_grid, frac);
238    }
239
240    // Normalize the frac_grid to [0, 1] (except version1)
241    if version != Version::Version1 {
242        let mut min_value = f64::INFINITY;
243        let mut max_value = f64::NEG_INFINITY;
244        frac_grid.grid.for_all(|x, y| {
245            let value = frac_grid.grid.get_value(x, y);
246            if value < min_value {
247                min_value = value;
248            }
249            if value > max_value {
250                max_value = value;
251            }
252        });
253
254        let width = frac_grid.grid.width;
255        let height = frac_grid.grid.height;
256        for y in 0..height {
257            for x in 0..width {
258                let value = frac_grid.grid.get_value(x, y);
259                let normalized = lerp_from(min_value, max_value, value);
260                frac_grid.grid.set_value(normalized, x, y);
261            }
262        }
263    }
264
265    let mut entropy = BitEnumerator::new(digest.to_vec());
266
267    match version {
268        Version::Detailed => {
269            entropy.next();
270        }
271        Version::Version2 => {
272            entropy.next_uint2();
273        }
274        _ => {}
275    }
276
277    let gradient = select_gradient(&mut entropy, version);
278    let pattern = select_pattern(&mut entropy, version);
279    let color_grid = ColorGrid::new(&frac_grid, &gradient, pattern);
280
281    make_image(
282        color_grid.grid.width,
283        color_grid.grid.height,
284        &color_grid.colors(),
285        module_size,
286        has_alpha,
287    )
288}