Vue下实现一个简单的弹幕组件

127 阅读1分钟

初始化

预先声明需要的参数,如容器的宽高、弹幕的最大数量,弹幕池、轨道池用于存储弹幕队列和轨道队列。

    const fontSize = 20;
    const opacity = 1;
    // 弹幕池滚动元素最大数量
    const danmakuRollDomPoolMaxCount = 20;
    // 轨道高度
    let trackHeight = Number(fontSize * props.danmakuSetting.value.danmakuSize) / 100 + 5 || fontSize + 5;
    // 弹幕移动时间不算本体宽度, 用于计算速度
    const movingDuration = 8;
    const minMovingDuration = 5;

    let danmakuRollDomPool = ref([]);
    let danmakuScrollList = computed(() => props.danmakuScrollList);

    let danmakuContainerRef = ref(null);
    let danmakuContainerHeight = 0;
    let danmakuContainerWidth = 0;
    let danmakuContainerRight = 0;

弹幕生成

createElement方法创建元素,可以通过css变量来动态的修改弹幕元素的样式,每个Dom元素用于承载一个弹幕,当弹幕动画执行完毕后,将Dom元素加入空闲队列,不需要重复的创建销毁

    const createDanmukuElement = (danmaku) => {
        const { content } = danmaku;
        const danmakuDom = document.createElement('div');
        danmakuDom.classList.add('player-danmaku-dm');
        danmakuContainerRef.value.appendChild(danmakuDom);
        danmakuDom.innerText = content;
        const color = intToHexColor(danmaku.color);
        danmakuDom.style = `
            display: flex;
            --opacity: ${Number(opacity * props.danmakuSetting.value.danmakuOpacity) / 100};
            --fontSize: ${Number(fontSize * props.danmakuSetting.value.danmakuSize) / 100}px;
            --fontFamily: SimHei, 'Microsoft JhengHei', Arial, Helvetica, sans-serif;
            --fontWeight: normal;
            --textShadow: 1px 0 1px #000000, 0 1px 1px #000000, 0 -1px 1px #000000, -1px 0 1px #000000;
            --color: ${color};
        `;
        danmakuRollDomPool.value.push(danmakuDom);
        return danmakuDom;
    };

分配轨道

一般从上到下选择可用的轨道,轨道可以的条件为轨道内最后一个弹幕元素离容器边缘的距离大于将要插入的弹幕元素的宽度。

    const selectRollTrack = R.curry((tracks, danmakuWidth) => {
        return R.findIndex(track => {            
            return R.ifElse(
                R.propEq(0, 'length'),
                R.always(true),
                R.pipe(
                    R.last,
                    R.invoker(0, 'getBoundingClientRect'),
                    // 是轨道最后一个弹幕的rect
                    rect => danmakuContainerRight - rect.right > danmakuWidth,
                )
            )(track);
        })(tracks);
    });

弹幕位置

通过绝对定位设置弹幕初始位置为容器的右侧,即设置left为-10px(大小随意),当弹幕动画结束后可以通过增删类名、强制浏览器重排重置动画。我们可以通过简单的数学计算算出后一个弹幕追上前一个弹幕的最大速度,随机设置一个区间速度来让弹幕滚动变得更加自然,追及:前弹幕剩余距离 / 前弹幕的速度 >= 后弹幕的距离 / 后弹幕的速度。

      // 计算追及速度
        const track = tracksPool.value[trackIndex];
        // 将初始速度设为正常速度到最大速度之间的随机值
        let catchUpSpeed = Math.random() * (maxSpeed - speed) + speed;
        if(track.length > 0) {
            const previousDanmuku = track[track.length - 1];
            const previousRect = previousDanmuku.getBoundingClientRect();
            const previousSpeed = previousDanmuku.catchUpSpeed;
            catchUpSpeed = previousSpeed * (danmakuContainerWidth + 10) / (previousRect.right + 10);
        };
        const rangeTop = Math.min(catchUpSpeed, maxSpeed);
        catchUpSpeed = Math.random() * (rangeTop - speed) + speed;
        const translateXNum = danmakuContainerWidth + danmakuDomWidth + 10 + 10;
        const duration = translateXNum / catchUpSpeed;
        danmakuDom.style.setProperty('--translateX', `-${translateXNum}px`);
        danmakuDom.style.setProperty('--top', `${trackIndex * trackHeight}px`);
        danmakuDom.style.setProperty('--duration', `${duration}s`);
        danmakuDom.style.setProperty('--offset', `${danmakuContainerWidth + 10}px`);
        danmakuDom.style.setProperty('--fontSize', `${Number(fontSize * props.danmakuSetting.value.danmakuSize) / 100}px`);
        danmakuDom.style.setProperty('--opacity', `${Number(opacity * props.danmakuSetting.value.danmakuOpacity) / 100}`);

        danmakuDom.catchUpSpeed = catchUpSpeed;
        tracksPool.value[trackIndex].push(danmakuDom);
        // 重置动画
        danmakuDom.classList.remove('player-danmaku-roll');
        void danmakuDom.offsetWidth;
        danmakuDom.classList.add('player-danmaku-roll');
        // 配置{once: true}防止同一个元素重绘后多次触发
        danmakuDom.addEventListener('animationend', () => {
            // 动画结束后隐藏
            danmakuDom.style.display = 'none';
            // 从轨道中移除
            tracksPool.value[trackIndex] = tracksPool.value[trackIndex].slice(1);
        }, {once: true});