Home Reference Source Repository

lib/mixins/dom-styles-reader.js

'use babel'

import Mixin from 'mixto'

/**
 * This mixin is used by the `CanvasDrawer` in `MinimapElement` to
 * read the styles informations from the DOM to use when rendering
 * the `Minimap`.
 */
export default class DOMStylesReader extends Mixin {
  /**
   * Returns the computed values for the given property and scope in the DOM.
   *
   * This function insert a dummy element in the DOM to compute
   * its style, return the specified property, and clear the content of the
   * dummy element.
   *
   * @param  {Array<string>} scopes a list of classes reprensenting the scope
   *                                to build
   * @param  {string} property the name of the style property to compute
   * @param  {boolean} [shadowRoot=true] whether to compute the style inside
   *                                     a shadow DOM or not
   * @param  {boolean} [cache=true] whether to cache the computed value or not
   * @return {string} the computed property's value
   */
  retrieveStyleFromDom (scopes, property, shadowRoot = true, cache = true) {
    this.ensureCache()

    let key = scopes.join(' ')
    let cachedData = this.constructor.domStylesCache[key]

    if (cache && (cachedData ? cachedData[property] : void 0) != null) {
      return cachedData[property]
    }

    this.ensureDummyNodeExistence(shadowRoot)

    if (!cachedData) {
      this.constructor.domStylesCache[key] = cachedData = {}
    }

    let parent = this.dummyNode
    for (let i = 0, len = scopes.length; i < len; i++) {
      let scope = scopes[i]
      let node = document.createElement('span')
      node.className = scope.replace(/\.+/g, ' ')

      if (parent != null) { parent.appendChild(node) }

      parent = node
    }

    let style = window.getComputedStyle(parent)
    let filter = style.getPropertyValue('-webkit-filter')
    let value = style.getPropertyValue(property)

    if (filter.indexOf('hue-rotate') > -1) {
      value = this.rotateHue(value, filter)
    }

    if (value !== '') { cachedData[property] = value }

    this.dummyNode.innerHTML = ''
    return value
  }

  /**
   * Creates a DOM node container for all the operations that need to read
   * styles properties from DOM.
   *
   * @param  {boolean} shadowRoot whether to create the dummy node in the shadow
   *                              DOM or not
   * @access private
   */
  ensureDummyNodeExistence (shadowRoot) {
    if (this.dummyNode == null) {
      /**
       * @access private
       */
      this.dummyNode = document.createElement('span')
      this.dummyNode.style.visibility = 'hidden'
    }

    this.getDummyDOMRoot(shadowRoot).appendChild(this.dummyNode)
  }

  /**
   * Ensures the presence of the cache object in the class that received
   * this mixin.
   *
   * @access private
   */
  ensureCache () {
    if (!this.constructor.domStylesCache) {
      this.constructor.domStylesCache = {}
    }
  }

  /**
   * Invalidates the cache by emptying the cache object.
   */
  invalidateDOMStylesCache () {
    this.constructor.domStylesCache = {}
  }

  /**
   * Invalidates the cache only for the first tokenization event.
   *
   * @access private
   */
  invalidateIfFirstTokenization () {
    if (this.constructor.hasTokenizedOnce) { return }
    this.invalidateDOMStylesCache()
    this.constructor.hasTokenizedOnce = true
  }

  /**
   * Computes the output color of `value` with a rotated hue defined
   * in `filter`.
   *
   * @param  {string} value the CSS color to apply the rotation on
   * @param  {string} filter the CSS hue rotate filter declaration
   * @return {string} the rotated CSS color
   * @access private
   */
  rotateHue (value, filter) {
    let match = value.match(/rgb(a?)\((\d+), (\d+), (\d+)(, (\d+(\.\d+)?))?\)/)
    let [, , r, g, b, , a] = match

    let [, hue] = filter.match(/hue-rotate\((\d+)deg\)/)

    ;[r, g, b, a, hue] = [r, g, b, a, hue].map(Number)
    ;[r, g, b] = rotate(r, g, b, hue)

    if (isNaN(a)) {
      return `rgb(${r}, ${g}, ${b})`
    } else {
      return `rgba(${r}, ${g}, ${b}, ${a})`
    }
  }
}

//    ##     ## ######## ##       ########  ######## ########   ######
//    ##     ## ##       ##       ##     ## ##       ##     ## ##    ##
//    ##     ## ##       ##       ##     ## ##       ##     ## ##
//    ######### ######   ##       ########  ######   ########   ######
//    ##     ## ##       ##       ##        ##       ##   ##         ##
//    ##     ## ##       ##       ##        ##       ##    ##  ##    ##
//    ##     ## ######## ######## ##        ######## ##     ##  ######

/**
 * Computes the hue rotation on the provided `r`, `g` and `b` channels
 * by the amount of `angle`.
 *
 * @param  {number} r the red channel of the color to rotate
 * @param  {number} g the green channel of the color to rotate
 * @param  {number} b the blue channel of the color to rotate
 * @param  {number} angle the angle to rotate the hue with
 * @return {Array<number>} the rotated color channels
 * @access private
 */
function rotate (r, g, b, angle) {
  let matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]
  const lumR = 0.2126
  const lumG = 0.7152
  const lumB = 0.0722
  const hueRotateR = 0.143
  const hueRotateG = 0.140
  const hueRotateB = 0.283
  const cos = Math.cos(angle * Math.PI / 180)
  const sin = Math.sin(angle * Math.PI / 180)

  matrix[0] = lumR + (1 - lumR) * cos - (lumR * sin)
  matrix[1] = lumG - (lumG * cos) - (lumG * sin)
  matrix[2] = lumB - (lumB * cos) + (1 - lumB) * sin
  matrix[3] = lumR - (lumR * cos) + hueRotateR * sin
  matrix[4] = lumG + (1 - lumG) * cos + hueRotateG * sin
  matrix[5] = lumB - (lumB * cos) - (hueRotateB * sin)
  matrix[6] = lumR - (lumR * cos) - ((1 - lumR) * sin)
  matrix[7] = lumG - (lumG * cos) + lumG * sin
  matrix[8] = lumB + (1 - lumB) * cos + lumB * sin

  return [
    clamp(matrix[0] * r + matrix[1] * g + matrix[2] * b),
    clamp(matrix[3] * r + matrix[4] * g + matrix[5] * b),
    clamp(matrix[6] * r + matrix[7] * g + matrix[8] * b)
  ]

  function clamp (num) {
    return Math.ceil(Math.max(0, Math.min(255, num)))
  }
}