讲解:
HTML 的部分
打开 CodePen 编辑器,在 HTML 的部份加入标题。由于它有一个副标题,所以我会用
<p>
标签去装着它。然后加入一个<div>
,id
名为iphone
,里面新增两个<div>
,id
分别是hardware
和ui
;hardware
会用来显示 iPhone 的机身和边框,而ui
则会显示画面里面的内容。
CSS 的部分
先定义一些 CSS,让我们更加清楚知道在做什么。加入
:root
选择器,定义一些变量和样式:
--device-width
是770px
--device-height
是1336px
--ui-width
是640px
基础文字大小是
15px
然后加入
#iphone
选择器,设定position
是relative
,宽度和高度的值从变量中获取。
再加入
#hardware
选择器,设定宽度和高度是100%
,然后背景图片,载入 iPhone 的机身图片,为了支援 Retina Display 高清屏幕,这张图片的尺寸是两倍大小,所以设定background-size
,宽度和高度同样等于变量的设定值。好了,在这里想介绍一个图片遮罩的 CSS 属性,大家会发现现在 iPhone 的四周,圆型边框背后有个黑色背景:
而我发现 Apple 特地准备了一张遮罩图片,画出了 iPhone 的外型,用白色填充,而圆型边框的四周是透明的:
我们可以通过
mask-image
这个属性将遮罩图片套用上去。由于这个 CSS 属性还在试验阶段,所以要加上-webkit
前缀的版本,与背景图片一样,图片是两倍大小,所以同样需要将遮罩的大小设定一下,将mask-size
设定为变量的值,并且加上-webkit
前缀的版本。
现在可以看到遮罩已经成功套用上,iPhone 的黑色背景不见了。至于为何 Apple 不直接将 iPhone 图片的背景设定为透明?我估计是为了优化图片的大小,因为透明底的图片需要用到 PNG 格式,而这么大张的彩色 PNG 图片容量上比起 JPG 会大不少,所以 Apple 就采用了高压缩比的 JPG 格式作为 iPhone 图片,再准备一张只有单色的 PNG 图片来做遮罩,这样的确可以省下不少网络频宽。
好了,介绍完这个属性,可以加入
body
选择器,将背景颜色设定为黑色,margin
设定为0
,然后将网页的内容上下左右置中:
display
设定为flex
flex-direction
设定为column
justify-content
设定为center
align-items
设定为center
min-height
设定为100vh
字体设定为
Helvetica
,再加入一些padding
,设定上下是4rem
,左右是0
。
再设定一下标题的样式,加入
h2
选择器,文字颜色设定为灰色,text-align
设定为center
将它置中,文字大小设定为4.5rem
,文字粗度是600
。然后margin
设定为上下6rem
,左右0
。加入
h2 p
选择器,设定副标题的样式,margin
设定为0
,文字颜色设定为白色就可以了。
框架大置上设定好了,下一步就设定 iPhone 画面的内容。
在
#ui
里面加入一张图片,是画面顶部的 UI,然后就是每一行资料的图片,我会用<ul>
列表去建构它,加入<li>
,内容是一张图片,总共有 7 行。
回到 CSS 的部份,加入
#ui
选择器,position
设定为absolute
,top
设定为0
。然后要将它左右置中,先将left
设定为50%
,再将transform
设定为translateX(-50%)
就可以了。加入
#ui .top-ui
选择器,设定顶部 UI 图片的定位。display
设定为block
,宽度从变量中获取,高度是auto
,然后margin
,设定上方是70px
,左右是auto
,下方是0
。再加一点padding-bottom
,设定为30px
,以及一条下边框线。
来到列表的部份,加入
#ui ul
选择器,将list-style
设定为none
,取消列表的样式,margin
和padding
设定为0
。再设定里面的图片的样式,加入
#ui ul li img
选择器,设定display
是block
,宽度从变量中获取,高度是auto
,margin
设定为上下10px
,左右auto
。padding-bottom
是10px
,然后再加一条下边框线。
现在界面基本上已设定好了,接下来处理动画的部份。
动画的部分
我们先了解一下这个动画的原理,它是在页面卷动的时候,一行一行的由大至小滑入至列表中,先试试在 CSS 中将效果实现出来。
加入
#ui ul li img:hover
选择器,设定transform
为scale(1.8)
,然后opacity
设定为0
。在#ui ul li img
选择器内加入transition: .25s transform ease-in-out;
,现在试试将游标移至其中一行,就可以实现将图片由放大至缩小,回到列表中的效果。
再加一些位移,例如
translateY()
设定为-60px
,效果大致上是这样:
不过这个动画并不是由游标触发,是随着页面卷动时触发,并且可以通过卷动控制动画的进度,所以我会通过 JavaScript 计算目前页面卷动了多少,然后再设定对应行数的
transform
和opacity
。为了简化 JavaScript 对 CSS 样式的设定,我想先在 CSS 这边做一个设定值,用来控制一行动画的进度。
先将
hover
和transition
的设定值移除,然后在 HTML 里面,在第一个li
那里加入style
,设定--progress
是0.5
:
--progress
是我自己定义的 CSS 变量,我想在它的设定值为0
时将scale()
设定为1.8
,以及opacity
设定为0
;而在它的设定值为1
的时候,将scale()
设定为1
,opacity
设定为1
。做到像是一个动画的开关的功能,那么我们在 JavaScript 那边,只需要调整这个--progress
就可以控制动画。在
#ui ul li img
里面,加入transform
,先设定scale()
,将1.8
减去0.8 * var(--progress)
,这样在--progress
为1
的时候,scale
就是1
;而--progress
为0
的时候,scale
就是1.8
了。再来
translateY()
,将-60px
乘以1 \- var(--progress)
,这样在--progress
为1
时,translateY
就是0
;而--progress
为0
的时候,translateY
就是-60px
。再将
opacity
直接设定为--progress
的值,0
的时候会将它隐藏,1
的时候它就会显示。试试调整一下--progress
的值,大家就可以看到这个设定值的具体作用了。
然后将
<li>
上的设定值移除,在#ui ul
里将--progress
初始化为0
就可以了。
JavaScript 的部分
最后,就是用 JavaScript 控制动画。
首先定义一个名为
rows
的常量,将每一个<li>
获取回来,再定义一个名为html
的常量,赋值为document.documentElement
,因为稍后会有几次引用document.documentElement
的。加入
document.addEventListener
,监听scroll
页面卷动,定义一个变量,scrolled
等于html.scrollTop
除以html.scrollHeight
减去html.clientHeight
。这样会得出一个由
0
至1
之间的值,页面卷动到最顶时是0
,卷动到最底时是1
。
接下来要将每一个
<li>
,一个接着一个,将--progress
设定为1
。新增一个
for of
回圈,将每一个<li>
获取出来,由于动画是一个接着一个,而页面卷动了多少可以经由scrolled
获取,所以我们要计算每一个<li>
在0
至1
之间的所属区间。定义一个名为
total
的变量,将1
除以<li>
的总数,然后计算两个值,一个是start
,将total
乘以index
,另一个是end
,将total
乘以index
加1
,通过console.log()
,将每一个<li>
的区间输出一下看看。
有了这些资料,我们可以计算每一个
<li>
的--progress
的值了。定义变量progress
,将scrolled
减start
除以end
减start
;然后如果progress
大于等于1
的话,将progress
设定为1
;少于等于0
的话,就设定为0
。最后,将
progress
的值通过setProperty
设定到<li>
上。
html
<div id="iphone"><div id="hardware"></div><div id="ui"><img src="https://assets.codepen.io/2002878/iphone12-5g_top_ui.jpg" class="top-ui" alt=""><ul><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_01.jpg" /></li><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_02.jpg" /></li><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_03.jpg" /></li><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_04.jpg" /></li><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_05.jpg" /></li><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_06.jpg" /></li><li><img src="https://assets.codepen.io/2002878/iphone12-5g_show_07.jpg" /></li></ul></div>
</div>
CSS
:root {--device-width: 770px;--device-height: 1336px;--ui-width: 640px;font-size: 15px;
}body {background-color: #000;margin: 0;display: flex;flex-direction: column;justify-content: center;align-items: center;min-height: 100vh;font-family: Helvetica;padding: 4rem 0;
}h2 {color: #6e6e73;text-align: center;font-size: 4.5rem;font-weight: 600;margin: 6rem 0;
}h2 p {margin: 0;color: #fff;
}#iphone {position: relative;width: var(--device-width);height: var(--device-height);
}#hardware {width: 100%;height: 100%;background-image: url(https://assets.codepen.io/2002878/iphone12-5g_on_phone.jpg);background-size: var(--device-width) var(--device-height);mask-image: url(https://assets.codepen.io/2002878/iphone12-5g_on_phone_mask.png);-webkit-mask-image: url(https://assets.codepen.io/2002878/iphone12-5g_on_phone_mask.png);mask-size: var(--device-width) var(--device-height);-webkit-mask-size: var(--device-width) var(--device-height);}#ui {position: absolute;top: 0;left: 50%;transform: translateX(-50%);
}#ui .top-ui {display: block;width: var(--ui-width);height: auto;margin: 70px auto 0;padding-bottom: 30px;border-bottom: 1px solid #222;
}#ui ul {list-style: none;margin: 0;padding: 0;--progress: 0;
}#ui ul li img {display: block;width: var(--ui-width);height: auto;margin: 10px auto;padding-bottom: 10px;border-bottom: 1px solid #222;transform: scale(calc(1.8 - (0.8 * var(--progress)))) translateY(calc(-60px * (1 - var(--progress))));opacity: var(--progress);
}
JS
const rows = document.querySelectorAll('#ui ul li')
const html = document.documentElementdocument.addEventListener('scroll', (e) => {let scrolled = html.scrollTop / (html.scrollHeight - html.clientHeight)let total = 1 / rows.lengthfor (let [index, row] of rows.entries()) {let start = total * indexlet end = total * (index + 1)let progress = (scrolled - start) / (end - start)if (progress >= 1) progress = 1if (progress <= 0) progress = 0row.style.setProperty('--progress', progress)}
})