前言
Canvas学了就想用,前两天写了Canvas实现数字雨和放大镜效果,感觉还要再练练手,今天来实现一下苹果官网的充电盒动效。后面有完整代码。
正文
还是先看看最终的效果,实现的原理也很简单。动态效果是一个视频,我们只要根据页面滚动的距离去计算当前播放的时间,再绘制到画布上就好了
,剩下就是对细节的处理:
- 滚动页面时如何将Canvas固定
- 对当前时间的计算
- 滚动一定距离后,如何与当前的页面做衔接
滚动页面将Canvas固定
首先我们要确定什么时候要固定Canvas,然后又在什么时候释放。我们可以使用minScroll
、maxScroll
来记录边界值与document.documentElement.scrollTop
做比较
minScroll
和maxScroll
的确定
当前时间的计算
看两个公式
- 滚动的单位时长 = 视频时间 / Canvas高度;
- 当前时间 = 滚动的单位时长 * 滚动距离
最后再加上时间边界的判断与设置,代码就出来了
如何与当前的页面做衔接
当我们从上往下滚动时上边界到Canvas的固定衔接是很顺畅,但是到下边界就会有闪动。我们可以在Canvas下面再接一个Canvas,用来过渡使用。它的内容是视频的最后一帧
。
完整代码
tsx
/** 滚动浮动*/
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import './RollingSuspension.scss'
export default function Index() {
const canvasDom = useRef<any>(null)
const canvasCtx = useRef<any>(null)
const canvasDomOne = useRef<any>(null)
const canvasCtxOne = useRef<any>(null)
const videoDom = useRef<any>(null);
const videoFrame = useRef<any>(null)
const [height, setHeight] = useState(0)
const [width, setWidth] = useState(0)
const [style, setStyle] = useState({})
const minScroll = useRef(0)
const maxScroll = useRef(0)
const videoTime = useRef(0)
/** 滚动时触发*/
const onScroll = useCallback(() => {
if (document.documentElement.scrollTop >= minScroll.current &&
document.documentElement.scrollTop < maxScroll.current) {
setStyle({
position: 'sticky',
})
} else {
setStyle({
position: 'relative',
})
}
/** 单位速度*/
let speed = videoTime.current / (height);
let scrollHeight = document.documentElement.scrollTop - (minScroll.current);
let currentTime = 0;
if (scrollHeight < 0) {
} else if (scrollHeight > 0 && scrollHeight < height) {
currentTime = scrollHeight * speed;
} else {
currentTime = videoTime.current;
videoDom.current.currentTime = currentTime;
canvasCtxOne.current.drawImage(videoDom.current, 0, 0, width, height);
}
videoDom.current.currentTime = currentTime;
canvasCtx.current.drawImage(videoDom.current, 0, 0, width, height);
}, [height, width])
/** 获取视频时长*/
const onLoadedmetadata = () => {
const duration = videoDom.current.duration;
videoTime.current = duration;
}
/** 初始化,设置边界信息*/
const onInit = useCallback(() => {
let info = videoFrame.current.getBoundingClientRect();
minScroll.current = videoFrame.current.offsetTop;
maxScroll.current = videoFrame.current.offsetTop + info.height;
setHeight(window.innerHeight)
setWidth(window.innerWidth)
/** 因为Canvas的大小变化时,其内部的绘图上下文也会被重置,可能导致之前绘制的内容丢失
* 要等待一段时间
*/
requestAnimationFrame(() => {
onScroll()
})
}, [onScroll])
useEffect(function () {
if (canvasDom.current === null) {
return
}
canvasCtx.current = canvasDom.current.getContext('2d');
canvasCtxOne.current = canvasDomOne.current.getContext('2d');
onInit()
videoDom.current.addEventListener('loadedmetadata', onLoadedmetadata);
return () => {
videoDom.current.removeEventListener('loadedmetadata', onLoadedmetadata);
}
}, [])
useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => {
window.removeEventListener('scroll', onScroll)
}
}, [onScroll])
useEffect(() => {
window.addEventListener('resize', onInit)
return () => {
window.removeEventListener('resize', onInit)
}
}, [onInit])
return (
<>
<div className='rolling__top'>
</div>
<div className='rolling__canvas'
ref={videoFrame}
style={style}
>
<canvas ref={canvasDom}
width={width}
height={height}
></canvas>
</div>
<div className='rolling__bottom'>
<canvas ref={canvasDomOne}
width={width}
height={height}
></canvas>
</div>
<div className='rolling__bottom'>
</div>
<video
style={{
display: 'none'
}}
ref={videoDom}
src='http://nice.zuo11.com/5-airpods-pro-play-video-on-scroll/airpods-pro.webm'
></video>
</>
)
}
// 可以换成这个链接看看 https://www.apple.com.cn/105/media/us/airpods-pro/2022/d2deeb8e-83eb-48ea-9721-f567cf0fffa8/anim/dancer/small.webm
结语
感兴趣的可以去试试。