Jump to content
Search Community

Using JS Proxy to create complex timelines with objects not yet present in DOM

iongion test
Moderator Tag

Recommended Posts

I've recently had to work on a task where I had to overlay multiple story animations across a video.

I synced their timelines in an acceptable way.

But the performance hit is that I need to add all elements from start, why:

 

- I start with a root timeline that I connect to a slider and normal play/resume/stop/rewind control buttons

- Each stories happen between phases  `phase1s,phas1e .... phase2s,phase2e ...... phase3s,phase3e` - these are exactly gsap timelines with offsets from root

 

This above is great, the root timeline provides the slider with enough information to control, seek, etc.

But there is a performance hit, I need to layout all phases scenes from start, which for 120 phases is not fun, as they all have their own complex DOM  + SVG animations.

 

So I've tried to find a way to still be able to have the prettiness of a single global timeline that controls the child timelines with frame precision, but avoid creating all in the DOM from start.

 

The problem is that gsap needs DOM elements to exit, but what if instead of using selectors, I use JS Proxy.

So I thought of building this helper called withActors

 

const stageIdenitifer = 1;
const { rectange, circle, logo, banner, person } = withActors(stageIdentifier, {
  rectangle: '.Rectangle',
  circle: '.Circle',
  logo: '.Logo',
  person: '.Person',
});

// Start creating animation scenario using st - the scenario timeline
const st = gsap.timeline();
// all these are not dom elements, they are proxies for elements that don't yet exist
st.to(rectangle, { top: 200, left: 100 });
st.to(circle, { top: 250, left: 100 });
st.to(logo, { top: 300, left: 100 });
st.to(person, { top: 400, left: 100 });

 

And this is how an hasty implementation looks like

 

const isPlayStageActor = Symbol("isPlayStageActor");
const playStageActorTargetElement = Symbol("playStageActorTargetElement");
function withPlayStageActors(id: any, keysMap: any) {
  const playStageSelector = `.Stage[data-stage-id="${id}"]`;
  const internalKeysMap = { ...keysMap, playStage: playStageSelector };
  const selectorsMap = Object.keys(internalKeysMap).reduce((acc, actorKey) => {
    const elementSelector = `${playStageSelector} ${internalKeysMap[actorKey]}`;
    let element: any;
    acc[actorKey] = new Proxy({} as any, {
      get(target, propKey, receiver) {
        if (propKey === isPlayStageActor) {
          return true;
        }
        if (propKey === playStageActorTargetElement) {
          return target.element;
        }
        if (!element) {
          const nodeSelector = actorKey === "playStage" ? playStageSelector : elementSelector;
          element = document.querySelector(nodeSelector);
          target.element = element;
        }
        if (element) {
          if (typeof element[propKey] === "function") {
            if (!target[propKey]) {
              target[propKey] = (...args: any[]) => {
                return (element as any)[propKey].apply(element, args);
              };
            }
            return target[propKey];
          }
          return Reflect.get(element, propKey);
        }
      },
      set(target, propKey, value) {
        if (!element) {
          const nodeSelector = actorKey === "playStage" ? playStageSelector : elementSelector;
          element = document.querySelector(nodeSelector);
          target.element = element;
        }
        if (element) {
          return Reflect.set(element, propKey, value);
        }
        return true;
      },
    });
    return acc;
  }, {} as any);
  return selectorsMap;
}

 

Now all is good, except that gsap CSSPlugin uses global window.getComputedStyle and that one wants to work only with Elements - not proxies, so to solve it as there is no way to inject it from outside into gsap CSSPlugin only, I have to patch it globally with something like this

 

  if (!(window as any).isGetComputedStylePatched) {
    const previousGetComputedStyle = window.getComputedStyle;
    window.getComputedStyle = function (node: any) {
      if (node[isPlayStageActor]) {
        return previousGetComputedStyle(node[playStageActorTargetElement]);
      }
      return previousGetComputedStyle(node);
    };
    (window as any).isGetComputedStylePatched = true;
  }

 

It works insanely well :) - just wanted to share with the next soul having to go through this.

Now I can create 1000 nested timelines without having any impact on the DOM (especially as I am combining this with React where I always have to wait for useEffect/useLayoutEffect)

 

What do you guys think ?

 

 

Link to comment
Share on other sites

I'm happy to hear you got it working and you're happy with it. Thanks for sharing. Some questions: 

  1. Are you saying that you want to create animations for targets that don't even exist [yet]? That seems quite odd and potentially problematic. Like what if immediately after you create the timeline, there's a seek() or progress() call that attempts to move the playhead forward to a spot that'd require those targets to exist (and render)?
  2. You're only using .to() tweens, right? This technique seems fragile to me and wouldn't work properly with .from() or .fromTo() or .set() calls because those render immediately, so if the targets don't exist, that'd be a problem. The same goes for ScrollTriggers. If the triggers or elements that need to get pinned don't exist yet, that'll break things and measurements couldn't be made correctly. 
  3. Can you explain why you think there's a performance hit when you have things in the DOM? Like...wouldn't it be much simpler and more reliable to use something like autoAlpha to toggle the visibility of things in your timeline so that all the non-animating (future) elements have visibility: hidden until the playhead reaches a spot in the timeline where they're needed? It seems expensive to keep shifting things in and out of the DOM during the animation. So you're kinda trading slowdowns. I'm not saying you're wrong about your particular setup - maybe it performs much worse when everything is in the DOM even with visibility: hidden, but in general the slowdowns come from graphics rendering in the browser but if you set visibility: hidden, it allows the browser to skip rendering those elements (better performance). So I just wonder if you're burning a lot of time creatively solving a problem that could be more easily solved in a different, straightforward manner. Perhaps you could share a minimal demo that clearly shows a comparison where your technique performs noticeably better? 
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...