Browse Source

tweaks to get page to sleep when nothing animated is on screen

Gregg Tavares 5 years ago
parent
commit
ebd941e81f
2 changed files with 108 additions and 64 deletions
  1. 44 5
      threejs/lessons/resources/threejs-lesson-utils.js
  2. 64 59
      threejs/resources/lessons-helper.js

+ 44 - 5
threejs/lessons/resources/threejs-lesson-utils.js

@@ -19,7 +19,7 @@ export const threejsLessonUtils = {
     this.pixelRatio = Math.max(2, window.devicePixelRatio);
 
     this.renderer = renderer;
-    this.renderFuncs = [];
+    this.elemToRenderFuncMap = new Map();
 
     const resizeRendererToDisplaySize = (renderer) => {
       const canvas = renderer.domElement;
@@ -34,8 +34,11 @@ export const threejsLessonUtils = {
 
     const clearColor = new THREE.Color('#000');
     let needsUpdate = true;
+    let rafRequestId;
+    let rafRunning;
 
     const render = (time) => {
+      rafRequestId = undefined;
       time *= 0.001;
 
       const resized = resizeRendererToDisplaySize(renderer);
@@ -52,7 +55,8 @@ export const threejsLessonUtils = {
         renderer.setScissorTest(true);
       }
 
-      this.renderFuncs.forEach((fn) => {
+      this.elementsOnScreen.forEach(elem => {
+        const fn = this.elemToRenderFuncMap.get(elem);
         const wasRendered = fn(renderer, time, resized);
         needsUpdate = needsUpdate || wasRendered;
       });
@@ -75,10 +79,44 @@ export const threejsLessonUtils = {
         renderer.domElement.style.transform = transform;
       }
 
-      requestAnimationFrame(render);
+      if (rafRunning) {
+        startRAFLoop();
+      }
     };
 
-    requestAnimationFrame(render);
+    function startRAFLoop() {
+      rafRunning = true;
+      if (!rafRequestId) {
+        rafRequestId = requestAnimationFrame(render);
+      }
+    }
+
+    this.elementsOnScreen = new Set();
+    this.intersectionObserver = new IntersectionObserver((entries) => {
+      entries.forEach(entry => {
+        if (entry.isIntersecting) {
+          this.elementsOnScreen.add(entry.target);
+        } else {
+          this.elementsOnScreen.delete(entry.target);
+        }
+        // Each entry describes an intersection change for one observed
+        // target element:
+        //   entry.boundingClientRect
+        //   entry.intersectionRatio
+        //   entry.intersectionRect
+        //   entry.isIntersecting
+        //   entry.rootBounds
+        //   entry.target
+        //   entry.time
+      });
+      if (this.elementsOnScreen.size > 0) {
+        startRAFLoop();
+      } else {
+        rafRunning = false;
+      }
+    });
+
+
   },
   addDiagrams(diagrams) {
     [...document.querySelectorAll('[data-diagram]')].forEach((elem) => {
@@ -220,7 +258,8 @@ export const threejsLessonUtils = {
       return true;
     };
 
-    this.renderFuncs.push(render);
+    this.intersectionObserver.observe(elem);
+    this.elemToRenderFuncMap.set(elem, render);
   },
   onAfterPrettify(fn) {
     this._afterPrettifyFuncs.push(fn);

+ 64 - 59
threejs/resources/lessons-helper.js

@@ -408,76 +408,81 @@
     }
   };
 
-  /**
-   * Gets the iframe in the parent document
-   * that is displaying the specified window .
-   * @param {Window} window window to check.
-   * @return {HTMLIFrameElement?) the iframe element if window is in an iframe
-   */
-  function getIFrameForWindow(window) {
-    if (!isInIFrame(window)) {
-      return;
-    }
-    const iframes = window.parent.document.getElementsByTagName('iframe');
-    for (let ii = 0; ii < iframes.length; ++ii) {
-      const iframe = iframes[ii];
-      if (iframe.contentDocument === window.document) {
-        return iframe;  // eslint-disable-line
+  // Replace requestAnimationFrame and cancelAnimationFrame with one
+  // that only executes when the body is visible (we're in an iframe).
+  // It's frustrating that th browsers don't do this automatically.
+  // It's half of the point of rAF that it shouldn't execute when
+  // content is not visible but browsers execute rAF in iframes even
+  // if they are not visible.
+  if (topWindow.requestAnimationFrame) {
+    topWindow.requestAnimationFrame = (function(oldRAF, oldCancelRAF) {
+      let nextFakeRAFId = 1;
+      const fakeRAFIdToCallbackMap = new Map();
+      let rafRequestId;
+      let isBodyOnScreen;
+
+      function rAFHandler(time) {
+        rafRequestId = undefined;
+        const ids = [...fakeRAFIdToCallbackMap.keys()];  // WTF! Map.keys() iterates over live keys!
+        for (const id of ids) {
+          const callback = fakeRAFIdToCallbackMap.get(id);
+          fakeRAFIdToCallbackMap.delete(id);
+          if (callback) {
+            callback(time);
+          }
+        }
       }
-    }
-  }
 
-  /**
-   * Returns true if window is on screen. The main window is
-   * always on screen windows in iframes might not be.
-   * @param {Window} window the window to check.
-   * @return {boolean} true if window is on screen.
-   */
-  function isFrameVisible(window) {
-    try {
-      const iframe = getIFrameForWindow(window);
-      if (!iframe) {
-        return true;
+      function startRAFIfIntersectingAndNeeded() {
+        if (!rafRequestId && isBodyOnScreen && fakeRAFIdToCallbackMap.size > 0) {
+          rafRequestId = oldRAF(rAFHandler);
+        }
       }
 
-      const bounds = iframe.getBoundingClientRect();
-      const isVisible = bounds.top < window.parent.innerHeight && bounds.bottom >= 0 &&
-                        bounds.left < window.parent.innerWidth && bounds.right >= 0;
-
-      return isVisible && isFrameVisible(window.parent);
-    } catch (e) {
-      return true;  // We got a security error?
-    }
-  }
+      function stopRAF() {
+        if (rafRequestId) {
+          oldCancelRAF(rafRequestId);
+          rafRequestId = undefined;
+        }
+      }
 
-  /**
-   * Returns true if element is on screen.
-   * @param {HTMLElement} element the element to check.
-   * @return {boolean} true if element is on screen.
-   */
-  function isOnScreen(element) {
-    let isVisible = true;
+      function initIntersectionObserver() {
+        const intersectionObserver = new IntersectionObserver((entries) => {
+          entries.forEach(entry => {
+            isBodyOnScreen = entry.isIntersecting;
+          });
+          if (isBodyOnScreen) {
+            startRAFIfIntersectingAndNeeded();
+          } else {
+            stopRAF();
+          }
+        });
+        intersectionObserver.observe(document.body);
+      }
 
-    if (element) {
-      const bounds = element.getBoundingClientRect();
-      isVisible = bounds.top < topWindow.innerHeight && bounds.bottom >= 0;
-    }
+      function betterRAF(callback) {
+        const fakeRAFId = nextFakeRAFId++;
+        fakeRAFIdToCallbackMap.set(fakeRAFId, callback);
+        startRAFIfIntersectingAndNeeded();
+        return fakeRAFId;
+      }
 
-    return isVisible && isFrameVisible(topWindow);
-  }
+      function betterCancelRAF(id) {
+        fakeRAFIdToCallbackMap.delete(id);
+      }
 
-  // Replace requestAnimationFrame.
-  if (topWindow.requestAnimationFrame) {
-    topWindow.requestAnimationFrame = (function(oldRAF) {
+      topWindow.cancelAnimationFrame = betterCancelRAF;
 
-      return function(callback, element) {
-        const handler = function() {
-          return oldRAF(isOnScreen(element) ? callback : handler, element);
-        };
-        return handler();
+      return function(callback) {
+        // we need to lazy init this because this code gets parsed
+        // before body exists. We could fix it by moving lesson-helper.js
+        // after <body> but that would require changing 100s of examples
+        initIntersectionObserver();
+        topWindow.requestAnimationFrame = betterRAF;
+        return betterRAF(callback);
       };
 
-    }(topWindow.requestAnimationFrame));
+    }(topWindow.requestAnimationFrame, topWindow.cancelAnimationFrame));
   }
 
   updateCSSIfInIFrame();