lifehash
Version:
TypeScript/JavaScript implementation of LifeHash, a visual hash algorithm
197 lines (196 loc) • 8.42 kB
JavaScript
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;
}
}