chromium/ui/accessibility/extensions/color_contrast_companion/ui.js

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var maxScale = 32;

var scale = 4;
var fgtoggle = true;
var fgcolor = null;
var bgcolor = null;
var srcImage = null;
var imageData = null;
var prevScale = 1;

let close = document.getElementById('close');
close.addEventListener('click', () => {
  console.log('Sending message');
  chrome.runtime.sendMessage({'close': true});
});
let help = document.getElementById('help');
help.addEventListener('click', () => {
  window.open(chrome.runtime.getURL('help.html'), '_blank');
});

let canvas = document.querySelector('canvas');

function repaint() {
  console.log('Repaint ' + scale);
  let width = srcImage.naturalWidth;
  let height = srcImage.naturalHeight;
  let context = canvas.getContext('2d');
  context.imageSmoothingEnabled = false;
  canvas.style.transform = 'scale(' + scale + ')';

  canvas.width = 1;
  canvas.height = 1;
  canvas.offsetLeft;
  canvas.width = width;
  canvas.height = height;
  canvas.offsetLeft;

  context.drawImage(srcImage, 0, 0, width, height);
  imageData = context.getImageData(0, 0, width, height).data;
}


chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.imageDataUrl) {
    var img = document.createElement('img');
    img.addEventListener('load', () => {
      srcImage = img;
      repaint();
    });
    img.src = request.imageDataUrl;
  }
});

let hover = document.getElementById('hover');
let hoverrgb = document.getElementById('hoverrgb');
let fg = document.getElementById('fg');
let fgrgb = document.getElementById('fgrgb');
let bg = document.getElementById('bg');
let bgrgb = document.getElementById('bgrgb');
let scrollpanel = document.getElementById('img_panel');
let ratio = document.getElementById('ratio');
let details = document.getElementById('details');
let zoomin = document.getElementById('zoomin');
let zoomout = document.getElementById('zoomout');
let highlight = document.getElementById('highlight');

fg.classList.add('current');
bg.classList.remove('current');

function rgbToHex(color) {
  var r = color[0];
  var g = color[1];
  var b = color[2];
  return '#' + ((r << 16) | (g << 8) | b).toString(16);
}

function getRelativeLuminance(color) {
  var rSRGB = color[0] / 255;
  var gSRGB = color[1] / 255;
  var bSRGB = color[2] / 255;
  var r =
      rSRGB <= .03928 ? rSRGB / 12.92 : Math.pow((rSRGB + .055) / 1.055, 2.4);
  var g =
      gSRGB <= .03928 ? gSRGB / 12.92 : Math.pow((gSRGB + .055) / 1.055, 2.4);
  var b =
      bSRGB <= .03928 ? bSRGB / 12.92 : Math.pow((bSRGB + .055) / 1.055, 2.4);
  return .2126 * r + .7152 * g + .0722 * b;
};

function getContrast(color1, color2) {
  var c1lum = getRelativeLuminance(color1);
  var c2lum = getRelativeLuminance(color2);
  return (Math.max(c1lum, c2lum) + .05) / (Math.min(c1lum, c2lum) + .05);
}

let context = canvas.getContext('2d');
let bounds = canvas.getBoundingClientRect();

function getXY(evt) {
  var x = evt.clientX + scrollpanel.scrollLeft - bounds.left;
  var y = evt.clientY + scrollpanel.scrollTop - bounds.top;
  return [Math.floor(x / scale), Math.floor(y / scale)];
}

function getColor(x, y) {
  try {
    var pixelIndex = y * srcImage.naturalWidth + x;
    return imageData.slice(4 * pixelIndex, 4 * (pixelIndex + 1));
  } catch (e) {
    return [0, 0, 0, 0];
  }
}

function brightness(color) {
  return (color[0] + color[1] + color[2]) / 3;
}

function localMax(x, y) {
  // This needs to be optional. Doesn't always do what we want.
  return [x, y];

  if (x < 0 || x >= srcImage.naturalWidth || y < 0 ||
      y + j >= srcImage.naturalHeight) {
    return [x, y];
  }

  var ctr = getColor(x, y);
  var max = brightness(ctr);

  var max;
  var amax;
  for (var i = -2; i <= 2; i++) {
    for (var j = -2; j <= 2; j++) {
      if (x + i < 0 || x + i >= srcImage.naturalWidth)
        continue;
      if (y + j < 0 || y + j >= srcImage.naturalHeight)
        continue;
      var c = getColor(x + i, y + j);
      var cbright = brightness(c);
      if (max > 128 && cbright > max) {
        max = cbright;
        amax = [x + i, y + j];
      } else if (max < 128 && cbright < max) {
        max = cbright;
        amax = [x + i, y + j];
      }
    }
  }

  if (amax)
    return amax;
  else
    return [x, y];
}

canvas.addEventListener('mousemove', (evt) => {
  var x1, y1, x, y;
  [x1, y1] = getXY(evt);
  [x, y] = localMax(x1, y1);
  var color = getColor(x, y);
  var hex = rgbToHex(color);
  hover.style.backgroundColor = hex;
  hoverrgb.innerText = hex;

  if (scale >= 8) {
    highlight.style.display = 'block';
    highlight.style.left = (scale * x) + 'px';
    highlight.style.top = (scale * y) + 'px';
    highlight.style.width = scale + 'px';
    highlight.style.height = scale + 'px';
    highlight.style.top = (scale * y) + 'px';
  } else {
    highlight.style.display = 'none';
  }
});

canvas.addEventListener('mouseenter', (evt) => {
  highlight.style.display = 'block';
});

canvas.addEventListener('mouseleave', (evt) => {
  highlight.style.display = 'none';
});

canvas.addEventListener('click', (evt) => {
  var x1, y1, x, y;
  [x1, y1] = getXY(evt);
  [x, y] = localMax(x1, y1);
  var color = getColor(x, y);
  var hex = rgbToHex(color);
  if (fgtoggle) {
    fg.style.backgroundColor = hex;
    fgrgb.innerText = hex;
    fgcolor = color;
    bg.classList.add('current');
    fg.classList.remove('current');
  } else {
    bg.style.backgroundColor = hex;
    bgrgb.innerText = hex;
    bgcolor = color;
    fg.classList.add('current');
    bg.classList.remove('current');
  }
  fgtoggle = !fgtoggle;
  if (fgcolor && bgcolor) {
    var contrast = getContrast(fgcolor, bgcolor);
    ratio.innerText = contrast.toFixed(2);
    details.innerHTML = 'Foreground: ' + fgrgb.innerText + '\n' +
        'Background: ' + bgrgb.innerText + '\n' +
        'Ratio: ' + ratio.innerText;
    details.select();
  }
});

function updateZoom() {
  highlight.style.display = 'none';
  var prevScrollLeft = scrollpanel.scrollLeft;
  var prevScrollTop = scrollpanel.scrollTop;
  var panelBounds = scrollpanel.getBoundingClientRect();

  localStorage.setItem('scale', scale);
  zoomout.disabled = (scale == 1);
  zoomin.disabled = (scale >= maxScale);
  if (srcImage)
    repaint();

  console.log('prev: ' + prevScrollLeft + ', ' + prevScrollTop);
  console.log('factor: ' + (scale / prevScale));
  var newLeft = prevScrollLeft * (scale / prevScale);
  var newTop = prevScrollTop * (scale / prevScale);
  console.log('newLeft: ' + newLeft);
  console.log('newTop: ' + newTop);
  if (scale > prevScale) {
    newLeft += panelBounds.width / 2;
    newTop += panelBounds.height / 2;
    console.log('c newLeft: ' + newLeft);
    console.log('c newTop: ' + newTop);
  } else if (scale < prevScale) {
    newLeft -= panelBounds.width / 4;
    newTop -= panelBounds.height / 4;
    console.log('c newLeft: ' + newLeft);
    console.log('c newTop: ' + newTop);
  }
  scrollpanel.scrollLeft = newLeft;
  scrollpanel.scrollTop = newTop;
  prevScale = scale;
}

var scalevalue = localStorage.getItem('scale');
scale = parseInt(scalevalue, 10);
if (!scale || scale < 1 || scale > maxScale)
  scale = 4;
prevScale = scale;

updateZoom();

function onZoomIn() {
  if (scale < maxScale)
    scale *= 2;
  updateZoom();
}

function onZoomOut() {
  if (scale > 1)
    scale /= 2;
  updateZoom();
}

zoomin.addEventListener('click', () => {
  onZoomIn();
});

zoomout.addEventListener('click', () => {
  onZoomOut();
});

document.addEventListener('keydown', function(e) {
  if (e.key == '+' || e.key == '=') {
    onZoomIn();
  }
  if (e.key == '-') {
    onZoomOut();
  }

  console.log(e.key);
});