浏览器滚动条控制canvas播放逐帧动画

用浏览器滚动条控制canvas播放逐帧动画,达到页面展示产品的视觉效果。各大品牌展示产品效果都使用过。

问题拆分#

要实现这个效果,需要把如何实现这个问题拆成2个部分:

  1. 滚动条控制
  2. canvas播放逐帧动画(准备需要使用的逐帧图片)

例子使用vue实现,内核还是javascript。可移植

滚动条监听#

先监听浏览器滚动条事件,区分滚动方向。监听方法在页面生成的生命周期中使用

document.addEventListener('scroll', this.onScroll, false);
// 监听方法 获取滚动高度和滚动方向
onScroll() {
            this.top = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
            if(this.beforeTop <= this.top){
            console.log("down");
                }else{
            console.log("up");
        }
             setTimeout(()=>{this.beforeTop = this.top;},0);
}
变量名 变量描述 备注
top 滚动条滚动高度
beforeTop 滚动条滚动高度2 用来区分滚动高度方向的值

准备画布 canvas#

//html:
<canvas ref="canvas" :width="canvasWidth" :height="canvasHeight"></canvas>
//js:
this.ctx = this.$refs.canvas.getContext('2d');
变量名 变量描述 备注
ctx 画布对象 2d模式
canvasWidth 画布宽度 请根据实际渲染图片尺寸作为依据
canvasHeight 画布高度

准备第一帧图片#

this.frame = new Image();
this.frame.src = `${this.path}/${this.fileName}${this.index}.${this.ext}`;
this.renderToCanvas(); // 渲染第一帧图片
变量名 变量描述 备注
frame 图片帧 图片格式的对象
path 路径 域名或目录名
fileName 文件名 除序号之外的文件名称
index 图片序号 当前帧(默认值:0,第一帧图片的序号)
ext 扩展名 图片文件自身的扩展名(jpg,png,bmp,webp等)

注意:透明背景的图片需要在渲染的同时,单独清除旧帧。否则显示效果会出现重影。

渲染函数#

renderToCanvas(){
       const _self = this;
       _self.frame.onload = function(){
           const frm = _self.ctx.createPattern(_self.frame,"no-repeat");
           _self.ctx.fillStyle = frm;
           _self.ctx.fillRect(0,0,_self.canvasWidth,_self.canvasHeight);
           // console.log('渲染');
       }
}

图片序列#

现在确认下需要渲染的图片数量,设定变量

变量名 变量描述 备注
maxFrame 图片序列总数 index的最大值,切换图片帧的范围

改造滚动条触发函数来图片渲染#

onScroll() {
        this.top = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
    if(this.beforeTop<=this.top){
      // console.log("down");
      if(this.index<this.maxFrame) this.index++;
    }else{
      // console.log("up");
      if(this.index>0) this.index--;
    }
    console.log('当前帧',this.index);
    this.frame.src = `${this.path}/${this.fileName}${this.index}.${this.ext}`;
    this.renderToCanvas();//逐帧渲染
    setTimeout(()=>{this.beforeTop = this.top;},0);
}

改造页面html代码,增加页面高度和样式#

<div class="page" :style="{ height: `${maxFrame * 30}px` }">
    <div class="canvas" :style="`--canvas-width:${canvasWidth}px;--canvas-height:${canvasHeight}px`">
      <canvas ref="canvas" :width="canvasWidth" :height="canvasHeight"></canvas>
  </div>
</div>
.canvas {
  width: 100%;
  aspect-ratio: ~"var(--canvas-width,1920)/var(--canvas-height,1080)";
  position: sticky;
  top: 0px;
  left: 0px;
  display: flex;
  background: #eee;

  canvas{
      flex:1;
  }
}

网络图片加载问题#

由于网络图片加载需要时间,而且帧图片数量居多。推荐将图片转换成webp格式减小网络加载吞吐量。图片需要一次性加载或者提前加载。使图片能够流畅切换帧。

loadAllImg() {
  for (let i = 0; i <= this.maxFrame; i++) {
    const img = new Image();
    img.src = `${this.path}/${this.fileName}${i}.${this.ext}`;
    img.onload = () => {
      console.log(`${i}加载完成`);
    };
  }
}

最后运行查看最终效果


由于scroll滚动条触发事件和切换图片帧不能保持一致,存在滚动过量超出触发事件次数的问题,导致无法退回第0帧或者元素高度不够导致无法播放到最后。

使用requestAnimationFrame()优化和调整#

将onScroll()函数重构。

  • 利用requestAnimationFrame()函数,按照固定(60帧或者30帧)频率获得滚动的距离。
  • 去掉滚动距离小数位变整数,赋值为帧序号。这样就能防止滚动高度和帧动画脱钩。
  • 使用flag来隔离onScroll和requestAnimationFrame的触发次数达到节流的效果。使得触发次数符合60帧的频率。
onScroll() {
// console.log('onScroll');
      if (this.flag) {
        return;
      }
      requestAnimationFrame(() => {
        // console.log('requestAnimationFrame');
        
        this.top =
          window.pageYOffset ||
          document.documentElement.scrollTop ||
          document.body.scrollTop;
        if (this.top >= 0 && this.top <= this.maxFrame) {
          this.index = Math.round(this.top);
          //  console.log("当前帧", this.index);
          this.frame.src = `${this.path}/${this.fileName}${this.index}.${this.ext}`;
          this.renderToCanvas(); //逐帧渲染
        }

        this.flag = false;
      });

   this.flag = true;
}

增加参数 flag 去掉参数 beforeTop

变量名 变量描述 备注
flag 默认值:false 隔离onScroll和requestAnimationFrame的触发次数,保持触发帧率

接下来,可以看优化后的效果

参考文章#