原文链接: https://cssanimation.rocks/portal/
图片或视频如果无法正常查看,可结合原文链接。
你不一定必须使用javascript
才能在浏览器端构建惊心动魄的3D
效果的项目。在这篇文章中,我将介绍一种纯CSS
实现的复杂的动画效果。
传送门在线演示地址,Github
源码地址
视频展示了3D
动画场景的构建,美化及动画效果。这篇文章我们将使用CSS3
重绘这个传送门视频。重点介绍人物从一个门穿到另一个门的过程。下面是原始视频:
这个视频在第一次发布的时候,就深深的印在了我的脑海中。传送门(创始人 Narbacular Drop )介绍了一种有趣的3D平台解谜游戏。
在视频中,我们看到这个游戏获得战利品的方式,不同于以往的游戏。文章里,我将尝试看能否只使用HTML和CSS来重现这个场景。
下面的图片就是我们打算要创造的场景。
推荐你使用prefix free
或者 SASS
来构建 CSS
,可以避免在编写CSS
的时候,重复的编写前缀。另外,要注意一下,在不同的浏览器中,兼容性前缀的问题。完整的源码,CSS
、SASS
还有HTML
,都可以在Github上找到。
我们的项目是在Chrome来进行开发和测试的,并不兼容Internet Explorer
浏览器。代码中包含的一些有趣的3D动画技术并不是现在的主流,但对于未来的前端开发有着它不可忽视的意义。
我们先在HTML
中创建一个容器,来告诉浏览器我们想把我们的3D
场景的内容放在这里。
<article class="container">article>
在这里我们使用了HTML5
中的article
标签,来表示这是独立的一段内容。
我们要提到的第一个属性是 perspective
。这个属性的值用像素值来表示3D
场景的景深。通常的设定范围是800px到1200px。
这个场景给我们感觉很像是一个大房子,我们设置他的perspective
值为 2600px,忽略前缀,写法如下:
article.container {
perspective: 2600px;
}
这个容器有一个Z轴景深,我们需要决定一下它的透视点。通过设置perspective-origin
属性,来设置我们的消失点,决定我们是从上向下看,还是从某一边看去。
.container {
perspective-origin: 50% -1400px;
}
perspective-origin
这个属性可以设置两个值,水平和垂直方向。这里我们设置了水平方向为50%,垂直方向向上1400像素。相当于我们是在物体上方从下看。
我是使用Chrome
浏览器的检查元素和肉眼来调整和决定这个值的。当你设置了你想要的场景画面,你的值可能会比这个数值要高或者低。这决定于你想要表达的效果。另外,需要谨记,这个值会影响动画效果和其他一些有趣的视觉角度转换(perspective-change effects)。
HTML
中的对象是很普通的HTML
元素,他们是矩形的,有宽和高。这就意味着你所创建3D
对象,需要把这些矩形放在该有的位置上。这种方式不同于由点到线,再到图形的方法。换种说法,也就是说,我们无法使用圆,立方体来画。
3D
场景中HTML
元素的摆放需要用到 transform
属性。
transform
可以实现对HTML
元素的一系列的调整。比如,移动元素,旋转角度, 倾斜或放大缩小。这些变形可以叠加使用。
.example {
transform: rotateY(45deg) translateZ(-100px);
}
这段代码的意思是将元素沿着Y轴顺时针旋转45度,沿Z轴向后100像素。效果如图:
当你想旋转元素的时候,需要设置一个起始点,我们用 transform-origin
属性来实现,可以传三个参数,分别是X轴,Y轴,Z轴。
.default-origin {
transform-origin: 50% 50% 0;
}
目前的例子里,我没有特别设置,保留了原有的默认值,但我们需要知道有这么个属性。
结合效果图,我们拼合属于我们自己的3D
杰作。先用HTML
和CSS
来构建我们的3D
对象。我们有必要花一分钟的时间来理解一下我们这种方法和其他3D
软件的区别。
<section class="stage">
<div class="shadow">div>
<div class="back-left">div>
<div class="back-right">div>
<div class="platform-left"><span>span>div>
<div class="platform-right"><span>span>div>
<div class="pit-left">div>
<div class="pit-right">div>
<div class="pit-back">div>
section>
stage
用来装所有的元素,div
表示整体中的某一部分结构,将场景中的墙,平台,凹陷的地方,以及阴影部分,拼成我们想要的。
当我开始构建这个图形的时候,我尝试把墙直接放在平台上,通过旋转来调整他的位置,可是图形看上去是等距的。更简单一点的方法实现就是,先放置每一个部分,然后将整体倾斜45度。
带着这个想法,HTML
就变成了下面这种平面图。
如图所示,左后与左侧对齐,右后与右侧对齐。为了达到这个效果,稍后我们整体旋转45度。
.stage div {
position: absolute;
transform-style: preserve-3d;
}
在使用变形之前,我们先给div
添加公共的属性。
.stage div {
position: absolute;
transform-style: preserve-3d;
}
每个div
都设置为绝对定位,事先将transform-style
设置为 3D
视图。
接下来我们就可以单独设置div
的大小和位置了。
.stage .back-left {
background-color: #6b522b;
border-left: 6px solid #574625;
border-top: 6px solid #8a683d;
height: 120px;
transform: rotateY(90deg) translateX(-256px);
width: 500px;
}
以上的规则描述了3D
场景中的一个500像素宽,120像素高的浅棕色的一面墙。div
沿着Y
轴旋转90度,沿着X
轴向后推,通过6像素的边框创造一个有景深的错觉。
back-right
也设置类似的属性。
.stage .back-right {
background-color: #9c7442;
border-right: 6px solid #78552c;
border-top: 6px solid #b5854a;
height: 120px;
transform: translateX(253px) translateZ(3px);
width: 446px;
}
这个div
要更小一点,像视频中的那样,不那么方。
接下来,加一些平台,和凹陷。
.stage .platform-left {
background-color: #bcb3a8;
border-bottom: 6px solid #857964;
height: 220px;
transform: rotateX(90deg) translateY(396px) translateX(253px) translateZ(-13px);
width: 446px;
}
.stage .platform-right {
background-color: #bcb3a8;
border-bottom: 6px solid #847660;
border-right: 6px solid #554c3d;
height: 164px;
transform: rotateX(90deg) translateY(88px) translateX(253px) translateZ(-41px);
width: 446px;
}
.stage .pit-left {
background-color: #4d4233;
height: 800px;
transform: translate3D(254px, 125px, 285px);
width: 447px;
}
.stage .pit-right {
background-color: #847660;
height: 800px;
top: -1400px;
transform: translate3D(254px, 125px, 173px);
width: 451px;
}
.stage .pit-back {
background-color: #6b522b;
height: 220px;
transform: rotateY(90deg) translate3D(-200px, 87px, 168px);
width: 170px;
}
目前我们的图形如下:
目前和目标图形不一样,我们还需要把它旋转一下,给section
增加一个transform
的属性。
.stage {
margin: 0 auto;
transform-style: preserve-3d;
transform: rotateY(-45deg);
width: 460px;
}
效果如下:
你可能注意到了边框有一个很漂亮的景深的效果,特别是在边角的地方,颜色不同,有45度的旋转。
因为我们是斜45度看过去的,所以这个效果是很可能出现的。有个别的角落看着有出入,但考虑到我们直接用的边框,没有引用图片,有这个瑕疵,是可以忍受的。
视频里,平台下面有一个很漂亮的阴影。我们可以使用CSS
的属性 box-shadow
来实现。
.stage .shadow {
background-color: transparent;
box-shadow: -600px 0 50px #afa79f;
height: 550px;
transform: rotateX(90deg) translateZ(-166px) translateX(550px);
width: 550px;
}
给div
加了一个盒阴影,阴影本身是透明的,与实体偏离600像素,整体偏离了stage
,所以是可见的。效果如下:
接下来我们添加一些装饰和鲜艳的门。
两个入口使用HTML
来表示。
<div class="portal red">div>
<div class="portal blue">div>
两个div
,一个表示红色的门,一个表示蓝色的门。两个都分别设置一些过渡色来实现发光的效果。
因为只用了一个div
元素,所以我们可以尝试使用CSS
伪类来实现整体效果。
第一步,先创建一个门的形状。
.stage .portal {
background-color: black;
border-radius: 44px/62px;
box-shadow: 0 0 15px 4px white;
height: 72px;
width: 48px;
}
用 border-radius
属性实现椭圆形,白色阴影实现发光的效果。同时我们给伪类也定义相同的大小
和边框。
.stage .portal:before {
border-radius: 44px/62px;
border: 4px solid white;
content: "";
display: block;
height: 72px;
margin-left: -4px
margin-top: -4px;
width: 48px;
}
门的整体效果已经设置完了,之后我们需要分别设置蓝色的门和红色的门。
首先,红色的门:
.stage .portal.red {
background: radial-gradient(#000000, #000000 50%, #ff4640 70%);
border: 7px solid #ff4640;
transform: translate3D(223px, 25px, 385px) rotateY(90deg) skewX(5deg);
}
.stage .portal.blue {
background: radial-gradient(#000000, #000000 50%, #258aff 70%);
border: 7px solid #258aff;
transform: translate3D(586px, 25px, 4px) skewX(-5deg);
}
红色的门,我们给了一个放射性渐变的颜色和一个红色边框。通过transform
将其放置到左侧墙上。蓝色的门类似,放置到右侧墙上。测试的时候,两个看起来都有一些错位,所以我给他们加了5度的倾斜。
早先,我们就在两个平台里分别放了一个span
标签。我们可以利用这个span
来定义每个门下面的照射到平台上的光。
.stage .platform-left span {
background: radial-gradient(left, #f3cac8, #c8b8ad 70px, #bcb3a8 90px);
display: block;
height: 200px;
left: 0;
position: absolute;
width: 120px;
}
.stage .platform-right span {
background: radial-gradient(top, #cdebe8, #c2cbc1 40px, #bcb3a8 60px);
display: block;
height: 60px;
left: 280px;
position: absolute;
width: 150px;
}
两个span
都被绝对定位在门下方,一个红色光,一个蓝色光。其实伪元素也可以实现这个效果,
但是伪元素对动画的支持不好。
在右侧的墙上,需要一个门表示退出。你可能想像不到我们用边框就实现了这个效果。一个的div
和有颜色的边框做了一个内凹的效果。
门的HTML
放在stage
里。
<div class="door">div>
给门设置几个边框,然后定位到右墙上。
.stage .door {
background: #efe8dd;
border-bottom: 6px solid #bcb3a8;
border-left: 7px solid #78552e;
height: 85px;
transform: translate3D(450px, 34px, 4px);
width: 65px;
}
下边框和左边框与平面和右面墙重合,有一种景深的感觉。不设置上边框,左边框立在左侧,展示的效果也很好。
有了传送门之后,我们需要有一个人物可以从一边到达另一边。第一步,先画一个人物。
刚开始的时候,我尝试了让一个人物从传送门进去,然后动画停止,立即从另一边继续。但当我让人物停止,在传送门之间移动的时候,会有闪烁。为了解决这个问题,我使用了2个人物,分别让他们移动。
人物主要两部分组成,头部和身体。腿是用身体的伪类创建的。同理,创建一个人物的影子。
<div class="dude one">
<figure class="head">figure>
<figure class="body">figure>
<div class="dude-shadow one">
<figure class="head">figure>
<figure class="body">figure>
div>
div>
因为影子是人物的一部分,所以要同时移动。CSS
如下:
.dude, .dude-shadow {
height: 100px;
width: 30px;
}
.dude figure, .dude-shadow figure {
background-color: black;
display: block;
position: absolute;
}
.dude figure.head, .dude-shadow figure.head {
border-radius: 22px;
height: 20px;
left: 3px;
top: 0;
width: 20px;
}
.dude figure.body, .dude-shadow figure.body {
border-radius: 30px 30px 0 0;
height: 30px;
top: 21px;
width: 26px;
}
.dude figure.body:before, .dude figure.body:after, .dude-shadow figure.body:before, .dude-shadow figure.body:after {
background-color: black;
content: "";
height: 15px;
position: absolute;
top: 30px;
width: 9px;
}
.dude figure.body:before, .dude-shadow figure.body:before {
left: 3px;
}
.dude figure.body:after, .dude-shadow figure.body:after {
left: 14px;
}
我们用这些样式来描述人物和影子。每个部分是绝对定位,border-radius
来设置圆角。腿用伪元素来定义,然后分别定义位置。
人物构建好了之后,将其定位到开始的位置。
.stage .dude.one {
transform: translate3D(514px, 50px, 375px) rotateY(78deg);
}
.stage .dude-shadow.one {
opacity: 0.1;
transform: translateX(-12px) rotateX(90deg) translateY(8px);
}
CSS
位置的变换既包含人物本身,也包含人物的影子,人物的透明度设置为0.1, 而不是设置成灰色,
这样做的好处是允许你看见影子后面的背景细节。
第一个人物现在在初始位置,参考视频,我们将人物旋转了一个相同的角度。稍后我们会加一些特效让他穿过传送门。
第二个人物有更多的细节,手, 这个想法是人物穿越传送门时产生的,象征着他们举起手来庆祝。
以下是HTML
代码:
<div class="dude two">
<figure class="head">figure>
<figure class="body">figure>
<figure class="arm left">figure>
<figure class="arm right">figure>
<div class="dude-shadow two">
<figure class="head">figure>
<figure class="body">figure>
<figure class="arm left">figure>
<figure class="arm right">figure>
div>
div>
第二个人物开始是透明的动画效果,然后在第一个人物动画效果开始的中途穿过传送门(第一个人物到达传送门),设置第二个人物定位在传送门。
.stage .dude.two {
transform: translate3D(610px, 40px, 10px) rotateY(15deg);
}
.stage .dude.two figure.arm {
background: black;
height: 8px;
position: absolute;
top: 20px;
width: 20px;
}
.stage .dude.two figure.arm.left {
left: -13px;
transform: rotateZ(40deg);
}
.stage .dude.two figure.arm.right {
right: -10px;
transform: rotateZ(-40deg);
}
.stage .dude-shadow.two {
opacity: 0.1;
transform: translateY(12px) translateX(-16px) translateZ(-6px) rotateZ(-90deg) rotateY(90deg) rotateZ(50deg) skewX(30deg) scaleX(0.8);
}
第二个人物将会赋予他手, 手一开始是消失的,但是稍后会出现。
当人物和背景都在出现在相应的位置, 这个场景已经准备好开始一些动画效果
让我们看看是怎么实现让它看起来像一个小人从第一个传送门到达第二个门。
如果你看了示例,您会看到几个动画。但我将重点放在人物穿过门的动画,没有讲述所有的动画实现。
通过使用keyframes
来实现HTML的关键帧动画。然后用animation
来执行一系列的关键帧动画。
第一件事是让人物从左边的传送门进入。下面是第一组关键帧:
@keyframes move-dude-one {
/* Character flies into scene */
0% {
transform: translate3D(514px, -10px, 375px) rotateY(78deg) scaleY(2);
}
/* Waits a moment */
1%, 18% {
opacity: 1;
transform: translate3D(514px, 50px, 375px) rotateY(78deg) scaleY(1);
}
/* Moves toward the portal */
34%, 39% {
opacity: 1;
transform: translate3D(284px, 40px, 375px) rotateY(78deg);
}
/* Pauses, them jumps in */
41%, 42% {
opacity: 1;
transform: translate3D(234px, 40px, 375px) rotateY(78deg);
}
/* Vanishes */
43%, 100% {
opacity: 0;
transform: translate3D(234px, 40px, 375px) rotateY(78deg);
}
}
/* Note: Use prefixes, such as @-webkit-keyframes, @-moz-keyframes, etc! */
关键帧是一系列的步骤,使用百分比来描述。百分比的设置会涉及到动画的时间。那么,如果一个动画持续10秒,10%设置为1秒。90%设置为9秒。
为了让人物完美的穿过传送门,我们每隔10秒设置一个动画。关于每个阶段的动画,我在代码中写了注释。
每个阶段都用了transform
属性来设置人物的位置和角度。43%的时候,动画将人物的不透明度设置为0。这时将第一个人物在传送门消失。第二个人物出现。在我们这么做之前,让我们先把第一个动画加到第一个人物上:
.dude.one {
animation: move-dude-one 10s linear infinite;
opacity: 0;
}
将animation
属性应用到了due one
元素上。设置了动画名称,动画时长10s,无限循环播放。为了确保角色在动画开始之前是隐藏的,不透明度设置成0。
完成之后,让我们为第二个人物进行相应的设置:
@keyframes move-dude-two {
/* Dude be invisible */
0%, 42% {
opacity: 0;
transform: translate3D(610px, 40px, 10px) rotateY(15deg);
}
/* Apparato! */
42.5% {
display: block;
opacity: 1;
transform: translate3D(610px, 40px, 10px) rotateY(15deg);
}
/* Move onto the platform */
46%, 75% {
opacity: 1;
transform: translate3D(610px, 40px, 120px) rotateY(15deg);
}
/* Stand there for a bit */
76%, 97% {
opacity: 0;
transform: translate3D(610px, -10px, 120px) rotateY(15deg) scaleY(2);
}
/* Fly up into the sky! */
98%, 100% {
opacity: 0;
transform: translate3D(610px, -10px, 120px) rotateY(15deg) scaleY(2);
}
}
@keyframes arms {
/* No arms */
0%, 53% {
opacity: 0;
}
/* Yes arms! */
54%, 100% {
opacity: 1;
}
}
按计划,动画在42%的时候开始。这个人物穿过传送门,站了一会儿,然后飞向天空。
第二组关键帧动画描述它的手。手开始是看不见的,然后举了起来。
我们给第二个人物添加了如下的动画:
.dude.two {
animation: move-dude-two 10s linear infinite;
opacity: 0;
}
.dude.two figure.arm {
animation: arms 10s linear infinite;
opacity: 0;
}
两个动画设置完了,由于需要很好的配合第一个动画,都设置了持续10秒,无限循环。
如果你还没有完成,可以在主流浏览器中打开演示示例做参考,不过最好不要是Internet Explorer浏览器。
谈到浏览器兼容性的时候,需要注意的是,目前这个示例不兼容Internet Explorer
。
Firefox
有点瑕疵,但也还可以。Safari
(苹果对webkit
支持补丁之后)和chrome
几乎是100%支持的。
抛开浏览器,在不同设备之间的展示效果。测试结果显示,iphone
上的Safari
比PC
上的chrome
效果要好。因为3D transforms
的CSS
属性,需要依赖于图形硬件。
查看在线门户CSS演示或从Github下载源代码。