垂直滚动 SVG 元素使其看起来有视差的原理?

Principles for vertical scrolling through SVG elements so it looks parallax?

我在 SVG 中有一个非常漂亮的场景,由一些云和风景组成,有点像这样:

现在我想在 React.js 中处理它,这样您就可以在场景中垂直滚动,并且它具有某种视差效果。也就是说,假设您最初只是将其视为您的视口。

向下滚动时,会显示更多垂直场景。但是,它不只是像往常一样向下滚动图像。假设月亮在视图中停留了几“页”的滚动,只是非常轻微地向上移动。然后砰的一声,你到达了某个点,月亮迅速滚出视线,你在山上。它缓慢地滚动穿过群山,然后迅速轰鸣到湖边。每次它“缓慢滚动”某些内容时,都会有一些内容被覆盖的空间。无论每个“部分”的内容有多长,都决定了场景该部分的滚动速度。因此,即使月亮可能比方说 500px,也可能有 3000px 的内容,所以它应该在月相时滚动比方说 200px 的月亮 SVG,因为它滚动前面 3000px 的内容。然后它滚动剩余的 300 像素再加上一些可能是为了越过月亮,然后慢慢滚动穿过山脉,假设有 10000 像素的内容。等等

然后,在山里的时候,“远处”的每一层山都比前面的山移动得稍微慢一点。那种东西。

问题是,如何划分 UI 组件/SVG/代码来创建这些效果?我现在的位置是,我有一个 SVG,每个元素都有大量的 transform="matrix(...)",像这样:

<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>

所以我需要将一些值插入到矩阵中的第 5 个和第 6 个参数字段中以移动内容。我不确定起始位置应该是什么,也许它以硬编码值开始。然后当 window 滚动事件发生时,我们以某种方式进行一些计算,我不知道它是如何工作的。我现在想的是,假设你在月球上并向下滚动……我们以某种方式提前静态捕获月球的高度,然后说“月球将需要 3000 像素才能滚动”排序的事情。好吧,“月亮将花费 X 的内容高度来滚动它”。所以我们计算 SVG“场景”组件之外的内容高度,并将 moonHeight / contentHeight + initialOffset 传递给 SVG 组件,即月亮滚动位置。这样说有意义吗?

然后我们以某种方式对每个元素都这样做。但现在我的思绪开始变得混乱,试图概念化这一切 seamlessly/easily 是如何组合在一起的。当月亮滚动时,你开始看到山尖,它们也在轻微滚动。就像插图的每个“层”都有一组可滚动的检查点视口或其他东西。内容区和fast/non-content区。我不知道,我迷路了。

任何帮助思考这个问题的人都将不胜感激。我还不确定去哪里。您如何配置初始系统,使其允许内容区域和非内容区域以不同的速度等移动,同时保持场景的粗略整体布局。这需要多少工作?我应该怎么做才能简化?

Here 是一个 React.js 视差库,我正在检查以寻找灵感,但简要浏览一下源代码,还不清楚发生了什么。另外,我认为这可能与我在一些不重要的方面尝试做的事情有很大不同。

经过一段时间的思考,这似乎是一个非常困难的问题。您基本上需要以某种方式手动编码每次滚动更改时的位置,例如关键帧。但这本身似乎乏味、容易出错且耗时。我对吗?如果没有,我很想知道。

也许我要做的是将插图分成清晰的内容/无内容区域,并在内容区域中缓慢滚动,在非内容(插图较多)区域中滚动得更快。这可能会简化问题,使其更容易解决。

如果您可以将 svg 内联到 html 中,并使用表示视差滚动平面的组来准备它,您可以执行类似下面的代码片段的操作。

由于 svg 结构,这些组已经按照从后到前(从远到近)的顺序排列。因此,您可以在组的 id 属性中插入像 prefixNN.NNN.

这样的视差因子

Javascript-side你只需要匹配组,提取视差因子去除前缀,并将其余值解析为float。

将视差因子乘以 SVG 的垂直中心与当前视图中心之间的距离,您将得到应用于每个组的垂直平移(如有必要,可以调整乘数)。

这里是结果:https://jsfiddle.net/t50qo9cp/

抱歉,由于 post 个字符限制,我只能附上 javascript 示例代码。

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 + ")");
    }
}

更新:更完整的实现

我找到了一种方法来处理垂直于观察平面(即图像上的水或河流)的视差组。

如果你仔细观察,包含水的组会被平移 并且还会在视差滚动之后垂直动态缩放 。它不是透视正确的,因为该组在顶部和底部边缘之间线性缩放,但它确实改善了图像深度的感知。

这是一个工作演示:https://jsfiddle.net/Lrqns1u9/

一切都和以前一样,除了:

  • javascript 管理这种特定类型群组的代码
  • 一种在 id 中指示视差因子的新方法,按远近顺序排列prefixNN.NNN-NN.NNN(参见下面的代码)。
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);
  }

}

备注:

  • 必须正确准备 svg 以避免视差平面有空白区域
  • 标记为视差平面的组不能有变换以简化javascript端的处理。