本文概述
我几乎每天都在处理自定义全屏布局。通常, 这些布局意味着大量的交互和动画。在大多数情况下, 无论是时间触发的复杂时间轴转换还是基于滚动的用户驱动的事件集, UI都需要的不只是使用现成的插件解决方案, 还需要进行一些调整和更改。另一方面, 我看到许多JavaScript开发人员倾向于使用他们最喜欢的JS插件来简化他们的工作, 即使该任务可能不需要某个插件提供的所有帮助。
免责声明:当然, 使用众多可用插件中的一种有其优势。你将获得各种选项, 可用于调整需求, 而无需进行大量编码。此外, 大多数插件作者都会优化代码, 使其跨浏览器和跨平台兼容, 等等。但是, 你仍然可以在项目中包含一个完整的库, 该库可能仅包含它提供的一两个不同的内容。我并不是说使用任何类型的第三方插件自然是一件坏事, 我每天都会在我的项目中这样做, 只是权衡每种方法的利弊通常是一个好主意编码方面的好习惯。当以这种方式来做自己的事情时, 需要更多的编码知识和经验才能知道你要寻找的内容, 但是最后, 你应该获得一段仅以一种方式执行某一项操作的代码。你想要它。
本文旨在展示一种纯CSS / JS方法, 用于开发具有自定义内容动画的全屏滚动触发的滑块布局。在这种按比例缩小的方法中, 我将介绍你期望通过CMS后端提供的基本HTML结构, 现代CSS(SCSS)布局技术以及可实现完全交互性的原始JavaScript编码。作为一个简单的概念, 可以很容易地将此概念扩展到大型插件和/或在不依赖核心的各种应用程序中使用。
我们将要创建的设计是一个简约的建筑师作品集展示, 其中包含每个项目的特色图片和标题。带有动画的完整滑块将如下所示:
你可以在此处查看演示, 也可以访问我的Github存储库以获取更多详细信息。
HTML概述
因此, 这是我们将使用的基本HTML:
<div id="hero-slider">
<div id="logo" class="mask">
<!-- Textual logo will go here -->
</div>
<div id="slideshow">
<div id="slides-main" class="slides">
<!-- Featured image slides will go here -->
</div>
<div id="slides-aux" class="slides mask">
<!-- Slide titles will go here -->
</div>
</div>
<div id="info">
<!-- Static info on the right -->
</div>
<nav id="slider-nav">
<!-- Current slide indicator -->
</nav>
</div>
我们的主要持有者是具有id的滑块。在内部, 布局分为几部分:
- 徽标(静态部分)
- 我们将主要处理的幻灯片
- 信息(静态部分)
- 滑块导航, 它将指示当前活动的幻灯片以及幻灯片总数
让我们集中讨论幻灯片部分, 因为这是我们对本文的关注点。在这里, 我们有两个部分-main和aux。 Main是包含特色图像的div, 而aux则保留图像标题。这两个支架内部的每张幻灯片的结构都很基本。这是主支架内部的图像幻灯片:
<div class="slide" data-index="0">
<div class="abs-mask">
<div class="slide-image" style="background-image: url(./assets/img/slide-1.jpg)"> </div>
</div>
</div>
索引数据属性是我们用来跟踪幻灯片显示位置的属性。我们将使用abs-mask div来创建有趣的过渡效果, 幻灯片图像div包含特定的特色图片。内联渲染图像, 就像它们直接来自CMS一样, 并由最终用户设置。
同样, 标题在辅助支架内部滑动:
<h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>
每个幻灯片标题都是带有相应数据属性的H2标签和一个链接, 该链接可以转到该项目的单个页面。
我们HTML的其余部分也非常简单。我们在顶部有一个徽标, 静态信息, 该信息可以告诉用户他们位于哪个页面上, 一些描述以及滑块的当前/总计指示器。
CSS概述
源CSS代码是用SCSS编写的, SCSS是CSS预处理程序, 然后将其编译为浏览器可以解释的常规CSS。 SCSS为你提供了使用变量, 嵌套选择, mixin和其他不错的东西的优点, 但是需要将其编译为CSS才能使浏览器按需读取代码。就本教程而言, 我已经使用Scout-App来处理编译, 因为我希望将工具做到最低限度。
我使用flexbox处理基本的并排布局。想法是在一侧放幻灯片, 在另一侧放信息部分。
#hero-slider {
position: relative;
height: 100vh;
display: flex;
background: $dark-color;
}
#slideshow {
position: relative;
flex: 1 1 $main-width;
display: flex;
align-items: flex-end;
padding: $offset;
}
#info {
position: relative;
flex: 1 1 $side-width;
padding: $offset;
background-color: #fff;
}
让我们深入探讨一下位置, 然后再次关注幻灯片部分:
#slideshow {
position: relative;
flex: 1 1 $main-width;
display: flex;
align-items: flex-end;
padding: $offset;
}
#slides-main {
@extend %abs;
&:after {
content: '';
@extend %abs;
background-color: rgba(0, 0, 0, .25);
z-index: 100;
}
.slide-image {
@extend %abs;
background-position: center;
background-size: cover;
z-index: -1;
}
}
#slides-aux {
position: relative;
top: 1.25rem;
width: 100%;
.slide-title {
position: absolute;
z-index: 300;
font-size: 4vw;
font-weight: 700;
line-height: 1.3;
@include outlined(#fff);
}
}
我将主滑块设置为绝对位置, 并使用background-size:cover属性将背景图像拉伸到整个区域。为了提供与幻灯片标题更大的对比, 我设置了一个绝对的伪元素作为叠加层。包含幻灯片标题的辅助滑块位于屏幕底部和图像顶部。
由于一次只能看到一张幻灯片, 因此我也将每个标题都设置为绝对, 并通过JS计算出持有人的大小, 以确保没有分界线, 但是在接下来的部分内容中, 我们会详细介绍。在这里, 你可以看到名为扩展的SCSS功能的使用:
%abs {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
由于我经常使用绝对定位, 因此我将此CSS拉到了一个可扩展的组件中, 以使其易于在各种选择器中使用。另外, 我创建了一个名为” outlined”的mixin, 以便在设置标题和主滑块标题的样式时提供DRY方法。
@mixin outlined($color: $dark-color, $size: 1px) {
color: transparent;
-webkit-text-stroke: $size $color;
}
至于这种布局的静态部分, 它没有什么复杂的, 但是在这里, 当放置必须在Y轴而不是其正常方向的文本时, 你可以看到一个有趣的方法:
.slider-title-wrapper {
position: absolute;
top: $offset;
left: calc(100% - #{$offset});
transform-origin: 0% 0%;
transform: rotate(90deg);
@include outlined;
}
我想提醒你注意transform-origin属性, 因为我发现它对于这种类型的布局确实使用不足。放置此元素的方式是使其锚点停留在该元素的左上角, 设置旋转点并使文本从该点连续向下流动, 而对于不同的屏幕尺寸则没有问题。
让我们看一下更有趣的CSS部分-初始加载动画:
通常, 这种同步动画行为是使用库实现的, 例如, GSAP是目前最好的库之一, 它提供出色的渲染功能, 易于使用并且具有时间轴功能, 使开发人员能够以编程方式链接元素彼此过渡。
但是, 由于这是一个纯CSS / JS示例, 因此我决定在这里变得非常基础。因此, 默认情况下, 每个元素都设置为其初始位置-通过转换或不透明度隐藏, 并在由JS触发的滑块加载时显示。手动调整所有过渡属性, 以确保自然有趣的流程, 每次过渡继续过渡到另一个过渡, 提供令人愉悦的视觉体验。
#logo:after {
transform: scaleY(0);
transform-origin: 50% 0;
transition: transform .35s $easing;
}
.logo-text {
display: block;
transform: translate3d(120%, 0, 0);
opacity: 0;
transition: transform .8s .2s, opacity .5s .2s;
}
.current, .sep:before {
opacity: 0;
transition: opacity .4s 1.3s;
}
#info {
transform: translate3d(100%, 0, 0);
transition: transform 1s $easing .6s;
}
.line {
transform-origin: 0% 0;
transform: scaleX(0);
transition: transform .7s $easing 1s;
}
.slider-title {
overflow: hidden;
>span {
display: block;
transform: translate3d(0, -100%, 0);
transition: transform .5s 1.5s;
}
}
如果你希望在这里看到一件事, 那就是使用了transform属性。移动HTML元素时, 无论是过渡元素还是动画元素, 建议使用transform属性。我看到很多人倾向于使用边距或边距, 甚至是顶部, 左侧等偏移, 这在渲染时不会产生足够的效果。
为了更深入地了解添加交互行为时如何使用CSS, 我不太推荐以下文章。
这是由Chrome工程师Paul Lewis撰写的, 几乎涵盖了人们应该知道的有关Web中像素渲染的所有信息, 无论是CSS还是JS。
JavaScript概述和滑块逻辑
JavaScript文件分为两个不同的函数。
heroSlider函数负责我们在此处需要的所有功能, 以及utils函数, 在其中我添加了几个可重复使用的实用程序函数。如果你想在项目中重用它们, 我已经评论了每个实用程序功能以提供上下文。
主要功能的编码方式有两个分支:init和resize。这些分支可通过返回主函数获得, 并在必要时调用。 init是主函数的初始化, 它是在窗口加载事件时触发的。同样, 在窗口调整大小时触发调整大小分支。调整大小功能的唯一目的是在窗口调整大小时重新计算标题的滑块大小, 因为标题字体大小可能会有所不同。
在heroSlider函数中, 我提供了一个滑动器对象, 其中包含我们将需要的所有数据和选择器:
const slider = {
hero: document.querySelector('#hero-slider'), main: document.querySelector('#slides-main'), aux: document.querySelector('#slides-aux'), current: document.querySelector('#slider-nav .current'), handle: null, idle: true, activeIndex: -1, interval: 3500
};
附带说明一下, 例如, 如果你使用React, 则可以轻松地采用这种方法, 因为你可以将数据存储为状态或使用新添加的挂钩。为了说明问题, 让我们仔细研究一下此处的每个键值对所代表的含义:
- 前四个属性是对我们将要操作的DOM元素的HTML引用。
- handle属性将用于启动和停止自动播放功能。
- idle属性是一个标志, 它将防止用户在幻灯片过渡时强制滚动。
- activeIndex将使我们能够跟踪当前活动的幻灯片
- interval表示滑块的自动播放间隔
滑块初始化后, 我们调用两个函数:
setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title'));
loadingAnimation();
setHeight函数可扩展到实用程序函数, 以根据最大标题大小设置辅助滑块的高度。这样, 我们可以确保提供足够的大小, 即使幻灯片的内容分成两行, 也不会剪切幻灯片标题。
loadingAnimation函数将CSS类添加到提供CSS过渡介绍的元素中:
const loadingAnimation = function () {
slider.hero.classList.add('ready');
slider.current.addEventListener('transitionend', start, {
once: true
});
}
由于滑块指示器是CSS过渡时间轴中的最后一个元素, 因此我们等待其过渡结束并调用start函数。通过提供其他参数作为对象, 我们确保仅触发一次。
让我们看一下启动功能:
const start = function () {
autoplay(true);
wheelControl();
window.innerWidth <= 1024 && touchControl();
slider.aux.addEventListener('transitionend', loaded, {
once: true
});
}
因此, 当布局完成时, 其初始过渡由loadingAnimation函数触发, 然后start函数接管。然后, 它会触发自动播放功能, 启用滚轮控制, 确定我们是在触摸式还是台式设备上, 并等待标题幻灯片第一次过渡以添加适当的CSS类。
自动播放
此布局的核心功能之一是自动播放功能。让我们看一下相应的功能:
const autoplay = function (initial) {
slider.autoplay = true;
slider.items = slider.hero.querySelectorAll('[data-index]');
slider.total = slider.items.length / 2;
const loop = () => changeSlide('next');
initial && requestAnimationFrame(loop);
slider.handle = utils().requestInterval(loop, slider.interval);
}
首先, 我们将自动播放标志设置为true, 以指示滑块处于自动播放模式。当确定用户与滑块互动后是否重新触发自动播放时, 此标志很有用。然后, 我们将引用所有滑块项目(幻灯片), 因为我们将更改它们的活动类并通过将所有项目相加并除以2(因为我们有两个同步的滑块布局(主要和辅助))来计算滑块将要进行的总迭代次数但只有一个”滑块”本身会同时更改它们。
代码中最有趣的部分是循环功能。它调用slideChange, 提供我们将在一分钟内浏览的幻灯片方向, 但是循环功能被调用了两次。让我们看看为什么。
如果初始参数的值为true, 我们将调用loop函数作为requestAnimationFrame回调。这仅在第一次滑动器加载时发生, 这会立即触发滑动器更换。使用requestAnimationFrame我们在下一帧重新绘制之前执行提供的回调。
但是, 由于我们希望在自动播放模式下继续浏览幻灯片, 因此我们将重复调用同一功能。这通常是通过setInterval实现的。但是在这种情况下, 我们将使用一种实用程序功能–requestInterval。尽管setInterval可以很好地工作, 但requestInterval是一个高级概念, 它依赖于requestAnimationFrame并提供了一种性能更高的方法。它确保仅在浏览器选项卡处于活动状态时才重新触发该功能。
在这篇很棒的文章中, 有关CSS概念的更多信息, 请参见CSS技巧。请注意, 我们将此函数的返回值分配给我们的slider.handle属性。该函数返回的唯一ID可供我们使用, 稍后我们会在使用cancelAnimationFrame时使用它来取消自动播放。
幻灯片更改
slideChange函数是整个概念中的主要功能。无论是通过自动播放还是通过用户触发, 它都会更改幻灯片。它知道滑块的方向, 并提供循环播放功能, 因此当你转到最后一张幻灯片时, 你将可以继续到第一张幻灯片。这是我的编码方式:
const changeSlide = function (direction) {
slider.idle = false;
slider.hero.classList.remove('prev', 'next');
if (direction == 'next') {
slider.activeIndex = (slider.activeIndex + 1) % slider.total;
slider.hero.classList.add('next');
} else {
slider.activeIndex = (slider.activeIndex - 1 + slider.total) % slider.total;
slider.hero.classList.add('prev');
}
//reset classes
utils().removeClasses(slider.items, ['prev', 'active']);
//set prev
const prevItems = [...slider.items]
.filter(item => {
let prevIndex;
if (slider.hero.classList.contains('prev')) {
prevIndex = slider.activeIndex == slider.total - 1 ? 0 : slider.activeIndex + 1;
} else {
prevIndex = slider.activeIndex == 0 ? slider.total - 1 : slider.activeIndex - 1;
}
return item.dataset.index == prevIndex;
});
//set active
const activeItems = [...slider.items]
.filter(item => {
return item.dataset.index == slider.activeIndex;
});
utils().addClasses(prevItems, ['prev']);
utils().addClasses(activeItems, ['active']);
setCurrent();
const activeImageItem = slider.main.querySelector('.active');
activeImageItem.addEventListener('transitionend', waitForIdle, {
once: true
});
}
这个想法是根据我们从HTML获得的数据索引来确定活动幻灯片。让我们解决每个步骤:
- 将滑杆空闲标志设置为false。这表明正在进行幻灯片更改, 并且禁用了滚轮和触摸手势。
- 先前的滑块方向CSS类被重置, 我们检查新的。如果我们来自自动播放功能, 则方向参数默认提供为”下一个”, 或者由用户调用的功能–wheelControl或touchControl提供。
- 基于方向, 我们计算活动的滑动索引并向滑动器提供当前方向CSS类。该CSS类用于确定将使用哪种过渡效果(例如, 从右到左或从左到右)
- 幻灯片使用另一个实用程序功能来重置其”状态” CSS类(上级, 活动), 该实用程序功能删除了CSS类, 但可以在NodeList上调用, 而不仅仅是单个DOM元素。之后, 只有先前和当前处于活动状态的幻灯片才将这些CSS类添加到它们中。这允许CSS仅将那些幻灯片作为目标并提供足够的过渡。
- setCurrent是一个回调, 它根据activeIndex更新滑块指示器。
- 最后, 我们等待活动图像幻灯片的过渡结束, 以触发waitForIdle回调, 如果该回调先前已被用户中断, 则该回调将重新启动自动播放。
用户控件
根据屏幕尺寸, 我添加了两种类型的用户控件-滚轮和触摸。车轮控制:
const wheelControl = function () {
slider.hero.addEventListener('wheel', e => {
if (slider.idle) {
const direction = e.deltaY > 0 ? 'next' : 'prev';
stopAutoplay();
changeSlide(direction);
}
});
}
在这里, 我们甚至听轮盘, 并且即使滑块当前处于空闲模式(当前不为幻灯片更改提供动画效果), 我们也可以确定轮盘的方向, 调用stopAutoplay停止自动播放功能(如果正在进行), 然后根据方向更改幻灯片。 stopAutoplay函数不过是一个简单的函数, 该函数将我们的autoplay标志设置为false值, 并通过调用cancelRequestInterval实用程序函数并将其传递给适当的句柄来取消间隔:
const stopAutoplay = function () {
slider.autoplay = false;
utils().clearRequestInterval(slider.handle);
}
与wheelControl相似, 我们有touchControl可以处理触摸手势:
const touchControl = function () {
const touchStart = function (e) {
slider.ts = parseInt(e.changedTouches[0].clientX);
window.scrollTop = 0;
}
const touchMove = function (e) {
slider.tm = parseInt(e.changedTouches[0].clientX);
const delta = slider.tm - slider.ts;
window.scrollTop = 0;
if (slider.idle) {
const direction = delta < 0 ? 'next' : 'prev';
stopAutoplay();
changeSlide(direction);
}
}
slider.hero.addEventListener('touchstart', touchStart);
slider.hero.addEventListener('touchmove', touchMove);
}
我们听两个事件:touchstart和touchmove。然后, 我们计算差异。如果返回负值, 则当用户从右向左滑动时, 我们将切换到下一张幻灯片。另一方面, 如果该值为正, 表示用户已从左向右滑动, 则我们将通过方向为” previous”触发slideChange。在这两种情况下, 自动播放功能都会停止。
这是一个非常简单的用户手势实现。基于此, 我们可以添加上一个/下一个按钮以在单击时触发slideChange或添加项目符号列表以根据其索引直接转到幻灯片。
关于CSS的总结和最终思考
因此, 你可以使用纯CSS / JS方式对具有现代过渡效果的非标准滑块布局进行编码。
我希望你发现这种方法可以用作一种思维方式, 并且在为非常规设计的项目编码时, 可以在你的前端项目中使用类似的方法。
对于那些对图像过渡效果感兴趣的人, 我将在接下来的几行中进行介绍。
如果我们重新访问介绍部分中提供的幻灯片HTML结构, 我们会看到每个图像幻灯片周围都有一个div, 其CSS类为abs-mask。这个div所做的是通过使用overflow:hidden并在与图像不同的方向上偏移它, 将可见图像的一部分隐藏了一定数量。例如, 如果我们看一下上一张幻灯片的编码方式:
&.prev {
z-index: 5;
transform: translate3d(-100%, 0, 0);
transition: 1s $easing;
.abs-mask {
transform: translateX(80%);
transition: 1s $easing;
}
}
上一张幻灯片的X轴偏移量为-100%, 将其移动到当前幻灯片的左侧, 但是, 内部abs-mask div向右平移80%, 从而提供了较窄的视口。这与活动幻灯片具有较大的z索引相结合, 会产生某种覆盖效果-活动图像会覆盖前一个图像, 同时通过移动提供完整视图的蒙版来扩展其可见区域。