4

I have a real nice scene in SVG consisting of some clouds and a landscape, sort of like this:

enter image description here

Now I would like to work it in React.js and make it so you can scroll vertically through the scene, and it has sort of parallax effects. That is, say you just see this as your viewport initially.

enter image description here

As you scroll down, it reveals more of the vertical scene. BUT, it doesn't just scroll down the image like normal. Let's say the moon stays in view for a few "pages" of scrolling, only very slightly animating up. Then boom you reach a certain point and the moon quickly scrolls out of view and you are in the mountains. It scrolls slowly through the mountains and then boom quickly to the lake. Each time it "scrolls slowly through" something, that is room for some content to be overlaid. However long the content is for each "part" dictates how much slow scrolling there will be through that part of the scene. So even though the moon might be let's say 500px, there might be 3000px worth of content, so it should scroll let's say 200px of the moon SVG while in the moon phase, as it scrolls through 3000px of the content in front. Then it scrolls the remaining 300px plus some more perhaps to get past the moon, and then scrolls slowly through the mountains, with let's say 10000px of content. Etc.

Then, when in the mountains, each layer of the mountains "in the distance" moves slightly slower than the one in front. That sort of stuff.

The question is, how do I divide up the UI components / SVG / code so as to create these effects? Where I'm at now is, I have an SVG which has tons of transform="matrix(...)" all through each element, like this:

<g transform="matrix(1.27408,0,0,1.58885,-245.147,-3069.76)">
  <rect x="-430" y="4419.39" width="3572" height="1846.61" fill="blue" />
</g>

So I need to plug in some values into the 5th and 6th parameter field in the matrix to move things around. I am not sure necessarily what the starting position should be, maybe it starts off with the hardcoded values. Then as the window scroll event occurs, we do some calculations somehow, I don't know how it would work. Where I'm at now in thinking about this is, say you are on the moon and scrolling down.... We somehow capture the height of the moon statically in advance, then say "Moon will take 3000px to scroll through it" sort of thing. Well, "moon will take X amount of content height to scroll through it". So we calculate the content height outside of the SVG "scene" component, and pass into the SVG component the moonHeight / contentHeight + initialOffset, and that is the moon scroll position. Does that sort of make sense?

Then we do that for every element somehow. But now my mind starts to go haywire trying to conceptualize how it all seamlessly/easily fits together. As moon is scrolling, you start to see the tips of the mountain, and they are scrolling slightly too. So it's like, each "layer" of the illustration has a scrollable viewport set of checkpoints or something. Content area and fast/non-content area. I don't know, I am getting lost.

Any help in thinking this through would be greatly appreciated. I am not sure where to go with it yet. How do you configure the initial system so it can allow for content areas and non-content areas to move at different speeds and such, while keeping the rough overall layout of the scene. How much work will this take? What should I do to simplify?

Here is a React.js parallax library I'm checking out for inspiration, but briefly looking through the source code it is not straightforward what is happening yet. Plus, I think that might be pretty different than what I'm trying to do in some non-significant ways.

This seems like an extremely hard problem after thinking about it for some time. You would need to basically manually encode the position at each scroll change somehow, like keyframes. But that in itself seems tedious, error prone, and time consuming. Am I right? If not, I'd love to know.

Maybe what I'll do is divide the illustration into clear content / no content areas, and scroll slowly through the content areas and faster through the non-content (illustration-heavy) areas. That might simplify the problem so it's more easily workable.

Aaron Sarnat
  • 1,207
  • 8
  • 16
Lance
  • 75,200
  • 93
  • 289
  • 503
  • 1
    I would also go ahead and remove all transforms (guess you're using Sketch), split the layers into separate images, and then add them as background images. Update background position for each image, using Intersection Observer. It can be a little hard to understand how to implement, but it's easy once you get it to work. https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API – Rickard Elimää Aug 17 '21 at 06:33
  • 1
    Totally doable, but any way you slice it it's going to be tedious to adapt the SVG scene that you have to use the complex effect you described and get it to look right. Since what you described is such a specific effect, I'm not sure there's a "canonical" way to do it, however I'd check out [this method that uses 3D CSS transforms](https://medium.com/@dailyfire/pure-css-parallax-simple-tricks-da102d0ffdb9). It achieves the parallax by using perspective basically. As for speeding up and slowing down based on scroll position, that might require tweaking the Y coordinate of the translation. – Aaron Sarnat Aug 17 '21 at 19:54

1 Answers1

3

If you can inline the svg inside the html and prepare it with groups that represent the parallax scrolling planes you can do something like the snippet below.

Due to svg structure these groups are already in order from back to front (farthest to nearest). So you can insert into id attribute of groups the parallax factor like prefixNN.NNN.

Javascript-side you only need to match the groups, extract the parallax factor removing the prefix, and parsing the rest of the value as float.

Multiplying the parallax factor by the distance between the vertical center of the SVG and the center of the current view you will get the vertical translation to be applied to each group (with a multiplier to be adjusted if necessary).

Here the result: https://jsfiddle.net/t50qo9cp/

Sorry I can only attach the javascript example code due to post characters limits.

let svg = document.querySelector("svg");
let groups = document.querySelectorAll("svg g[id]");

let lastKnownScrollPosition = 0;
let ticking = false;

document.addEventListener('scroll', function(e) {
    lastKnownScrollPosition = window.scrollY;

    if (!ticking) {
        window.requestAnimationFrame(function() {
            parallax(lastKnownScrollPosition);
            ticking = false;
        });

        ticking = true;
    }
});

function parallax(scrollPos) {
    let delta = svg.clientTop + svg.clientHeight * 0.5 - scrollPos;

    for (let i = 0; i < groups.length; ++i) {
        let id = groups[i].getAttribute("id");
        let factor = parseFloat(id.substr(1)) * delta;
        groups[i].setAttribute("transform", "translate(0 " + factor + ")");
    }
}

UPDATE: More complete implementation

I have found a way to handle also parallax groups that are perpendicular to the viewing plane (i.e. the water, or the river on your image).

If you look carefully, the group containing the water is translated and also dynamically scaled vertically, following the parallax scrolling. It is not perspective correct because the group is scaled linearly between the top and bottom edges, but it certainly improves the perception of image depth.

Here a working demo: https://jsfiddle.net/Lrqns1u9/

Everything is as before except for:

  • javascript code to manage this particular type of groups
  • a new way to indicate the parallax factor in the id, in distant and near order prefixNN.NNN-NN.NNN (see the code below).
let svg = document.querySelector("svg");
// Groups filtered by g prefix on id attribute.
let groups = document.querySelectorAll("svg g[id^='g']");
// Regex to match and extract parallax factor(s)
let groupRegex = /^\w+(\d*(?:\.\d+)?)(?:-(\d*(?:\.\d+)?))?$/;

let lastKnownScrollPosition = 0;
let ticking = false;

document.addEventListener('scroll', function(e) {
  lastKnownScrollPosition = window.scrollY;

  if (!ticking) {
    window.requestAnimationFrame(function() {
      parallax(lastKnownScrollPosition);
      ticking = false;
    });

    ticking = true;
  }
});

// Do parallax scrolling on groups.
function parallax(scrollPos) {
  let svgHeight = svg.getBBox().height;

  // This variable controls the vertical coordinate of the document in which
  // the image appears as visible in the editors (all groups have no transformation).
  let delta = svg.clientTop + svg.clientHeight * 0.5 - scrollPos;

  for (let i = 0; i < groups.length; ++i) {
    let id = groups[i].getAttribute("id");
    let match = id.match(groupRegex);
    if (match === null)
      continue;

    let factor = parseFloat(match[1]) * delta;
    let transform = "translate(0 " + factor + ")";
    // If a second float is specified i.e.: 60.65-1.78 the group is perpendicular to
    // the viewing plane and need additional computation.
    if (match[2] != undefined) {
      let boundingBox = groups[i].getBBox();
      // Get parallax factor for bottom edge of group bounding box.
      let factorFront = parseFloat(match[2]) * delta + boundingBox.height;
      // Compute the scale for the group.
      let scale = (factorFront - factor) / boundingBox.height;
      // Compute the translation.
      let y = boundingBox.y + factor;

      // Tranform the group aligning first on zero y coordinate, then scale, 
      // then moving it back to correct position.
      transform = "translate(0, " + y + ") scale(1 " + scale + ") translate(0 " + -boundingBox.y + ")";
    }

    groups[i].setAttribute("transform", transform);
  }

}

NOTES:

  • the svg must be prepared correctly to avoid that the parallax planes have empty areas
  • the groups marked as parallax planes must not have transform to simplify the handling javascript-side.
Marco Sacchi
  • 712
  • 6
  • 21