UNPKG

lifehash

Version:

TypeScript/JavaScript implementation of LifeHash, a visual hash algorithm

197 lines (196 loc) 8.42 kB
import { sha256 } from '@noble/hashes/sha2.js'; import { BitEnumerator } from './BitEnumerator.js'; import { CellGrid } from './CellGrid.js'; import { ChangeGrid } from './ChangeGrid.js'; import { ColorGrid } from './ColorGrid.js'; import { FracGrid } from './FracGrid.js'; import { Pattern } from './Pattern.js'; import { Size } from './Size.js'; import { Image } from './Image.js'; import { LifeHashVersion } from './types/LifeHashVersion.js'; import { select_gradient } from './color-utils.js'; import { Colors } from './constants.js'; import { clamped, lerp_from } from './math-utils.js'; export class LifeHash { static toDigest(data) { let input; if (typeof data === 'string') { input = new TextEncoder().encode(data); } else if (data instanceof Uint8Array) { input = data; } else { throw new Error('data must be utf-8 string or bytes'); } return sha256(input); } static runGameOfLife(digest, version) { let max_generations; let length; if ([LifeHashVersion.version1, LifeHashVersion.version2].includes(version)) { length = 16; max_generations = 150; } else if ([ LifeHashVersion.detailed, LifeHashVersion.fiducial, LifeHashVersion.grayscale_fiducial, ].includes(version)) { length = 32; max_generations = 300; } else { throw new Error('Invalid version.'); } const size = new Size(length, length); let current_cell_grid = new CellGrid(size); let next_cell_grid = new CellGrid(size); let current_change_grid = new ChangeGrid(size); let next_change_grid = new ChangeGrid(size); const history_set = new Set(); const history = []; switch (version) { case LifeHashVersion.version1: next_cell_grid.set_data(digest); break; case LifeHashVersion.version2: next_cell_grid.set_data(sha256(digest)); break; case LifeHashVersion.detailed: case LifeHashVersion.fiducial: case LifeHashVersion.grayscale_fiducial: { const digest1 = version === LifeHashVersion.grayscale_fiducial ? sha256(digest) : digest; const digest2 = sha256(digest1); const digest3 = sha256(digest2); const digest4 = sha256(digest3); const digest_final = new Uint8Array(128); digest_final.set(digest1, 0); digest_final.set(digest2, 32); digest_final.set(digest3, 64); digest_final.set(digest4, 96); next_cell_grid.set_data(digest_final); break; } } next_change_grid.set_all(true); while (history.length < max_generations) { [current_cell_grid, next_cell_grid] = [next_cell_grid, current_cell_grid]; [current_change_grid, next_change_grid] = [ next_change_grid, current_change_grid, ]; const data = current_cell_grid.get_data(); const hashDigest = sha256(data); if (history_set.has(hashDigest.toString())) { break; } history_set.add(hashDigest.toString()); history.push(data); current_cell_grid.next_generation(current_change_grid, next_cell_grid, next_change_grid); } return { history, size }; } static selectGradientAndPattern(digest, version) { const entropy = new BitEnumerator(digest); if (version === LifeHashVersion.detailed) { entropy.next(); } else if (version === LifeHashVersion.version2) { entropy.next(); entropy.next(); } const gradient = select_gradient(entropy, version); const pattern = Pattern.select_pattern(entropy, version); return { gradient, pattern }; } static buildFracGrid(history, upToGeneration, size, version) { const cell_grid = new CellGrid(size); const frac_grid = new FracGrid(size); for (let i = 0; i < upToGeneration; i += 1) { cell_grid.set_data(history[i]); const frac = clamped(lerp_from(0, history.length, i + 1)); frac_grid.overlay(cell_grid, frac); } if (version !== LifeHashVersion.version1) { const values = []; for (const point of frac_grid.get_points()) { values.push(frac_grid.get_value(point)); } const min_value = Math.min(...values); const max_value = Math.max(...values); if (max_value > min_value) { for (const point of frac_grid.get_points()) { const current_value = frac_grid.get_value(point); frac_grid.set_value(lerp_from(min_value, max_value, current_value), point); } } } return frac_grid; } static renderColorGrid(frac_grid, gradient, pattern, module_size, has_alpha) { const color_grid = new ColorGrid(frac_grid, gradient, pattern); const colors = color_grid.colors(); return Image.make_image(color_grid.size.width, color_grid.size.height, colors, module_size, has_alpha); } static makeFrom(data, version = LifeHashVersion.version2, module_size = 1, has_alpha = false) { return this.makeFromDigest(this.toDigest(data), version, module_size, has_alpha); } static makeFromDigest(digest, version = LifeHashVersion.version2, module_size = 1, has_alpha = false) { if (digest.length !== 32) { throw new Error('Digest must be 32 bytes.'); } const { history, size } = this.runGameOfLife(digest, version); const { gradient, pattern } = this.selectGradientAndPattern(digest, version); const frac_grid = this.buildFracGrid(history, history.length, size, version); return this.renderColorGrid(frac_grid, gradient, pattern, module_size, has_alpha); } static makeAnimationFrames(data, version = LifeHashVersion.version2, module_size = 1, has_alpha = false, frame_count = 60) { const digest = this.toDigest(data); if (digest.length !== 32) { throw new Error('Digest must be 32 bytes.'); } const { history, size } = this.runGameOfLife(digest, version); const { gradient, pattern } = this.selectGradientAndPattern(digest, version); const totalGenerations = history.length; const frame_indices = []; if (frame_count >= totalGenerations) { for (let i = 1; i <= totalGenerations; i++) { frame_indices.push(i); } } else { for (let f = 0; f < frame_count; f++) { const gen = Math.round(((f + 1) / frame_count) * totalGenerations); if (frame_indices.length === 0 || frame_indices[frame_indices.length - 1] !== gen) { frame_indices.push(gen); } } } const bwGradient = (val) => val > 0 ? Colors.white : Colors.black; const frames = []; const cell_grid = new CellGrid(size); const cellFracGrid = new FracGrid(size); for (const gen of frame_indices) { const frac_grid = this.buildFracGrid(history, gen, size, version); const image = this.renderColorGrid(frac_grid, gradient, pattern, module_size, has_alpha); cell_grid.set_data(history[gen - 1]); const cellStateImage = Image.make_image(size.width, size.height, cell_grid.colors(), module_size, has_alpha); for (const point of cellFracGrid.get_points()) { cellFracGrid.set_value(cell_grid.get_value(point) ? 1 : 0, point); } const cellStateMirroredImage = this.renderColorGrid(cellFracGrid, bwGradient, pattern, module_size, has_alpha); frames.push({ image, cellStateImage, cellStateMirroredImage, generation: gen, totalGenerations, }); } return frames; } }