用浏览器滚动条控制canvas播放逐帧动画,达到页面展示产品的视觉效果。各大品牌展示产品效果都使用过。
问题拆分#
要实现这个效果,需要把如何实现这个问题拆成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的触发次数,保持触发帧率 |
接下来,可以看优化后的效果