Canvas实现苹果充电盒动效

前言

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

正文

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

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

滚动页面将Canvas固定

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

minScrollmaxScroll的确定

当前时间的计算

看两个公式

  • 滚动的单位时长 = 视频时间 / 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

结语

感兴趣的可以去试试。

相关推荐
我是伪码农10 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king34 分钟前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳43 分钟前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法
程序员猫哥_2 小时前
HTML 生成网页工具推荐:从手写代码到 AI 自动生成网页的进化路径
前端·人工智能·html