likes
comments
collection
share

Canvas实现苹果充电盒动效

作者站长头像
站长
· 阅读数 16

前言

Canvas学了就想用,前两天写了Canvas实现数字雨和放大镜效果,感觉还要再练练手,今天来实现一下苹果官网的充电盒动效。后面有完整代码。

正文

还是先看看最终的效果,实现的原理也很简单。动态效果是一个视频,我们只要根据页面滚动的距离去计算当前播放的时间,再绘制到画布上就好了,剩下就是对细节的处理:

  • 滚动页面时如何将Canvas固定
  • 对当前时间的计算
  • 滚动一定距离后,如何与当前的页面做衔接

Canvas实现苹果充电盒动效

滚动页面将Canvas固定

首先我们要确定什么时候要固定Canvas,然后又在什么时候释放。我们可以使用minScrollmaxScroll来记录边界值与document.documentElement.scrollTop做比较

Canvas实现苹果充电盒动效

minScrollmaxScroll的确定

Canvas实现苹果充电盒动效

当前时间的计算

看两个公式

  • 滚动的单位时长 = 视频时间 / Canvas高度;
  • 当前时间 = 滚动的单位时长 * 滚动距离

最后再加上时间边界的判断与设置,代码就出来了

Canvas实现苹果充电盒动效

如何与当前的页面做衔接

当我们从上往下滚动时上边界到Canvas的固定衔接是很顺畅,但是到下边界就会有闪动。我们可以在Canvas下面再接一个Canvas,用来过渡使用。它的内容是视频的最后一帧

Canvas实现苹果充电盒动效

完整代码

/** 滚动浮动*/
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

结语

感兴趣的可以去试试。