UNPKG

lifehash

Version:

TypeScript/JavaScript implementation of LifeHash, a visual hash algorithm

361 lines (360 loc) 12.4 kB
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; };