All files / utils color-delta.js

100% Statements 108/108
100% Branches 27/27
100% Functions 3/3
100% Lines 108/108

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 1092x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 148008x 148008x 148008x 148008x 148008x 148008x 148008x 148008x 148008x 148008x 148008x 148008x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 148022x 148022x 148022x 148013x 5x 5x 5x 148013x 148017x 148017x 148017x 148017x 9x 9x 9x 148017x 148017x 148017x 148017x 148017x 148022x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 92268x 148017x 148017x 148017x 55749x 55749x 55749x 148017x 2x 2x 2x 2x 2x 2x  
import { colorBlend } from './color-blend.js'
import { distYIQ, distCIEDE2000, distRGB, distRieRGB, distYIQBrightness } from './color-metrics.js'
 
/** @param {number} num - number to round */
const rounded = (num) => +num.toFixed(5)
 
export const maxDelta = {
  YIQ: rounded(distYIQ(0, 0, 0, 255, 255, 255)),
  RGB: rounded(distRGB(0, 0, 0, 255, 255, 255)),
  RieRGB: rounded(distRieRGB(0, 0, 0, 255, 255, 255)),
  CIEDE2000: rounded(distCIEDE2000(0, 0, 0, 255, 255, 255)),
  Brightness: rounded(Math.abs(distYIQBrightness(0, 0, 0, 255, 255, 255))),
}
 
export const validAlgorithms = Object.freeze(/** @type {const} */(['YIQ', 'CIEDE2000', 'RGB', 'RieRGB', 'Brightness']))
/** @typedef {(typeof validAlgorithms)[number]} Algorithm */
 
/**
 *
 * @param {Uint8Array | Uint8ClampedArray} img1 - original image
 * @param {Uint8Array | Uint8ClampedArray} img2 - image to compare
 * @param {number} posImg1 - pixel position on `img1`
 * @param {number} posImg2 - pixel position on `img2`
 * @param {Algorithm} algorithm - algorithm used to calculate delta defaults to CIEDE2000
 * @returns {colorDelta} color delta result
 */
export function colorDeltaImgPosition (img1, img2, posImg1, posImg2, algorithm) {
  const r1 = img1[posImg1 + 0]
  const g1 = img1[posImg1 + 1]
  const b1 = img1[posImg1 + 2]
  const a1 = img1[posImg1 + 3]
 
  const r2 = img2[posImg2 + 0]
  const g2 = img2[posImg2 + 1]
  const b2 = img2[posImg2 + 2]
  const a2 = img2[posImg2 + 3]
 
  return colorDelta(r1, g1, b1, a1, r2, g2, b2, a2, algorithm)
}
 
/**
 * calculate color difference according to the paper "Measuring perceived color difference
 * using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos
 * @param {number} r1 - color 1 rgba red value
 * @param {number} g1 - color 1 rgba green value
 * @param {number} b1 - color 1 rgba blue value
 * @param {number} a1 - color 1 rgba alpha value
 * @param {number} r2 - color 2 rgba red value
 * @param {number} g2 - color 2 rgba green value
 * @param {number} b2 - color 2 rgba blue value
 * @param {number} a2 - color 2 rgba alpha value
 * @param {Algorithm} algorithm - algorithm used to calculate delta defaults to CIEDE2000
 * @returns {colorDelta} color delta result
 */
export function colorDelta (r1, g1, b1, a1, r2, g2, b2, a2, algorithm) {
  algorithm = Object.hasOwn(maxDelta, algorithm) ? algorithm : 'CIEDE2000'
 
  if (a1 === a2 && r1 === r2 && g1 === g2 && b1 === b2) {
    return {
      delta: 0,
      maxDelta: maxDelta[algorithm],
    }
  }
 
  const algorithmFunction = (() => {
    switch (algorithm) {
      case 'YIQ': return distYIQ
      case 'RGB': return distRGB
      case 'RieRGB': return distRieRGB
      case 'Brightness': return distYIQBrightness
      case 'CIEDE2000':
      default: return distCIEDE2000
    }
  })()
 
  if (a1 < 255 || a2 < 255) {
    const rgb1Light = colorBlend(r1, g1, b1, a1, 255, 255, 255)
    const rgb2Light = colorBlend(r2, g2, b2, a2, 255, 255, 255)
    const deltaLightBlend = rounded(algorithmFunction(
      rgb1Light.R, rgb1Light.G, rgb1Light.B,
      rgb2Light.R, rgb2Light.G, rgb2Light.B
    ))
 
    const rgb1Dark = colorBlend(r1, g1, b1, a1, 0, 0, 0)
    const rgb2Dark = colorBlend(r2, g2, b2, a2, 0, 0, 0)
    const deltaDarkBlend = rounded(algorithmFunction(
      rgb1Dark.R, rgb1Dark.G, rgb1Dark.B,
      rgb2Dark.R, rgb2Dark.G, rgb2Dark.B
    ))
 
    return {
      delta: Math.max(deltaLightBlend, deltaDarkBlend),
      maxDelta: maxDelta[algorithm],
    }
  }
 
  const delta = rounded(algorithmFunction(r1, g1, b1, r2, g2, b2))
  return {
    delta,
    maxDelta: maxDelta[algorithm],
  }
}
 
/**
 * @typedef {object} colorDelta
 * @property {number} delta - color delta result
 * @property {number} maxDelta - max possible result value
 */