lifehash
Version:
TypeScript/JavaScript implementation of LifeHash, a visual hash algorithm
361 lines (360 loc) • 12.4 kB
JavaScript
import { Colors, grayscale, spectrum, spectrum_cmyk_safe, } from './constants.js';
import { HSBColor } from './HSBColor.js';
import { modulo, lerp } from './math-utils.js';
import { LifeHashVersion } from './types/LifeHashVersion.js';
import { reverse } from './utils.js';
const select_grayscale = (entropy) => {
if (entropy.next()) {
return grayscale;
}
else {
return reverse(grayscale);
}
};
const make_hue = (t) => {
return new HSBColor(t).to_color();
};
export const blend = (color1, color2) => {
return (t) => {
return color1.lerp_to(color2, t);
};
};
export const blend_many = (colors) => {
const color_count = colors.length;
if (color_count === 0) {
return blend(Colors.black, Colors.black);
}
else if (color_count === 1) {
return blend(colors[0], colors[0]);
}
else if (color_count === 2) {
return blend(colors[0], colors[1]);
}
else {
return (t) => {
if (t >= 1) {
return colors[color_count - 1];
}
else if (t <= 0) {
return colors[0];
}
else {
const segments = color_count - 1;
const s = t * segments;
const segment = Math.trunc(s);
const segment_frac = modulo(s, 1);
const c1 = colors[segment];
const c2 = colors[segment + 1];
return c1.lerp_to(c2, segment_frac);
}
};
}
};
const adjust_for_luminance = (color, contrast_color) => {
const lum = color.luminance();
const contrast_lum = contrast_color.luminance();
const threshold = 0.6;
const offset = Math.abs(lum - contrast_lum);
if (offset > threshold) {
return color;
}
const boost = 0.7;
const t = lerp(0, threshold, boost, 0, offset);
if (contrast_lum > lum) {
return color.darken(t).burn(t * 0.6);
}
else {
return color.lighten(t).burn(t * 0.6);
}
};
const monochromatic = (entropy, hue_generator) => {
const hue = entropy.next_frac();
const is_tint = entropy.next();
const is_reversed = entropy.next();
const key_advance = entropy.next_frac() * 0.3 + 0.05;
const neutral_advance = entropy.next_frac() * 0.3 + 0.05;
let key_color = hue_generator(hue);
let contrast_brightness;
if (is_tint) {
contrast_brightness = 1;
key_color = key_color.darken(0.5);
}
else {
contrast_brightness = 0;
}
const neutral_color = grayscale(contrast_brightness);
const key_color_2 = key_color.lerp_to(neutral_color, key_advance);
const neutral_color_2 = neutral_color.lerp_to(key_color, neutral_advance);
const gradient = blend(key_color_2, neutral_color_2);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const monochromatic_fiducial = (entropy) => {
const hue = entropy.next_frac();
const is_reversed = entropy.next();
const is_tint = entropy.next();
const contrast_color = is_tint ? Colors.white : Colors.black;
const key_color = adjust_for_luminance(spectrum_cmyk_safe(hue), contrast_color);
const gradient = blend_many([key_color, contrast_color, key_color]);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const complementary = (entropy, hue_generator) => {
const spectrum1 = entropy.next_frac();
const spectrum2 = modulo(spectrum1 + 0.5, 1);
const lighter_advance = entropy.next_frac() * 0.3;
const darker_advance = entropy.next_frac() * 0.3;
const is_reversed = entropy.next();
const color1 = hue_generator(spectrum1);
const color2 = hue_generator(spectrum2);
const luma1 = color1.luminance();
const luma2 = color2.luminance();
let darker_color;
let lighter_color;
if (luma1 > luma2) {
darker_color = color2;
lighter_color = color1;
}
else {
darker_color = color1;
lighter_color = color2;
}
const adjusted_lighter_color = lighter_color.lighten(lighter_advance);
const adjusted_darker_color = darker_color.darken(darker_advance);
const gradient = blend(adjusted_darker_color, adjusted_lighter_color);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const complementary_fiducial = (entropy) => {
const spectrum1 = entropy.next_frac();
const spectrum2 = modulo(spectrum1 + 0.5, 1);
const is_tint = entropy.next();
const is_reversed = entropy.next();
const neutral_color_bias = entropy.next();
const neutral_color = is_tint ? Colors.white : Colors.black;
const color1 = spectrum_cmyk_safe(spectrum1);
const color2 = spectrum_cmyk_safe(spectrum2);
const biased_neutral_color = neutral_color
.lerp_to(neutral_color_bias ? color1 : color2, 0.2)
.burn(0.1);
const gradient = blend_many([
adjust_for_luminance(color1, biased_neutral_color),
biased_neutral_color,
adjust_for_luminance(color2, biased_neutral_color),
]);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const triadic = (entropy, hue_generator) => {
const spectrum1 = entropy.next_frac();
const spectrum2 = modulo(spectrum1 + 1.0 / 3, 1);
const spectrum3 = modulo(spectrum1 + 2.0 / 3, 1);
const lighter_advance = entropy.next_frac() * 0.3;
const darker_advance = entropy.next_frac() * 0.3;
const is_reversed = entropy.next();
const color1 = hue_generator(spectrum1);
const color2 = hue_generator(spectrum2);
const color3 = hue_generator(spectrum3);
const sorted_colors = [color1, color2, color3].sort((a, b) => a.luminance() - b.luminance());
const darker_color = sorted_colors[0];
const middle_color = sorted_colors[1];
const lighter_color = sorted_colors[2];
const adjusted_lighter_color = lighter_color.lighten(lighter_advance);
const adjusted_darker_color = darker_color.darken(darker_advance);
const gradient = blend_many([
adjusted_lighter_color,
middle_color,
adjusted_darker_color,
]);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const triadic_fiducial = (entropy) => {
const spectrum1 = entropy.next_frac();
const spectrum2 = modulo(spectrum1 + 1.0 / 3, 1);
const spectrum3 = modulo(spectrum1 + 2.0 / 3, 1);
const is_tint = entropy.next();
const neutral_insert_index = (entropy.next_uint8() % 2) + 1;
const is_reversed = entropy.next();
const neutral_color = is_tint ? Colors.white : Colors.black;
const colors = [
spectrum_cmyk_safe(spectrum1),
spectrum_cmyk_safe(spectrum2),
spectrum_cmyk_safe(spectrum3),
];
if (neutral_insert_index === 1) {
colors[0] = adjust_for_luminance(colors[0], neutral_color);
colors[1] = adjust_for_luminance(colors[1], neutral_color);
colors[2] = adjust_for_luminance(colors[2], colors[1]);
}
else if (neutral_insert_index === 2) {
colors[1] = adjust_for_luminance(colors[1], neutral_color);
colors[2] = adjust_for_luminance(colors[2], neutral_color);
colors[0] = adjust_for_luminance(colors[0], colors[1]);
}
else {
throw new Error('Internal error.');
}
colors.splice(neutral_insert_index, 0, neutral_color);
const gradient = blend_many(colors);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const analogous = (entropy, hue_generator) => {
const spectrum1 = entropy.next_frac();
const spectrum2 = modulo(spectrum1 + 1.0 / 12, 1);
const spectrum3 = modulo(spectrum1 + 2.0 / 12, 1);
const spectrum4 = modulo(spectrum1 + 3.0 / 12, 1);
const advance = entropy.next_frac() * 0.5 + 0.2;
const is_reversed = entropy.next();
const color1 = hue_generator(spectrum1);
const color2 = hue_generator(spectrum2);
const color3 = hue_generator(spectrum3);
const color4 = hue_generator(spectrum4);
let darkest_color;
let dark_color;
let light_color;
let lightest_color;
if (color1.luminance() < color4.luminance()) {
darkest_color = color1;
dark_color = color2;
light_color = color3;
lightest_color = color4;
}
else {
darkest_color = color4;
dark_color = color3;
light_color = color2;
lightest_color = color1;
}
const adjusted_darkest_color = darkest_color.darken(advance);
const adjusted_dark_color = dark_color.darken(advance / 2);
const adjusted_light_color = light_color.lighten(advance / 2);
const adjusted_lightest_color = lightest_color.lighten(advance);
const gradient = blend_many([
adjusted_darkest_color,
adjusted_dark_color,
adjusted_light_color,
adjusted_lightest_color,
]);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
const analogous_fiducial = (entropy) => {
const spectrum1 = entropy.next_frac();
const spectrum2 = modulo(spectrum1 + 1.0 / 10, 1);
const spectrum3 = modulo(spectrum1 + 2.0 / 10, 1);
const is_tint = entropy.next();
const neutral_insert_index = (entropy.next_uint8() % 2) + 1;
const is_reversed = entropy.next();
const neutral_color = is_tint ? Colors.white : Colors.black;
const colors = [
spectrum_cmyk_safe(spectrum1),
spectrum_cmyk_safe(spectrum2),
spectrum_cmyk_safe(spectrum3),
];
if (neutral_insert_index === 1) {
colors[0] = adjust_for_luminance(colors[0], neutral_color);
colors[1] = adjust_for_luminance(colors[1], neutral_color);
colors[2] = adjust_for_luminance(colors[2], colors[1]);
}
else if (neutral_insert_index === 2) {
colors[1] = adjust_for_luminance(colors[1], neutral_color);
colors[2] = adjust_for_luminance(colors[2], neutral_color);
colors[0] = adjust_for_luminance(colors[0], colors[1]);
}
else {
throw new Error('Internal error');
}
colors.splice(neutral_insert_index, 0, neutral_color);
const gradient = blend_many(colors);
if (is_reversed) {
return reverse(gradient);
}
else {
return gradient;
}
};
export const select_gradient = (entropy, version) => {
if (version === LifeHashVersion.grayscale_fiducial) {
return select_grayscale(entropy);
}
const val = entropy.next_uint2();
if (val === 0) {
if (version === LifeHashVersion.version1) {
return monochromatic(entropy, make_hue);
}
else if (version === LifeHashVersion.version2 ||
version === LifeHashVersion.detailed) {
return monochromatic(entropy, spectrum_cmyk_safe);
}
else if (version === LifeHashVersion.fiducial) {
return monochromatic_fiducial(entropy);
}
}
else if (val === 1) {
if (version === LifeHashVersion.version1) {
return complementary(entropy, spectrum);
}
else if (version === LifeHashVersion.version2 ||
version === LifeHashVersion.detailed) {
return complementary(entropy, spectrum_cmyk_safe);
}
else if (version === LifeHashVersion.fiducial) {
return complementary_fiducial(entropy);
}
}
else if (val === 2) {
if (version === LifeHashVersion.version1) {
return triadic(entropy, spectrum);
}
else if (version === LifeHashVersion.version2 ||
version === LifeHashVersion.detailed) {
return triadic(entropy, spectrum_cmyk_safe);
}
else if (version === LifeHashVersion.fiducial) {
return triadic_fiducial(entropy);
}
}
else if (val === 3) {
if (version === LifeHashVersion.version1) {
return analogous(entropy, spectrum);
}
else if (version === LifeHashVersion.version2 ||
version === LifeHashVersion.detailed) {
return analogous(entropy, spectrum_cmyk_safe);
}
else if (version === LifeHashVersion.fiducial) {
return analogous_fiducial(entropy);
}
}
return grayscale;
};