创建 Wavescroll

Avatar of Nikolay Talanov
Nikolay Talanov 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200美元免费额度!

本文逐步介绍了我如何制作此演示,展示了一种独特的面板滚动方式。

此演示中的代码(希望如此)非常简单易懂。这里没有 npm 模块或 ES2015 内容,我使用了经典的 jQuery、SCSS 和 HTML(加上一点 Greensock)。

前几天浏览我的 Twitter 订阅源时,我看到了一个指向 jetlag.photos 的链接。我对这种效果感到惊叹,并想尝试在不查看其源代码的情况下重现它。

后来,当我确实查看时,我发现他们的实现是基于,而我本来就不擅长这个,所以我很高兴我使用了我的技能集。

让我们从 HTML 开始

我们需要一个全屏大小的容器,并在其中包含一些用于每个面板的块。我们在这里使用ws-作为“wavescroll”的所有内容的命名空间。

<div class="ws-pages">
  <div class="ws-bgs">
    <div class="ws-bg"></div>
    <div class="ws-bg"></div>
    <div class="ws-bg"></div>
    <div class="ws-bg"></div>
    <div class="ws-bg"></div>
  </div>
</div>

我们还有一个额外的容器,因为稍后我们需要一个地方来放置我们的文本标题。

现在使用 Sass 进行基本样式设置

我们在这里使用 SCSS 语法,它有助于使命名空间部分变得非常容易。

.ws {
  &-pages {
    overflow: hidden;
    position: relative;
    height: 100vh; // main container should be 100% height of the screen
  }
  &-bgs {
    position: relative;
    height: 100%;
  }
  &-bg {
    display: flex;
    height: 100%;
    background-size: cover;
    background-position: center center;
  }
}

现在我们需要创建背景切片。每个“波浪”切片将是它自己的<div>。我们的目标是使它们看起来像一个大的背景,该背景以background-size: cover的帮助居中并调整大小。

每个切片将是屏幕宽度的 100%(不仅仅是切片的视觉宽度),并在每一步向左移动。

.ws-bg {
  &__part {
    overflow: hidden; // every part must show content only within it's own dimensions
    position: relative;
    height: 100%;
    cursor: grab;
    user-select: none; // significantly improves mouse-drag experience, by preventing background-image drag event
    &-inner {
      position: absolute;
      top: 0;
      // `left` property will be assigned through JavaScript
      width: 100vw; // each block takes 100% of screen width
      height: 100%;
      background-size: cover;
      background-position: center center;
    }
  }
}

然后使用 JavaScript 附加背景切片

我们可以使用 JavaScript for循环动态创建切片。我最初尝试使用循环甚至使用::before元素在 SCSS 中完成所有这些操作以减少 HTML,但这种方式更容易且更好,因为您不需要在 Sass 和 JavaScript 之间同步变量。

var $wsPages = $(".ws-pages");
var bgParts = 24; // variable for amount of slices
var $parts;

function initBgs() {
  var arr = [];
  var partW = 100 / bgParts; // width of one slice in %

  for (var i = 1; i <= bgParts; i++) {
    var $part = $('<div class="ws-bg__part">'); // a slice
    var $inner = $('<div class="ws-bg__part-inner">'); // inner slice
    var innerLeft = 100 / bgParts * (1 - i); // calculating position of inner slice
    $inner.css("left", innerLeft + "vw"); // assigning `left` property for each inner slice with viewport units
    $part.append($inner);
    $part.addClass("ws-bg__part-" + i).width(partW + "%"); // adding class with specific index for each slice and assigning width in %
    arr.push($part);
  }

  $(".ws-bg").append(arr); // append array of slices
  $wsPages.addClass("s--ready"); // we'll need this class later
  $parts = $(".ws-bg__part"); // new reference to all slices
};

initBgs();

在此函数的末尾,我们向容器添加了一个s--ready类(s表示状态,由 JavaScript 控制)。我们需要它来删除.ws-bg的静态background-image,我们最初显示它以便用户立即看到某些内容(感知性能!)。

添加切片后,原始容器的背景变得无用(并且有害,因为它们不会移动),所以让我们解决这个问题。

.ws-bg {
  .ws-pages.s--ready & {
    background: none;
  }
}
// Sass loop to set backgrounds
.ws-bg {
  @for $i from 1 through 5 {
    $img: url(../images/onepgscr-#{$i + 3}.jpg);
    &:nth-child(#{$i}) {
      background-image: $img;
      .ws-bg__part-inner {
        background-image: $img;
      }
    }
  }
}

处理鼠标移动

让我们为鼠标滑动附加处理程序,这些处理程序执行面板更改。

var numOfPages = $(".ws-bg").length;

// save the window dimensions
var winW = $(window).width();
var winH = $(window).height();

// Not debouncing since setting variables is low cost.
$(window).on("resize", function() {
  winW = $(window).width();
  winH = $(window).height();
});

var startY = 0;
var deltaY = 0;

// Delegated mouse handler, since all the parts are appended dynamically
$(document).on("mousedown", ".ws-bg__part", function(e) { 
  startY = e.pageY; // Y position of mouse at the beginning of the swipe
  deltaY = 0; // reset variable on every swipe

  $(document).on("mousemove", mousemoveHandler); // attaching mousemove swipe handler

  $(document).on("mouseup", swipeEndHandler); // and one for swipe end
});

var mousemoveHandler = function(e) {
  var y = e.pageY; // Y mouse position during the swipe

  // with the help of the X mouse coordinate, we are getting current active slice index (the slice the mouse is currently over)
  var x = e.pageX;
  index = Math.ceil(x / winW * bgParts);

  deltaY = y - startY; // calculating difference between current and starting positions
  moveParts(deltaY, index); // moving parts in different functions, by passing variables
};

var swipeEndHandler = function() {
  // removing swipeMove and swipeEnd handlers, which were attached on swipeStart
  $(document).off("mousemove", mousemoveHandler);
  $(document).off("mouseup", swipeEndHandler);

  if (!deltaY) return; // if there was no movement on Y axis, then we don't need to do anything else

  // if "swipe distance" is bigger than half of the screen height in specific direction, then we call the function to change panels
  if (deltaY / winH >= 0.5) navigateUp();
  if (deltaY / winH <= -0.5) navigateDown();

  // even if the panel doesn't change, we still need to move all parts to their default position for the current panel
  changePages(); 
};

// Update the current page
function navigateUp() {
  if (curPage > 1) curPage--;
};

function navigateDown() {
  if (curPage < numOfPages) curPage++;
};

添加波浪运动

是时候添加波浪了!

每个切片的位置都根据“活动”切片(光标所在的切片)以及“deltaY”和“index”变量确定。切片在 Y 轴上移动,并根据其与活动切片的距离延迟一段时间。此“延迟”不是一个静态数字,而是一个随着距离静态切片的距离越远而减小的数字,甚至降至零(变得平坦)。

var staggerStep = 4; // each slice away from the active slice moves slightly less
var changeAT = 0.5; // animation time in seconds

function moveParts(y, index) {

  var leftMax = index - 1; // max index of slices left of active
  var rightMin = index + 1; // min index of slices right of active

  var stagLeft = 0;
  var stagRight = 0;
  var stagStepL = 0;
  var stagStepR = 0;
  var sign = (y > 0) ? -1 : 1; // direction of swipe

  movePart(".ws-bg__part-" + index, y); // move active slice

  for (var i = leftMax; i > 0; i--) { // starting loop from right to left with slices, which are on the left side from the active slice

    var step = index - i;
    var sVal = staggerVal - stagStepL;

    // the first 15 steps we are using the default stagger, then reducing it to 1
    stagStepL += (step <= 15) ? staggerStep : 1;

    // no negative movement
    if (sVal < 0) sVal = 0;

    stagLeft += sVal;
    var nextY = y + stagLeft * sign; // Y value for current step

    // if the difference in distance of the current step is more than the deltaY of the active one, then we fix the current step on the default position
    if (Math.abs(y) < Math.abs(stagLeft)) nextY = 0;
    movePart(".ws-bg__part-" + i, nextY);
  }

  // same as above, for the right side
  for (var j = rightMin; j <= bgParts; j++) {
    var step = j - index;
    var sVal = staggerVal - stagStepR;
    stagStepR += (step <= 15) ? staggerStep : 1;
    if (sVal < 0) sVal = 0;
    stagRight += sVal;
    var nextY = y + stagRight * sign;
    if (Math.abs(y) < Math.abs(stagRight)) nextY = 0;
    movePart(".ws-bg__part-" + j, nextY);
  }

};

function movePart($part, y) {
  var y = y - (curPage - 1) * winH;

  // GSAP for animation
  TweenLite.to($part, changeAT, {y: y, ease: Back.easeOut.config(4)});
};

我正在使用 GSAP(Greensock)进行动画处理。通常我不使用动画库,但在这种情况下,我们需要实时动画(例如,在每个 mousemove 事件上暂停和重新启动),而不会失去平滑度,而 GSAP 在这方面做得非常好。

这是更改页面功能的实际函数。

var waveStagger = 0.013; // we don't want to move all slices at the same time, so we add a 13ms stagger
// we will remove the cumulative delay from animation time, because we don't want user to wait extra time just for this interaction

function changePages() {
  var y = (curPage - 1) * winH * -1; // position, based on current page variable
  var leftMax = index - 1;
  var rightMin = index + 1;

  TweenLite.to(".ws-bg__part-" + index, changeAT, {y: y});

  for (var i = leftMax; i > 0; i--) {
    var d = (index - i) * waveStagger;
    TweenLite.to(".ws-bg__part-" + i, changeAT - d, {y: y, delay: d});
  }

  for (var j = rightMin; j <= bgParts; j++) {
    var d = (j - index) * waveStagger;
    TweenLite.to(".ws-bg__part-" + j, changeAT - d, {y: y, delay: d});
  }
};

// call the function on resize to reset pixel values. you may want to debounce this now
$(window).on("resize", function() {
  winW = $(window).width();
  winH = $(window).height();
  changePages();
});

现在我们可以滑动!这是我们目前所处阶段的演示。

使用鼠标滚轮和箭头键进行分页

@EliFitch 帮我命名为“WaveScroll”。这些 UX 改进使它感觉更像波浪。

// used to block scrolling so one wheel spin doesn't go through all the panels
var waveBlocked = false;

var waveStartDelay = 0.2;

// mousewheel handlers. DOMMouseScroll is required for Firefox
$(document).on("mousewheel DOMMouseScroll", function(e) {
  if (waveBlocked) return;
  if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) {
    navigateWaveUp();
  } else { 
    navigateWaveDown();
  }
});

$(document).on("keydown", function(e) {
  if (waveBlocked) return;
  if (e.which === 38) { // key up
    navigateWaveUp();
  } else if (e.which === 40) { // key down
    navigateWaveDown();
  }
});

function navigateWaveUp() {
  if (curPage === 1) return;
  curPage--;
  waveChange();
};

function navigateWaveDown() {
  if (curPage === numOfPages) return;
  curPage++;
  waveChange();
};

function waveChange() {
  waveBlocked = true; // blocking scroll waveScroll
  var y = (curPage - 1) * winH * -1;

  for (var i = 1; i <= bgParts; i++) {
    // starting animation for each vertical group of slices with staggered delay
    var d = (i - 1) * waveStagger + waveStartDelay;
    TweenLite.to(".ws-bg__part-" + i, changeAT, {y: y, delay: d});
  }

  var delay = (changeAT + waveStagger * (bgParts - 1)) * 1000; // whole animation time in ms
  setTimeout(function() {
    waveBlocked = false; // remove scrollBlock when animation is finished
  }, delay);
};

现在所有部分都已组合在一起,我们有了最终的演示。

移动性能

在我用手机(Nexus 5)检查此演示后,我发现drag事件期间存在一些严重的性能问题。然后我记得通常需要优化任何移动处理程序(mousemove/touchmove),因为它们在短时间内触发了太多次。

requestAnimationFrame是解决方案。requestAnimationFrame 是浏览器为高性能动画创建的特殊 API。您可以在此处了解更多信息。

// Polyfill for rAF
window.requestAnimFrame = (function() {
  return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function(callback) {
      window.setTimeout(callback, 1000 / 60);
    };
})();

// Throttling function
function rafThrottle(fn) { // takes a function as parameter 
  var busy = false;
  return function() { // returning function (a closure)
    if (busy) return; // busy? go away! 
    busy = true; // hanging "busy" plate on the door 
    fn.apply(this, arguments); // calling function
    // using rAF to remove the "busy" plate, when browser is ready
    requestAnimFrame(function() {
      busy = false;
    });
  };
};

var mousemoveHandler = rafThrottle(function(e) {
  // same code as before 
});

我们如何处理触摸事件?

您可以在演示中找到此代码。它只是一个用于touchmove的额外处理程序,它与mousemove执行相同操作。我决定不在文章中写这个,因为即使在 rAF 性能优化之后,与原始网站(jetlag.photos)相比,移动性能仍然很糟糕,后者通过 . 仍然,考虑到它是 DOM 元素中的图像,它并不糟糕。

某些浏览器在切片的边缘存在额外的黑色线条问题。此问题是由于组合使用百分比宽度和移动过程中的 3D 变换而引起的,这会导致子像素渲染问题,并显示黑色背景穿过裂缝。

就是这样!

如果您有任何关于我如何做得更好的建议,欢迎在评论中提出。