苹果的官网一直是引领者前端网页效果的发展,本文对苹果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距离顶部的距离就可以得到滚动了多少尺寸,通过比值计算就可以得到滚动的百分比,得到当前滚动到了百分之多少的位置,假设提前定义好了不同的百分比距离的样式,那么根据这个就可以计算该设置的样式了,先看一张图

iOS 时间轴布局 苹果自带时间轴_关键帧

 红色圈起来的就是我的组件的配置项,我定义了三个元素的不同节点的时候该展示什么样的样式,专业叫法叫关键帧,比如像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.com.cn/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使用,效果基本还原一致,在动画框架中还没看到相似功能的,