弹幕是一种在视频、直播或网页上实时显示用户评论的交互形式,具有很强的实时性和互动性。在Vue项目中实现弹幕功能,需要考虑以下几个核心方面:
<!-- Danmaku.vue -->
<template>
<div class="danmaku-container" ref="container">
<slot /> <!-- 背景内容 -->
<div class="danmaku-area" ref="danmakuArea"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
const props = defineProps({
// 弹幕数据
messages: {
type: Array,
default: () => []
},
// 轨道数量
trackCount: {
type: Number,
default: 10
},
// 默认字体大小
fontSize: {
type: Number,
default: 18
},
// 默认速度
speed: {
type: Number,
default: 10
},
// 是否暂停
paused: {
type: Boolean,
default: false
}
});
const emits = defineEmits(['send']);
const container = ref(null);
const danmakuArea = ref(null);
const containerWidth = ref(0);
const containerHeight = ref(0);
const usedTracks = ref(new Set());
const activeDanmakus = ref(new Map()); // 活跃的弹幕
const danmakuId = ref(0); // 弹幕ID计数器
// 初始化
onMounted(() => {
updateContainerDimensions();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
// 处理已有弹幕
renderExistingDanmakus();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
// 清除所有动画
activeDanmakus.value.forEach(danmaku => {
danmaku.animation.cancel();
});
});
// 监听弹幕数据变化
watch(() => props.messages, (newMessages, oldMessages) => {
// 只处理新增的弹幕
if (newMessages.length > oldMessages.length) {
const newItems = newMessages.slice(oldMessages.length);
newItems.forEach(message => {
addDanmaku(message);
});
}
});
// 更新容器尺寸
const updateContainerDimensions = () => {
if (container.value) {
containerWidth.value = container.value.offsetWidth;
containerHeight.value = container.value.offsetHeight;
}
};
// 处理窗口大小变化
const handleResize = () => {
updateContainerDimensions();
// 重新计算所有弹幕位置
activeDanmakus.value.forEach(danmaku => {
updateDanmakuPosition(danmaku);
});
};
// 渲染已有弹幕
const renderExistingDanmakus = () => {
props.messages.forEach(message => {
addDanmaku(message);
});
};
// 添加弹幕
const addDanmaku = (message) => {
if (!danmakuArea.value || props.paused) return;
const track = getAvailableTrack();
if (track === -1) return; // 没有可用轨道
const id = `danmaku-${danmakuId.value++}`;
const danmaku = document.createElement('div');
danmaku.className = 'danmaku-item';
danmaku.id = id;
// 设置弹幕样式
const color = message.color || '#FFFFFF';
const fontSize = message.fontSize || props.fontSize;
const opacity = message.opacity !== undefined ? message.opacity : 1;
danmaku.style.color = color;
danmaku.style.fontSize = `${fontSize}px`;
danmaku.style.opacity = opacity;
danmaku.style.fontWeight = 'bold';
danmaku.style.textShadow = '0 0 2px rgba(0,0,0,0.8)';
danmaku.style.zIndex = '10';
danmaku.style.whiteSpace = 'nowrap';
danmaku.style.position = 'absolute';
// 设置弹幕内容
danmaku.textContent = message.text;
// 添加到容器
danmakuArea.value.appendChild(danmaku);
// 计算弹幕宽度
const danmakuWidth = danmaku.offsetWidth;
// 设置初始位置
const top = track * (containerHeight.value / props.trackCount) + 5;
danmaku.style.top = `${top}px`;
danmaku.style.left = `${containerWidth.value}px`;
// 标记轨道为已使用
usedTracks.value.add(track);
// 计算动画持续时间
const speed = message.speed || props.speed;
const duration = (containerWidth.value + danmakuWidth) / speed * 100;
// 应用动画
const animation = danmaku.animate(
[
{ transform: `translateX(0)` },
{ transform: `translateX(${-containerWidth.value - danmakuWidth}px)` }
],
{
duration: duration,
easing: 'linear'
}
);
animation.onfinish = () => {
// 动画结束后移除弹幕
if (danmakuArea.value && danmaku.parentNode === danmakuArea.value) {
danmakuArea.value.removeChild(danmaku);
}
// 释放轨道
usedTracks.value.delete(track);
// 从活跃弹幕列表中移除
activeDanmakus.value.delete(id);
};
// 保存弹幕信息
activeDanmakus.value.set(id, {
element: danmaku,
animation,
track,
width: danmakuWidth,
startTime: Date.now()
});
};
// 获取可用轨道
const getAvailableTrack = () => {
// 优先随机选择一个轨道
for (let i = 0; i < 3; i++) {
const randomTrack = Math.floor(Math.random() * props.trackCount);
if (!usedTracks.value.has(randomTrack)) {
return randomTrack;
}
}
// 如果没有可用轨道,尝试使用最早的轨道
if (usedTracks.value.size < props.trackCount) {
for (let i = 0; i < props.trackCount; i++) {
if (!usedTracks.value.has(i)) {
return i;
}
}
}
// 如果所有轨道都被占用,返回-1
return -1;
};
// 更新弹幕位置
const updateDanmakuPosition = (danmaku) => {
if (!containerWidth.value) return;
// 计算当前位置
const progress = danmaku.animation.effect.getComputedTiming().progress;
const originalWidth = containerWidth.value + danmaku.width;
const currentX = originalWidth * (1 - progress);
// 重新计算动画
danmaku.animation.cancel();
const newDuration = (currentX / originalWidth) * danmaku.animation.effect.getComputedTiming().duration;
danmaku.animation = danmaku.element.animate(
[
{ transform: `translateX(${containerWidth.value - currentX}px)` },
{ transform: `translateX(${-danmaku.width}px)` }
],
{
duration: newDuration,
easing: 'linear'
}
);
danmaku.animation.onfinish = () => {
if (danmakuArea.value && danmaku.element.parentNode === danmakuArea.value) {
danmakuArea.value.removeChild(danmaku.element);
}
usedTracks.value.delete(danmaku.track);
activeDanmakus.value.delete(danmaku.element.id);
};
};
// 发送弹幕
const sendDanmaku = (text, options = {}) => {
const message = {
text,
...options
};
emits('send', message);
addDanmaku(message);
};
// 暂停/恢复弹幕
const togglePause = (paused) => {
activeDanmakus.value.forEach(danmaku => {
if (paused) {
danmaku.animation.pause();
} else {
danmaku.animation.play();
}
});
};
// 监听暂停状态变化
watch(() => props.paused, (newValue) => {
togglePause(newValue);
});
export default {
methods: {
sendDanmaku,
togglePause
}
};
</script>
<style scoped>
.danmaku-container {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.danmaku-area {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.danmaku-item {
position: absolute;
white-space: nowrap;
will-change: transform;
transition: none !important;
}
</style><!-- DanmakuSender.vue -->
<template>
<div class="danmaku-sender">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<input
type="text"
v-model="message"
:placeholder="placeholder"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
@keyup.enter="send"
>
</div>
<div class="flex items-center space-x-3">
<div class="flex items-center">
<input type="radio" id="color-white" name="color" value="white" v-model="color" class="h-4 w-4 text-primary focus:ring-primary border-gray-300">
<label for="color-white" class="ml-2 text-sm text-gray-700">白色</label>
</div>
<div class="flex items-center">
<input type="radio" id="color-red" name="color" value="red" v-model="color" class="h-4 w-4 text-primary focus:ring-primary border-gray-300">
<label for="color-red" class="ml-2 text-sm text-gray-700">红色</label>
</div>
<div class="flex items-center">
<input type="radio" id="color-yellow" name="color" value="yellow" v-model="color" class="h-4 w-4 text-primary focus:ring-primary border-gray-300">
<label for="color-yellow" class="ml-2 text-sm text-gray-700">黄色</label>
</div>
<button
@click="send"
class="bg-primary hover:bg-primary/90 text-white px-6 py-3 rounded-lg transition-colors duration-200"
>
发送弹幕
</button>
</div>
</div>
<div class="mt-4 flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<input type="checkbox" id="show-danmaku" v-model="showDanmaku" class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded">
<label for="show-danmaku" class="ml-2 text-sm text-gray-700">显示弹幕</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="scroll-danmaku" v-model="scrollDanmaku" class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded">
<label for="scroll-danmaku" class="ml-2 text-sm text-gray-700">滚动弹幕</label>
</div>
</div>
<div class="text-sm text-gray-500">
{{ danmakuCount }}条弹幕
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, defineProps, defineEmits } from 'vue';
const props = defineProps({
placeholder: {
type: String,
default: '发送一条弹幕...'
},
danmakuCount: {
type: Number,
default: 0
}
});
const emits = defineEmits(['send', 'toggleVisibility', 'toggleScroll']);
const message = ref('');
const color = ref('white');
const showDanmaku = ref(true);
const scrollDanmaku = ref(true);
const send = () => {
if (!message.value.trim()) return;
emits('send', {
text: message.value,
color: color.value,
timestamp: Date.now()
});
message.value = '';
};
const toggleShowDanmaku = () => {
showDanmaku.value = !showDanmaku.value;
emits('toggleVisibility', showDanmaku.value);
};
const toggleScrollDanmaku = () => {
scrollDanmaku.value = !scrollDanmaku.value;
emits('toggleScroll', scrollDanmaku.value);
};
</script><!-- pages/VideoPlayer.vue -->
<template>
<div class="video-player-container">
<!-- 视频播放器 -->
<div class="relative w-full bg-black aspect-video rounded-lg overflow-hidden">
<video
ref="videoRef"
class="w-full h-full object-cover"
controls
poster="https://picsum.photos/1200/675?random=2"
>
<source src="https://example.com/video.mp4" type="video/mp4">
您的浏览器不支持视频播放
</video>
<!-- 弹幕组件 -->
<Danmaku
ref="danmakuRef"
:messages="danmakuList"
:paused="!isPlaying"
@send="handleSendDanmaku"
>
<!-- 视频播放器会作为弹幕背景 -->
</Danmaku>
</div>
<!-- 弹幕发送器 -->
<DanmakuSender
@send="handleSendDanmaku"
:danmakuCount="danmakuList.length"
/>
<!-- 弹幕列表 -->
<div class="mt-8">
<h3 class="text-lg font-medium mb-4">最新弹幕</h3>
<div class="space-y-2 max-h-[300px] overflow-y-auto">
<div v-for="(item, index) in danmakuList.slice(-10)" :key="index" class="p-2 bg-gray-50 rounded">
<span :style="{ color: item.color }">{{ item.text }}</span>
<span class="text-xs text-gray-400 ml-2">{{ formatTime(item.timestamp) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import Danmaku from '@/components/Danmaku.vue';
import DanmakuSender from '@/components/DanmakuSender.vue';
const videoRef = ref(null);
const danmakuRef = ref(null);
const danmakuList = ref([]);
const isPlaying = ref(false);
onMounted(() => {
// 监听视频播放状态
if (videoRef.value) {
videoRef.value.addEventListener('play', () => {
isPlaying.value = true;
});
videoRef.value.addEventListener('pause', () => {
isPlaying.value = false;
});
}
// 添加一些示例弹幕
addSampleDanmakus();
});
const addSampleDanmakus = () => {
const samples = [
{ text: '666666', color: 'white' },
{ text: '前方高能', color: 'red' },
{ text: '这个视频太精彩了', color: 'white' },
{ text: '主播太强了', color: 'yellow' },
{ text: '第一次看这个视频', color: 'white' }
];
samples.forEach((sample, index) => {
setTimeout(() => {
const message = {
...sample,
timestamp: Date.now()
};
danmakuList.value.push(message);
}, index * 2000);
});
};
const handleSendDanmaku = (message) => {
// 添加时间戳
const newMessage = {
...message,
timestamp: Date.now()
};
// 添加到弹幕列表
danmakuList.value.push(newMessage);
// 如果需要,可以发送到服务器
// sendDanmakuToServer(newMessage);
};
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
};
</script>为了方便在整个应用中使用弹幕功能,可以创建一个Vue插件:
// plugins/danmaku.ts
import type { App } from 'vue';
import Danmaku from '@/components/Danmaku.vue';
import DanmakuSender from '@/components/DanmakuSender.vue';
export const DanmakuPlugin = {
install(app: App) {
app.component('Danmaku', Danmaku);
app.component('DanmakuSender', DanmakuSender);
// 添加全局方法
app.config.globalProperties.$danmaku = {
send(text: string, options: any = {}) {
// 这里可以实现全局发送弹幕的逻辑
console.log('发送弹幕:', text, options);
}
};
}Vue, 网页弹幕,前端开发,JavaScript,HTML,CSS,WebSocket, 实时通信,动画效果,用户交互,组件化开发,数据绑定,响应式设计,前端框架,动态渲染
资源地址:
https://pan.quark.cn/s/e8c2520dc9db
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。