Vue/Nuxt.js 滚动动画:Canvas 与 Video 实现详解
2025-01-04 10:46:37
Vue/Nuxt.js 中的滚动动画实现方案
该问题的核心是在 Vue/Nuxt.js 环境下复现使用原生 HTML, CSS 和 JavaScript 实现的基于滚动条位置的背景视频播放效果。难点在于如何在 Vue 的生命周期中管理 DOM 操作、状态,以及与 Nuxt.js 的路由和布局进行协调。主要问题拆解为:如何控制视频帧播放、如何集成到组件以及如何在 Nuxt.js 中使用。
方案一: 基于 Canvas 的图像序列播放
该方案直接翻译原始代码的逻辑,将图片序列加载并用 Canvas 进行渲染。
原理: 该方案的核心在于根据滚动位置,动态计算并更新 canvas
元素上显示的图像帧,从而模拟视频播放。 这样做的好处是可以精准地控制每一帧的展示,同时也可以更有效地控制动画播放方向,包括反向播放。
实现步骤:
-
创建 Vue 组件:
创建一个名为ScrollAnimation.vue
的组件。组件的结构基本复制原始 HTML 结构,使用canvas
标签渲染图片序列,其他元素作为覆盖层,需要进行相应的 vue 语法调整。<template> <div class="wrapper"> <div class="overlay"></div> <div class="content"> <h1>Some overlaying Text</h1> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, deserunt.</p> <a href="#firstEl">Skip animation</a> </div> <canvas ref="canvas" id="hero-lightpass"></canvas> </div> <div id="firstEl" class="moreStuff"> <p>#1 Lorem ipsum dolor, sit amet consectetur adipisicing elit. In sed consequuntur excepturi sapiente cupiditate dicta eos ut officiis a consequatur, labore nam velit enim, quibusdam error tenetur voluptatibus corrupti quasi asperiores</p> </div> </template> <script> export default { data() { return { frameCount: 100, img: null, context: null } }, mounted() { this.canvas = this.$refs.canvas; this.context = this.canvas.getContext("2d"); this.canvas.width = 1920; this.canvas.height = 1080; this.img = new Image(); this.img.onload = () => { this.context.drawImage(this.img, 0, 0); } this.img.src = this.currentFrame(1); this.preloadImages(); window.addEventListener("scroll", this.handleScroll); }, beforeDestroy() { window.removeEventListener('scroll', this.handleScroll) }, methods:{ currentFrame(index){ return `https://medgeclients.com/majestic/seq/${index.toString().padStart(4, "")}.jpg` }, preloadImages(){ for (let i = 1; i < this.frameCount; i++) { const img = new Image(); img.src = this.currentFrame(i); } }, updateImage(index){ this.img.src = this.currentFrame(index); this.context.drawImage(this.img, 0, 0); }, handleScroll(){ const html = document.documentElement; const scrollTop = html.scrollTop; const maxScrollTop = html.scrollHeight - window.innerHeight; const scrollFraction = scrollTop / maxScrollTop; const frameIndex = Math.min(this.frameCount - 1, Math.ceil(scrollFraction * this.frameCount)); requestAnimationFrame(() => this.updateImage(frameIndex + 1)); }, } }; </script> <style scoped>
:root {
--fromTop: calc((28826 / 1.1) * 1px);
}
-
{
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}body {
margin: 0;
padding: 0;
position: relative;
}canvas {
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.wrapper {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
}
.overlay {
background:linear-gradient(to right, rgba(13, 14, 53, 0.9), rgba(13, 14, 53, 0.5), transparent);
position: absolute;
left: 0;
top: 0;
width: 70%;
height: 100%;
z-index: 2;
}
.content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 3;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 5vw;
}
.content * {
color: red;
text-shadow: 0 0 8px #050529;
}
.moreStuff {
background: #ffffff;
position: relative;
left: 0;
top: var(--fromTop);
width: 100%;
height: 100%;
padding: 100px 50px 1500px;
z-index: 99999;
}
2. **在页面或布局中使用该组件:**
在 Nuxt.js 中,可以将该组件导入到页面(`pages` 目录下),或直接放到布局(`layouts` 目录下)中。
例如:如果要在默认布局中使用,修改 `layouts/default.vue`:
```vue
<template>
<div>
<ScrollAnimation />
<nuxt />
</div>
</template>
<script>
import ScrollAnimation from '@/components/ScrollAnimation.vue'
export default {
components:{
ScrollAnimation
}
}
</script>
```
**注意事项:**
* 组件内部使用 `mounted` 钩子来获取 `canvas` 上下文。 `beforeDestroy` 钩子可以解除监听事件。
* 需要添加对应 css 代码,使用`scoped`属性是为了减少组件之间的影响。
### 方案二:使用 Video 标签配合滚动播放控制
该方案使用 HTML `video` 标签进行视频播放控制,配合 CSS `position: fixed` 让其始终处于页面顶层。这种方式能够更方便的使用一些 HTML video API 。
**原理:** 该方案不使用 Canvas, 而是直接采用HTML `<video>`标签播放预先准备的视频资源,通过监听滚动事件,计算当前播放的时间位置,配合视频标签的 api 快速seek,进而控制播放效果,也可以轻松地控制播放速度。
**实现步骤:**
1. **创建 Video 组件:** 创建 `VideoScroll.vue` 组件。需要有一个 `video` 标签用于显示视频,其他的覆盖层如方案一。
```vue
<template>
<div class="wrapper">
<div class="overlay"></div>
<div class="content">
<h1>Some overlaying Text</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, deserunt.</p>
<a href="#firstEl">Skip animation</a>
</div>
<video ref="video" class="hero-video" muted playsinline />
</div>
<div id="firstEl" class="moreStuff">
<p>#1 Lorem ipsum dolor, sit amet consectetur adipisicing elit. In sed consequuntur excepturi sapiente cupiditate dicta eos ut officiis a consequatur, labore nam velit enim, quibusdam error tenetur voluptatibus corrupti quasi asperiores</p>
</div>
</template>
<script>
export default {
data() {
return {
videoUrl: '/video/scroll-animation.mp4',
duration:0
};
},
mounted() {
this.video = this.$refs.video;
this.video.src = this.videoUrl;
this.video.addEventListener("loadedmetadata", this.handleMetadataLoaded);
window.addEventListener("scroll", this.handleScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
handleMetadataLoaded(){
this.duration = this.video.duration;
},
handleScroll() {
const html = document.documentElement;
const scrollTop = html.scrollTop;
const maxScrollTop = html.scrollHeight - window.innerHeight;
const scrollFraction = scrollTop / maxScrollTop;
const currentPlayTime = Math.max(0, Math.min(this.duration ,scrollFraction * this.duration) ) ;
this.video.currentTime = this.duration- currentPlayTime ;
},
},
};
</script>
<style scoped>
:root {
--fromTop: calc((28826 / 1.1) * 1px);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
padding: 0;
position: relative;
}
.hero-video {
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.wrapper {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
}
.overlay {
background:linear-gradient(to right, rgba(13, 14, 53, 0.9), rgba(13, 14, 53, 0.5), transparent);
position: absolute;
left: 0;
top: 0;
width: 70%;
height: 100%;
z-index: 2;
}
.content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 3;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 5vw;
}
.content * {
color: red;
text-shadow: 0 0 8px #050529;
}
.moreStuff {
background: #ffffff;
position: relative;
left: 0;
top: var(--fromTop);
width: 100%;
height: 100%;
padding: 100px 50px 1500px;
z-index: 99999;
}
</style>
- 视频准备: 将提前准备好的 mp4 格式视频资源放到项目的
/static
文件夹,便于引入。如果想使用自定义的目录结构, 请在nuxt.config.js
文件中配置相应的路径,并修改videoUrl
属性为正确的路径。 - 引入组件: 如方案一所述,可以在需要的页面或者布局中引入
VideoScroll.vue
组件。
注意事项:
mounted
钩子需要等待视频元数据加载完毕 (loadedmetadata
事件)。- 使用了
playsinline
和muted
属性, 以防止在部分移动设备上的视频自动播放问题, 同时注意, 在大部分浏览器中, 使用<video>
标签进行播放需要muted
属性才能成功,并且可以通过 javascript 代码控制是否解除静音。 - 可以添加视频循环播放的功能。
安全建议:
- 考虑到浏览器性能,建议对预加载的图像进行优化。比如图像尺寸优化,图片资源压缩等方式。
方案选择:
两个方案都可以在 Vue/Nuxt.js 项目中实现基于滚动的视频播放。方案一使用 canvas, 能够更精准的控制每一帧图像,比较适合细粒度的滚动动画,对开发人员的要求更高,调试比较复杂。方案二采用 Video 标签,更容易理解,代码更简洁,对控制精度要求不高的情况下更合适。
以上两个方案都演示了如何在 Vue/Nuxt.js 项目中处理这种特殊的滚动动画效果。可以根据项目具体需求选择方案进行落地。