title: Efficiently get an element's visible area coordinates
body: <p>StackOverflow is <a href="https://stackoverflow.com/questions/704758/how-to-check-if-an-element-is-really-visible-with-javascript">loaded</a> <a href="https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom">with</a> <a href="https://stackoverflow.com/questions/19498068/how-to-get-on-screen-visible-element-objects-in-jquery">questions</a> about how to check if an element is really visible in the viewport, but they all seek for a <em>boolean</em> answer. I'm interested in getting the element's actual areas that are visible.</p>
  
  <pre><code>function getVisibleAreas(e) {
      ...
      return rectangleSet;
  }
  </code></pre>
  
  <p>Putting it more formally - the <em>visible areas</em> of elements is the set of (preferably non-overlapping) rectangles in CSS coordinates for which <a href="https://developer.mozilla.org/en-US/docs/Web/API/document.elementFromPoint" rel="noreferrer"><code>elementFromPoint(x, y)</code></a> will return the element if the point (x, y) is contained in (at least) one of the rectangles in the set.</p>
  
  <p>The outcome of calling this function on all DOM elements (including iframes) should be a set of non-overlapping area sets which union is the entire viewport area.</p>
  
  <p>My goal is to create some kind of a viewport "dump" data structure, which can efficiently return a single element for a given point in the viewport, and vice versa - for a given element in the dump, it will return the set of visible areas.
  (The data structure will be passed to a remote client application, so I will not necessarily have access to the actual document when I need to query the viewport structure).</p>
  
  <p>Implementation requirements:
  <li>Obviously, the implementation should consider element's <code>hidden</code> state, <code>z-index</code>, header &amp; footer etc.
  <li>I am looking for an implementation that works in all common used browsers, especially mobile - Android's Chrome and iOS's Safari.
  <li>Preferably doesn't use external libraries.</p>
  
  <p>Of course, I could be naïve and call <code>elementFromPoint</code> for every discrete point in the viewport, But performance is crucial since I iterate over all of the elements, and will do it quite often.</p>
  
  <p>Please direct me as to how I can achieve this goal.</p>
  
  <p><strong>Disclaimer:</strong> I'm pretty noob to web programming concepts, so I might have used wrong technical terms.</p>
  
  <p><strong>Progress:</strong></p>
  
  <p>I came up with an implementation. The algorithm is pretty simple:</p>
  
  <ol>
  <li> Iterate over all elements, and add their vertical / horizontal lines to a coordinates map (if the coordinate is within the viewport).
  <li> Call `document.elementFromPoint` for each "rectangle" center position. A rectangle is an area between two consecutive vertical and two consecutive horizontal coordinates in the map from step 1.
  </ol>
  
  <p>This produces a set of areas / rectangles, each pointing to a single element.</p>
  
  <p>The problems with my implementation are:</p>
  
  <ol>
  <li> It is inefficient for complicated pages (can take up to 2-4 minutes for a really big screen and gmail inbox).
  <li> It produces a large amount of rectangles per a single element, which makes it inefficient to stringify and send over a network, and also inconvenient to work with (I would want to end up with a set with as few rectangles as possible per element).
  </ol>
  
  <p>As much as I can tell, the <code>elementFromPoint</code> call is the one that takes a lot of time and causes my algorithm to be relatively useless...</p>
  
  <p>Can anyone suggest a better approach?</p>
  
  <p>Here is my implementation:</p>
  
  <pre><code>function AreaPortion(l, t, r, b, currentDoc) {
      if (!currentDoc) currentDoc = document;
      this._x = l;
      this._y = t;
      this._r = r;
      this._b = b;
      this._w = r - l;
      this._h = b - t;
  
      center = this.getCenter();
      this._elem = currentDoc.elementFromPoint(center[0], center[1]);
  }
  
  AreaPortion.prototype = {
      getName: function() {
          return "[x:" + this._x + ",y:" + this._y + ",w:" + this._w + ",h:" + this._h + "]";
      },
  
      getCenter: function() {
          return [this._x + (this._w / 2), this._y + (this._h / 2)];
      }
  }
  
  function getViewport() {
      var viewPortWidth;
      var viewPortHeight;
  
      // IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
      if (
              typeof document.documentElement != 'undefined' &amp;&amp;
              typeof document.documentElement.clientWidth != 'undefined' &amp;&amp;
              document.documentElement.clientWidth != 0) {
          viewPortWidth = document.documentElement.clientWidth,
          viewPortHeight = document.documentElement.clientHeight
      }
  
      // the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
      else if (typeof window.innerWidth != 'undefined') {
          viewPortWidth = window.innerWidth,
          viewPortHeight = window.innerHeight
      }
  
      // older versions of IE
      else {
          viewPortWidth = document.getElementsByTagName('body')[0].clientWidth,
          viewPortHeight = document.getElementsByTagName('body')[0].clientHeight
      }
  
      return [viewPortWidth, viewPortHeight];
  }
  
  function getLines() {
      var onScreen = [];
      var viewPort = getViewport();
      // TODO: header &amp; footer
      var all = document.getElementsByTagName("*");
  
      var vert = {};
      var horz = {};
  
      vert["0"] = 0;
      vert["" + viewPort[1]] = viewPort[1];
      horz["0"] = 0;
      horz["" + viewPort[0]] = viewPort[0];
      for (i = 0 ; i &lt; all.length ; i++) {
          var e = all[i];
          // TODO: Get all client rectangles
          var rect = e.getBoundingClientRect();
          if (rect.width &lt; 1 &amp;&amp; rect.height &lt; 1) continue;
  
          var left = Math.floor(rect.left);
          var top = Math.floor(rect.top);
          var right = Math.floor(rect.right);
          var bottom = Math.floor(rect.bottom);
  
          if (top &gt; 0 &amp;&amp; top &lt; viewPort[1]) {
              vert["" + top] = top;
          }
          if (bottom &gt; 0 &amp;&amp; bottom &lt; viewPort[1]) {
              vert["" + bottom] = bottom;
          }
          if (right &gt; 0 &amp;&amp; right &lt; viewPort[0]) {
              horz["" + right] = right;
          }
          if (left &gt; 0 &amp;&amp; left &lt; viewPort[0]) {
              horz["" + left] = left;
          }
      }
  
      hCoords = [];
      vCoords = [];
      //TODO: 
      for (var v in vert) {
          vCoords.push(vert[v]);
      }
  
      for (var h in horz) {
          hCoords.push(horz[h]);
      }
  
      return [hCoords, vCoords];
  }
  
  function getAreaPortions() {
      var portions = {}
      var lines = getLines();
  
      var hCoords = lines[0];
      var vCoords = lines[1];
  
      for (i = 1 ; i &lt; hCoords.length ; i++) {
          for (j = 1 ; j &lt; vCoords.length ; j++) {
              var portion = new AreaPortion(hCoords[i - 1], vCoords[j - 1], hCoords[i], vCoords[j]);
              portions[portion.getName()] = portion;
          }
      }
  
      return portions;
  }
  </code></pre>