前言
Win10 Metro 相较于前一代完全扁平化风格的 Win8 Metro 在动画效果与交互体验上有了比较大的差异,那么想要实现一个较为逼真的Win10 Metro需要哪些动画效果呢?
真的是Windows 10 Metro吗?
先来看一下这个Demo,看似复杂的交互实现起来其实并不难,下面我们就来拆解一下其中的动画效果以及实现原理吧。
Metro动画效果与实现
1. 3D旋转
Win10 Metro的一个比较显著的特点就是磁贴的3D旋转。
原版
实现
我们将每一个磁贴展示为横截面为正方形的长方体,然后通过css旋转动画来实现3D旋转。
html
<div class="scene">
<div class="box-container">
<div class="front">
</div>
<div class="back">
</div>
<div class="top">
</div>
<div class="bottom">
</div>
<div class="right">
</div>
<div class="left">
</div>
</div>
</div>
在html结构中,scene
节点的主要作用是作为一个场景容器,有了scene
我们便可以对场景中的属性作出一些调整,例如通过调节perspective
,即可增强或者减弱长方体在旋转时的立体感。
box-container
是整个长方体的容器节点,其中包含了长方体的6个面节点front
, back
, top
, bottom
, right
, left
。
css
.scene {
position: relative;
width: 300px;
height: 150px;
perspective: 700px;
}
.box-container {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.5s;
transform-origin: 50% 50% -75px;
}
.front {
background-color: rgba(3,122,241,0.5);
position: absolute;
width: 300px;
height: 150px;
}
.back {
background-color: rgba(241,3,3,0.5);
position: absolute;
width: 300px;
height: 150px;
transform: translateZ(-150px) rotateZ(180deg) rotateY(180deg);
}
.top {
background-color: rgba(3,241,122,0.5);
position: absolute;
width: 300px;
height: 150px;
transform: translate3d(0,-75px,-75px) rotateX(90deg);
}
.bottom {
background-color: rgba(241,241,3,0.5);
position: absolute;
width: 300px;
height: 150px;
transform: translate3d(0,75px,-75px) rotateX(-90deg);
}
.left {
background-color: rgba(270,97,48,0.5);
position: absolute;
width: 150px;
height: 150px;
transform: translate3d(-75px,0,-75px) rotateY(-90deg);
}
.right {
background-color: rgba(30,97,48,0.5);
position: absolute;
width: 150px;
height: 150px;
transform: translate3d(225px,0,-75px) rotateY(90deg);
}
在css中,有以下几点值得注意的地方
.box-container
中使用了transform-style: preserve-3d
来保留子节点在3D变换时的3D坐标。并用了transform-origin: 50% 50% -75px
将.box-container
的旋转原点放在了长方体的中心。- 为了使长方体背面的内容在旋转后不颠倒,在
.back
的定义中我们需要在transform
中额外添加rotateZ(180deg)
预先颠倒背面的内容。 - 为了保持每一面中内容不因为
Z轴
的坐标而放大或者缩小,我们始终保持了当前正对观察者面的Z坐标
为0
。
Demo
2. 倾斜
在点击并按住磁贴的时,磁贴会出现一个向点击位置倾斜的动画。
原版
实现
如果仔细观察原版会发现,面的倾斜角会随着按压位置的不同而产生不同的变化。当按压位置靠近磁贴边缘时,倾斜角会变大一些;而当按压位置靠近磁贴中心时,倾斜角会随之减小。总结以上的规律我们可以得出:
以磁贴的中心作为坐标系原点(0, 0),当点击位置为(x, y)时,X轴上的倾斜角Ɵx ∝ |y|,而Y轴上的倾斜角 Ɵy ∝ |x|。
html
<div class="container">
<div class="tile">
<span>Hello World</span>
</div>
</div>
css
.container {
width: 200px;
height: 200px;
perspective: 700px;
}
.tile {
background-color: #2d89ef;
width: 100%;
height: 100%;
transition: transform 0.5s;
text-align: center;
color: white;
}
js
const maxTiltAngle = 30; // 设置最大倾斜角度
const container = document.getElementsByClassName('container')[0];
const tile = document.getElementsByClassName('tile')[0];
const boundingRect = container.getBoundingClientRect();
const tilt = event => {
// 计算鼠标相对于容器的位置
const relativeX = event.pageX - (boundingRect.left + window.scrollX);
const relativeY = event.pageY - (boundingRect.top + window.scrollY);
// 将原点从容器左上角移至容器中心
const normalizedX = relativeX - boundingRect.width / 2;
const normalizedY = -(relativeY - boundingRect.height / 2);
// 计算倾斜角
const tiltX = normalizedY / (boundingRect.height / 2) * maxTiltAngle;
const tiltY = normalizedX / (boundingRect.width / 2) * maxTiltAngle;
// 倾斜
tile.style.transform = `rotateX(${tiltX}deg) rotateY(${tiltY}deg)`;
}
const recover = () => {
// 恢复倾斜
tile.style.transform = '';
}
container.addEventListener('mousedown', tilt);
container.addEventListener('mouseup', recover);
container.addEventListener('mouseleave', recover);
在倾斜的实现上,有以下几点需要注意的地方
- 以长为L宽为W的磁贴中心作为坐标系原点(0, 0),当点击位置为(x, y)时,实现中使用的公式为:X轴上的倾斜角Ɵx = y / (W / 2) * Ɵmax,而Y轴上的倾斜角 Ɵy = x / (L / 2) * Ɵmax。
- 仅在mouseup事件中恢复倾斜是不够的,在mouseleave的时候也需要恢复倾斜。
Demo
3. 悬停光晕
当鼠标悬停在磁铁上时,磁贴上会有一个跟随鼠标移动的光圈。
原版
实现
光晕从中心至外围颜色渐渐淡化,光晕中心的位置会随着鼠标的移动而移动。
html
<div class="container">
<div class="hoverLayer">
</div>
<div class="hoverGlare">
</div>
</div>
css
.container {
position: relative;
background-color: #000;
width: 200px;
height: 200px;
overflow: hidden;
}
.hoverLayer {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
}
.hoverGlare {
position: absolute;
background-image: radial-gradient(circle at center, rgba(255,255,255, 0.7) 0%, rgba(255,255,255,0.1) 100%);
transform: translate(-100px, -100px);
width: 400px;
height: 400px;
opacity: 0.4;
}
js
const boundingRect = document.getElementsByClassName('container')[0].getBoundingClientRect();
const hoverGlare = document.getElementsByClassName('hoverGlare')[0];
const glare = event => {
// 计算鼠标相对于容器的位置
const relativeX = event.pageX - (boundingRect.left + window.scrollX);
const relativeY = event.pageY - (boundingRect.top + window.scrollY);
// 将原点从容器左上角移至容器中心
const normalizedX = relativeX - boundingRect.width / 2;
const normalizedY = relativeY - boundingRect.height / 2;
// 调整光晕透明度及位置
hoverGlare.style.opacity = 0.4;
hoverGlare.style.transform = `translate(${normalizedX}px, ${normalizedY}px) translate(-${boundingRect.width / 2}px, -${boundingRect.height / 2}px)`;
}
const resetGlare = () => {
// 隐藏光晕
hoverGlare.style.opacity = 0;
}
const hoverLayer = document.getElementsByClassName('hoverLayer')[0];
hoverLayer.addEventListener('mousemove', glare);
hoverLayer.addEventListener('mouseleave', resetGlare);
在光晕的实现上,有以下几点需要注意的地方
- 我们使用了
z-index
为1
的.hoverLayer
来当作鼠标事件节点,避免因为子节点覆盖父节点而产生鼠标定位不准确的问题。 - 我们创建了一个2倍于容器宽高的光晕悬浮层,并通过
translate
移动这个悬浮层来实现高效率的光晕位置变换。
Demo
4. 点击波纹
当鼠标点击磁贴时,在点击位置会形成圆形向外扩散的波纹动画
原版
实现
我们可以看到圆形的波纹由点击位置开始向外渐渐扩散直至消散。
html
<div class="tile">
<div class="clickGlare">
</div>
</div>
css
.tile {
position: relative;
width: 200px;
height: 200px;
background-color: #000;
overflow: hidden;
}
.clickGlare {
position: absolute;
width: 90px;
height: 90px;
border-radius: 50%;
opacity: 0;
filter: blur(5px);
background-image: radial-gradient(rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0) 100%);
}
.ripple {
animation-name: ripple;
animation-duration: 1.3s;
animation-timing-function: ease-in;
}
@keyframes ripple {
0% {
opacity: 0.5;
}
100% {
transform: scale(5);
opacity: 0;
}
}
js
const tile = document.getElementsByClassName('tile')[0];
const boundingRect = tile.getBoundingClientRect();
const clickGlare = document.getElementsByClassName('clickGlare')[0];
const ripple = event => {
// 仅当节点的class中不含ripple时执行
if (clickGlare.classList.contains('ripple')) return;
// 计算鼠标相对于容器的位置
const relativeX = event.pageX - (boundingRect.left + window.scrollX);
const relativeY = event.pageY - (boundingRect.top + window.scrollY);
// 根据鼠标位置调整波纹的中心位置
clickGlare.style.top = `${relativeY - 45}px`;
clickGlare.style.left = `${relativeX - 45}px`;
// 添加波纹动画
clickGlare.classList.add('ripple');
}
const resetRipple = () => {
// 移除波纹动画
clickGlare.classList.remove('ripple');
}
tile.addEventListener('mousedown', ripple);
clickGlare.addEventListener('animationend', resetRipple);
在点击波纹的实现上,有以下几点需要注意的地方
- 为了使得波纹动画更加近似于原版,我们使用了
filter: blur
这一条css,这条css可能会引起老版本浏览器或者IE/Edge中的兼容性问题。
Demo
小结
如果将以上的动画结合起来,我们就可以实现一个比较逼真的Windows 10 Metro 布局了。看上去复杂的Windows 10 Metro,是不是其实挺简单的呢?
Windows 10磁贴中包含的动画拆解开来都是一些比较常见的、能提升用户体验的动画。大家也可以在平时的项目或者工作中尝试去模仿一下这些简易的动画,来使得交互与设计更加得友好。
最后,我也使用Vue做了一个小组件,方便大家在Vue中实现Win 10 Metro布局,欢迎各位的交流与讨论~