本文逐步介绍了我如何制作此演示,展示了一种独特的面板滚动方式。
此演示中的代码(希望如此)非常简单易懂。这里没有 npm 模块或 ES2015 内容,我使用了经典的 jQuery、SCSS 和 HTML(加上一点 Greensock)。
我从一个随机的 Twitter 链接中获得了这个想法
前几天浏览我的 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 变换而引起的,这会导致子像素渲染问题,并显示黑色背景穿过裂缝。
就是这样!
如果您有任何关于我如何做得更好的建议,欢迎在评论中提出。
我惊呆了。我接触这个领域才 3 个月左右。这太棒了?
很棒的代码等等……但这种效果不适合我,我的眼睛有点不舒服!