苹果的官网一直是引领者前端网页效果的发展,本文对苹果mac book的宣传页面前端实现做一个实现步骤的解析和复现 使用框架 react ts
首先看原网页效果:
苹果macair网页效果
首先观察页面,随着页面滚动,开头一个标题文字逐渐放大,放到最大之后标题消失然后出现笔记本的元素随着滚动逐渐打开,然后出现笔记本文字,注意: 这些元素没有随着滚动而往下动,而是吸顶定位,ok 现在页面大概方式大概理完了,开始下一步
笔记本和标题为什么滚动的时候没有跟着滚动?
直接讲实现方式吧, position: sticky; top:0 粘性布局, 最外层的总div设置的非常高,比如800vh ,就是八个屏幕的高度,内部的元素都堆叠在一起控制显示和隐藏,然后内部的这些元素的小div就是大概100vh的高度 设置粘性布局,top:0,这个时候就发现滚动的时候里边的div就固定在中间了,等大div 800vh滚动结束的时候内部div才会随着滚动,这个时候发现整体的布局就结束了,进行下一步 , 操作元素的显示隐藏等动画时间轴
操作元素的时间轴动画
上一步已经布局好了,假设布局中共计三个子元素,包含一个h1标题,一个笔记本元素,一个笔记本名称,这三个元素需要在不同的滚动节点出现,并可以设置不同的样式,并且随着滚动的距离控制动画的进度,并且支持回滚等操作,一听这就是十分复杂的,其中笔记本还随着滚动有开合的效果,先告诉大家笔记本是怎么实现的
笔记本随滚动的开合效果: 笔记本是个视频,滚动的时候根据滚动距离设置视频的当前帧时间定位,属性是 currentTime ,操作方式就是 dom.currentTime = * 就行
那现在开始思考如何让这三个元素紧密衔接进行动画操作?假设整个div的800vw是个时间轴 共计100%,那么监控这个div距离顶部的距离就可以得到滚动了多少尺寸,通过比值计算就可以得到滚动的百分比,得到当前滚动到了百分之多少的位置,假设提前定义好了不同的百分比距离的样式,那么根据这个就可以计算该设置的样式了,先看一张图
红色圈起来的就是我的组件的配置项,我定义了三个元素的不同节点的时候该展示什么样的样式,专业叫法叫关键帧,比如像h1,滚动到0%的时候我定义透明度 0.1,大小 1倍,滚动到15%的时候透明度1,大小一倍,讲到这里可能你稍微有了一点想法,我把元素的变化的关键帧都配置好,通过id(有点low用id)去进行关联元素,这样的话我再拿到当前滚动的百分比,我就可以判断当前滚动到了那个关键帧,假设滚动到了 9%,那么我就循环所有的配置项,一个一个的找到元素9%的时候该是什么样式,比如h1元素,就是在 0% 关键帧到 15%关键帧之间,这里有点复杂需要理解下,就是获取到两个前后关键帧才能计算他们的样式该是什么,下文成为前后帧,9% 在1 到 15 这俩前后帧中处在 60%(这里怎么计算需要自己感悟,9 / 15 / 100 = 60 %),百分之60再计算前后帧之间样式的差,比如 0帧的时候透明度 0.1 15关键帧透明度1,那么前后帧的透明度差就是0.9 再 * 计算出的所处位置 60% 就等于 0.54 透明度,然后根据id修改元素的样式就行了,这里的计算方法是核心复杂点,需要好好理解消化,计算方法已经写成了通用的组件,可以看下文
总结
看的云里雾里的,再总结梳理一下,定义好时间轴配置,每个元素到哪个节点该干什么,然后求div滚动的百分比得到滚动到哪了,然后根据滚动到的位置百分比得值去求时间轴配置里边找关键帧,然后找到前后关键帧之后计算该设置什么样式,设置样式就行了,看不懂的话就直接拿下边代码直接用吧,react 函数式写法,ts,支持常见样式变化
最终通用组件代码
import React, { cloneElement, useEffect, useRef, useState } from "react"
import './index.less'
import { isBrowser } from "@utils/util"
export default ({children, config, ...rest}:any)=> {
const [refList, setRefList] = useState({})
const { props } = children || {}
const { children:childrenList, ref, ...rest1 } = props || {}
const allRef = ref ? ref : useRef(null) as any
const timeRef = useRef(null)
// 6,在这里计算前后帧和滚动百分比,得到该设置的值
const handleStyle = (dom: HTMLElement | null, endStyle: any, beginStyle: any, value: number)=> {
const endStyleList = Object.keys(endStyle) as any
endStyleList.forEach((key:any , index: number)=> {
const itemStyle = endStyle[key]
if(key === 'scale'){
dom.style.transform = `scale(${(endStyle[key] - beginStyle[key])*value / 100 + beginStyle[key]})`
} else if(key === 'top'){
dom.style.transform = `translateY(${(endStyle[key] - beginStyle[key])*value / 100}px)`
} else if(key === 'currentTime'){
requestAnimationFrame(()=> dom[key] = `${(endStyle[key] - beginStyle[key])*value / 100 || value}`)
} else {
dom.style[key] = `${(endStyle[key] - beginStyle[key]) * value / 100 + beginStyle[key]}`
}
});
}
const handleScroll = () => {
// 2, 计算出接收的组件的位置高度距离信息
// 动画容器距离网页顶部的滚动距离
const { top } = allRef.current.parentNode.getBoundingClientRect() || {top: 0}
// 网页可视高度
const viewHeight = document.body.clientHeight
// 动画容器的高度 + 0.5页面高度为了优化结束的时候的效果
const all_height = isBrowser() ? allRef.current?.parentNode.scrollHeight + 0.5 * viewHeight: 1200 //页面显示区域总高度
// 计算动画容器距离底部网上滚动的多少
const top_value = -top + viewHeight
// 根据动画元素容器滚动的距离计算出已经滚动距离的百分比
let proportion = 100 - (all_height - top_value)/all_height * 100
const proportionValue = proportion <= 0 ? 0 : (proportion >= 100 ? 100 : proportion)
// 3,遍历接收的子节点,有配置时间轴的话就去做动画处理
childrenList.forEach((element:any) => {
const { id } = element.props
let configList = config[id]
let dom = document.getElementById(id)
if(configList){
const config = Object.keys(configList) as any
for (let index = 0; index < config.length; index++) {
const key = config[index];
const lastKey = config[(index === 0 || (index === config.length-1 && proportionValue >= config[index])) ? index : index - 1 ]
if( Number(key) >= proportionValue || index === config.length -1){
//
// 4, 计算当前阶段走到的比例值
const now_value = key === lastKey ? 100 : (1 - (key - proportionValue) / (key - lastKey)) * 100
// requestAnimationFrame(()=> handleStyle(dom, configList[key], configList[lastKey], now_value ))
// 5, 定义的设置dom样式的方法,传进去dom 前后帧,当前的滚动位置百分比
handleStyle(dom, configList[key], configList[lastKey], now_value )
break
}
}
} else {
return
}
});
}
useEffect(()=> {
// 1 , 设置滚动监听事件 约17毫秒一次,
handleScroll()
isBrowser() && window.addEventListener('scroll', handleScroll);
return (()=> {
isBrowser() && window.removeEventListener('scroll', handleScroll)
})
}, [])
return <div {...rest1} ref={allRef}>
{
childrenList.map((element: any, index: number) => {
const { className, style, ...rest } = element.props
return cloneElement(element,{...element.props, className: className, } )
})
}
</div>
}
使用方法
<div className="test223">
<RollAnimation
config={{
title: {
0: {opacity: 0.1,scale:1 },
15: {opacity: 1,scale:1},
40: {opacity: 1,scale:260},
45: {opacity: 1,scale:340},
50:{opacity: 1,scale:580.5},
51:{opacity: 0,scale:595},
},
video: {
0: {opacity: 0, currentTime:0},
50:{opacity: 0,currentTime:2.5},
51:{opacity: 1,currentTime:2.52},
100:{opacity: 1, currentTime:5.5},
},
span1: {
49:{scale:0, opacity: 0,},
65: {scale:1 , opacity: 0,},
72: {scale:1, opacity: 1,},
80 : {scale:1, opacity: 0,},
},
}}
>
<div className="video">
<h1 id="title" className="test">华为云服务</h1>
<div id="span1">啊</div>
<video id="video" ref={myVideo} src={'https://www.apple.***.***/105/media/us/macbook-air-13-and-15/2023/f52c7a72-dff4-4f3c-9511-bf08e46c6f5f/anim/hero/large.webm'} ></video>
</div>
</RollAnimation>
</div>
注意最外层div设置的要高一点,内部div定位方式要设置一下,拿过去就可以直接 react使用,效果基本还原一致,在动画框架中还没看到相似功能的,代码比较烂,仅供参考