Как работал барраж?

внешний интерфейс JavaScript WebSocket Canvas
Как работал барраж?

В мире видео только шквал не сломлен

Говоря о пулеэкране, те, кто видел видео, не будут незнакомы, экран полон элегантных комментариев, от которых люди пьянеют и не могут выбраться.

Недавно, это было также потому, что я узнал оcanvasИтак, сегодня я хочу поделиться с вами историей о заграждении

Так как именно делается заграждение? Смотрим вниз(look)

СмотретьКакие? СмотретьЭффект

Визуализации были представлены вам, так что вы немного взволнованы? Да, я очень эмоциональна, мои мысли мирны, и я потерял дар речи.

Независимо от того, будем ли мы соответствовать таким требованиям в своей работе в будущем, пожалуйста, дайте себе шанс повысить свои навыки! ! !

Эффект этого пулевого экрана, структура проекта показан на следующем рисунке.

Общий проект дан, так что давайте засучим рукава и усердно поработаем.

пусть заградительный огонь летит

Мы упомянули о холсте выше, так что это убийца создания заграждения. Мы используем рисунок на холсте, чтобы реализовать функцию заграждения

Во-первых, давайте структуру html

// index.html文件
<div class="wrap">
    <h1>听妈妈的话 - 周杰伦</h1>
    <div class="main">
        <canvas id="canvas"></canvas>
        <video src="../source/mv.mp4" id="video" controls width="720" height="480"></video>
    </div>
    <div class="content">
        <input type="text" id="text">
        <input type="button" value="发弹幕" id="btn">
        <input type="color" id="color">
        <input type="range" id="range" max="40" min="20">
    </div>
</div>
// 引入index.js文件用来实现弹幕功能
<script src="./index.js"></script>

если нужноВидеоресурсыДа, просто нажмитездесьБар (код добычи: tsei)

Условно говоря, структура не имеет расширенного содержания, главное написатьтег холстаитег видео, они идеально подходят для экрана пули на веб-сайте с видео.

Что ж, больше никаких распродаж, давайте перейдем к главному событию.

Данные моделирования

// index.js文件
let data = [
    {value: '周杰伦的听妈妈的话,让我反复循环再循环', time: 5, color: 'red', speed: 1, fontSize: 22},
    {value: '想快快长大,才能保护她', time: 10, color: '#00a1f5', speed: 1, fontSize: 30},
    {value: '听妈妈的话吧,晚点再恋爱吧!爱呢?', time: 15},
];

Что представляют собой данные:

  • значение: представляет заграждениесодержание(обязательный)
  • время: от имени отображаемого заграждениявремя(обязательный)
  • цвет: представляет текст маркированного экранацвет
  • скорость: представляет заграждение, плавающее надскорость
  • fontSize: представляет текст загражденияразмер
  • непрозрачность: представляет текст экрана маркерапрозрачность

Кроме содержания заграждения и времени отображения, все остальное опционально, неважно, если этих параметров нет в смоделированных данных.

получить элемент дом

// index.js文件
// 模拟数据
...省略

// 获取到所有需要的dom元素
let doc = document;
let canvas = doc.getElementById('canvas');
let video = doc.getElementById('video');
let $txt = doc.getElementById('text');
let $btn = doc.getElementById('btn');
let $color = doc.getElementById('color');
let $range = doc.getElementById('range');

Холст визуализирует заградительный огонь

Далее мы будем использовать объектно-ориентированный метод для реализации функции заграждения рисования холста Причина, по которой мы выбираем этот метод, заключается в том, чтобы облегчить повторное использование и последующее добавление методов.

Давайте сначала создадим класс CanvasBarrage, который в основном используется в качестве холста для рендеринга всего заграждения.

Перед реализацией давайте вызовем его и посмотрим, как создается экземпляр

// index.js文件
// 模拟数据
...省略
// 获取到所有需要的dom元素
...省略

// 创建CanvasBarrage类
class CanvasBarrage {
    // todo
}
// 创建CanvasBarrage实例
let canvasBarrage = new CanvasBarrage(canvas, video, { data });

Создать экземпляр очень просто, объекта нет, просто нужен новый, ха-ха. Далее давайте приступим к делу, давайте быстро завершим todo-часть приведенного выше кода, чтобы улучшить класс CanvasBarrage.

Реализовать CanvasBarrage

// index.js文件
class CanvasBarrage {
    constructor(canvas, video, opts = {}) { 
        // opts = {}表示如果opts没传就设为{},防止报错,ES6语法
        
        // 如果canvas和video都没传,那就直接return掉
        if (!canvas || !video) return;
        
        // 直接挂载到this上
        this.video = video;
        this.canvas = canvas;
        // 设置canvas的宽高和video一致
        this.canvas.width = video.width;
        this.canvas.height = video.height;
        // 获取画布,操作画布
        this.ctx = canvas.getContext('2d');
        
        // 设置默认参数,如果没有传就给带上
        let defOpts = {
            color: '#e91e63',
            speed: 1.5,
            opacity: 0.5,
            fontSize: 20,
            data: []
        };
        // 合并对象并全都挂到this实例上
        Object.assign(this, defOpts, opts);
       
       // 添加个属性,用来判断播放状态,默认是true暂停
       this.isPaused = true;
       // 得到所有的弹幕消息
       this.barrages = this.data.map(item => new Barrage(item, this));
       // 渲染
       this.render();
       console.log(this);
    }
    // 渲染canvas绘制的弹幕
    render() {
        // todo
    }
}

В «получить все сообщения чата пули» метод карты массива возвращает массив, но возвращаемое содержимое является классом Barrage, почему это?

Помните, что я говорил ранее: преимущество использования класса заключается в том, что его легко расширять, и если вы добавите метод позже, вы сможете добавить его прямо в класс.

Поэтому мы не уважаем форму возврата {} непосредственно в методе прямой карты.

// 不推荐
this.barrages = this.data.map(item => { item });

Говоря об этом, мы должны сначала написать класс Barrage, иначе следующий console.log(this) сообщит об ошибке, потому что класс Barrage не может быть найден.

// index.js文件

++++++++++++++++++++++
// 创建Barrage类,用来实例化每一个弹幕元素
class Barrage {
    constructor(obj, ctx) {
        // todo
    }
}
++++++++++++++++++++++

class CanvasBarrage {
    ...省略
}

Теперь через console.log(this) в приведенном выше коде мы можем увидеть, что перед нами представлены все свойства и методы прототипа, смонтированные на этом экземпляре.

сделать это

Затем продолжайте писать метод рендеринга в классе CanvasBarrage выше, давайте закончим задачу

// index.js文件
class CanvasBarrage {
    constructor(canvas, video, opts = {}) {
        ...省略
        // 渲染
        this.render();
    }
    render() {
        // 渲染的第一步是清除原来的画布,方便复用写成clear方法来调用
        this.clear();
        // 渲染弹幕
        this.renderBarrage();
        // 如果没有暂停的话就继续渲染
        if (this.isPaused === false) {
            // 通过raf渲染动画,递归进行渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    clear() {
        // 清除整个画布
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
}

что сделал тодо?

  1. Очистите все предыдущие холсты, чтобы предотвратить эффект перекрывающихся рисунков.
    • this.clear()
  2. Рендеринг реальных данных пуля чата (еще не реализовано)
    • this.renderBarrage()
  3. Определить, продолжать ли рендеринг заграждения
    • Когда this.isPaused имеет значение false, оно выражается какстатус воспроизведения
  4. рекурсивный вызов рендера
    • Вызов рендеринга рекурсивно через requestAnimationFrame
    • Гораздо лучше, чем setInterval

Отрисовка всего метода рендеринга барража завершена, далее для продолжения записи следует именно метод рендеринга барража, который только что не был реализован.

Но перед этим надо написать отдельный, это класс Barrage

Поскольку он по-прежнему необходим для демонстрации своих навыков, каждый экземпляр заграждения создается им.

Создайте класс Barrage

Создатель заграждений уже здесь, давайте реализуем этот класс Barrage, чтобы увидеть, какие у него есть свойства и методы, и продолжим это делать.

// index.js文件
class Barrage {
    constructor(obj, ctx) {
        this.value = obj.value; // 弹幕的内容
        this.time = obj.time;   // 弹幕出现时间
        // 把obj和ctx都挂载到this上方便获取
        this.obj = obj;
        this.context = ctx;
    }
    // 初始化弹幕
    init() {
        // 如果数据里没有涉及到下面4种参数,就直接取默认参数
        this.color = this.obj.color || this.context.color;
        this.speed = this.obj.speed || this.context.speed;
        this.opacity = this.obj.opacity || this.context.opacity;
        this.fontSize = this.obj.fontSize || this.context.fontSize;
        
        // 为了计算每个弹幕的宽度,我们必须创建一个元素p,然后计算文字的宽度
        let p = document.createElement('p');
        p.style.fontSize = this.fontSize + 'px';
        p.innerHTML = this.value;
        document.body.appendChild(p);
        
        // 把p元素添加到body里了,这样就可以拿到宽度了
        // 设置弹幕的宽度
        this.width = p.clientWidth;
        // 得到了弹幕的宽度后,就把p元素从body中删掉吧
        document.body.removeChild(p);
        
        // 设置弹幕出现的位置
        this.x = this.context.canvas.width;
        this.y = this.context.canvas.height * Math.random();
        // 做下超出范围处理
        if (this.y < this.fontSize) {
            this.y = this.fontSize;
        } else if (this.y > this.context.canvas.height - this.fontSize) {
            this.y = this.context.canvas.height - this.fontSize;
        }
    }
    // 渲染每个弹幕
    render() {
        // 设置画布文字的字号和字体
        this.context.ctx.font = `${this.fontSize}px Arial`;
        // 设置画布文字颜色
        this.context.ctx.fillStyle = this.color;
        // 绘制文字
        this.context.ctx.fillText(this.value, thix.x, this.y);
    }
}

что сделал тодо?

  1. Получить необходимое значение и время из входящего объекта
    this.value = obj.value; // 内容
    this.time = obj.time;   // 时间
    
  2. Инициализировать заграждение
    • Задайте параметры, необходимые для каждого барражирования, если нет объекта, возьмите его.параметры по умолчанию
    • Рассчитать ширину каждой заграждения
      • Поскольку вы не можете напрямую манипулировать элементами на холсте холста, сначала создайте тег p.
      • Ширина тега p равна ширине экрана маркера -> this.width = p.clientWidth
    • Установите координаты x и y (начальное положение) каждого заграждения
      • Начальная позиция горизонтальной координаты x вводится справа, то есть: ширина холста
        • this.x = this.context.canvas.width
      • Начальное положение вертикальной координаты Y не фиксировано, и оно появляется в любом месте на холсте.
        • this.y = this.context.canvas.height * Math.random()
    • Обработка заграждения за пределами холста
      • Canvas отображает шрифты в соответствии с базовым размером шрифта, еслименьше, чемэторазмер шрифтаразмер
        • this.y = this.fontSize
      • еслибольше, чемохватыватьвысота холста - размер шрифтаразмер
        • this.y = this.context.canvas.height - this.fontSize
  3. Рендерить каждый шквал
    • Чтобы нарисовать текст, вам нужно установить текстразмер шрифта,цвети текстовыйсодержаниеикоординировать
    • размер шрифта API
      • this.context.ctx.font = ${this.value}px Arial
    • цвет API
      • this.context.ctx.fillStyle = this.color
    • содержание и координаты API
      • this.context.ctx.fillText(this.value, this.x, this.y)

Вышеупомянутые три шага - это то, что делает весь класс Barrage. Класс Barrage был типизирован, так что давайте приступим к реальному шагу рендеринга.

renderBarrage — главный герой

// index.js文件
class CanvasBarrage {
    ...省略
    renderBarrage() {
        // 首先拿到当前视频播放的时间
        // 要根据该时间来和弹幕要展示的时间做比较,来判断是否展示弹幕
        let time = this.video.currentTime;
        
        // 遍历所有的弹幕,每个barrage都是Barrage的实例
        this.barrages.forEach(barrage => {
            // 用一个flag来处理是否渲染,默认是false
            // 并且只有在视频播放时间大于等于当前弹幕的展现时间时才做处理
            if (!barrage.flag && time >= barrage.time) {
                // 判断当前弹幕是否有过初始化了
                // 如果isInit还是false,那就需要先对当前弹幕进行初始化操作
                if (!barrage.isInit) {
                    barrage.init();
                    barrage.isInit = true;
                }
                // 弹幕要从右向左渲染,所以x坐标减去当前弹幕的speed即可
                barrage.x -= barrage.speed;
                barrage.render(); // 渲染当前弹幕
                
                // 如果当前弹幕的x坐标比自身的宽度还小了,就表示结束渲染了
                if (barrage.x < -barrage.width) {
                    barrage.flag = true; // 把flag设为true下次就不再渲染
                }
            }
        });
    }
}

На этом этапе мы добавляем еще одно событие, которое запускает шквал, чтобы позволить шквалу летать.

// index.js文件
class CanvasBarrage {
    ...省略
}

// 创建CanvasBarrage实例
let canvasBarrage = new CanvasBarrage(canvas, video, { data });
++++++++++++++++++++++++++++++++++++++
// 设置video的play事件来调用CanvasBarrage实例的render方法
video.addEventListener('play', () => {
    canvasBarrage.isPaused = false;
    canvasBarrage.render(); // 触发弹幕
});
++++++++++++++++++++++++++++++++++++++

Тут все написали, пора показывать результаты, смотрите вниз

Не волнуйся, пусть шквал летит какое-то время

Функцию отрисовки шквала мы выполнили, давайте напишем как сделатьзаградительный огоньБар. Не медлите, поехали! ! !

заградительный огонь

// index.js文件
class CanvasBarrage {
    ...省略
}
video.addEventListener('play', ...省略);

+++++++++++++++++++++++++++++++++++++++
// 发送弹幕的方法
function send() {
    let value = $txt.value;  // 输入的内容
    let time = video.currentTime; // 当前视频时间
    let color = $color.value;   // 选取的颜色值
    let fontSize = $range.value; // 选取的字号大小
    let obj = { value, time, color, fontSize };
    // 添加弹幕数据
    canvasBarrage.add(obj);
    $txt.value = ''; // 清空输入框
}
// 点击按钮发送弹幕
$btn.addEventListener('click', send);
// 回车发送弹幕
$txt.addEventListener('keyup', e => {
    let key = e.keyCode;
    key === 13 && send();
});
+++++++++++++++++++++++++++++++++++++++

Отправить заграждение относительно просто: получив значение, время, цвет и размер шрифта, передайте их как объекты CanvasBarrage.добавить методПросто добавь

Давайте снова напишем метод add и вернемся к классу CanvasBarrage, чтобы продолжить писать.

// index.js文件
class CanvasBarrage {
    constructor() { ...省略}
    render() { ...省略 }
    renderBarrage() { ...省略 }
    clear() { ...省略 }
    +++++++++++++++++++++++++++
    add(obj) {
        // 实际上就是往barrages数组里再添加一项Barrage的实例而已
        this.barrages.push(new Barrage(obj, this));
    }
    +++++++++++++++++++++++++++
}

Готово, красиво, посмотрите на эффект

На данный момент мы завершили функцию заграждения на видео-сайте, поздравляем

Далее давайте улучшим обработку воспроизведения шквала при воспроизведении видео.

Пауза и перетаскивание

  • Пауза, чтобы остановить рендеринг шквала
// index.js文件
...省略
// 播放
video.addEventListener('play', () => {
    canvasBarrage.isPaused = false;
    canvasBarrage.render();
});
+++++++++++++++++++++++++++++++++++++++
// 暂停
video.addEventListener('pause', () => {
    // isPaused设为true表示暂停播放
    canvasBarrage.isPaused = true;
});
+++++++++++++++++++++++++++++++++++++++
  • Шквал в этот момент нужно перерендерить во время воспроизведения.
// index.js文件

// 暂停
video.addEventListener('pause', () => {
    canvasBarrage.isPaused = true;
});
+++++++++++++++++++++++++++++++++++++++
// 拖动进度条时触发seeked事件
video.addEventListener('seeked', () => {
    // 调用CanvasBarrage类的replay方法进行回放,重新渲染弹幕
    canvasBarrage.replay();
});
+++++++++++++++++++++++++++++++++++++++

Вернемся снова к классу CanvasBarrage.

// index.js文件
class CanvasBarrage {
    constructor() { ...省略}
    render() { ...省略 }
    renderBarrage() { ...省略 }
    clear() { ...省略 }
    add(obj) { ...省略 }
    +++++++++++++++++++++++++++
    replay() {
        this.clear(); //先清除画布
        // 获取当前视频播放时间
        let time = this.video.currentTime;
        // 遍历barrages弹幕数组
        this.barrages.forEach(barrage => {
            // 当前弹幕的flag设为false
            barrage.flag = false;
            // 并且,当前视频时间小于等于当前弹幕所展现的时间
            if (time <= barrage.time) {
                // 就把isInit重设为false,这样才会重新初始化渲染
                barrage.isInit = false;
            } else { // 其他时间对比不匹配的,flag还是true不用重新渲染
                barrage.flag = true;
            }
        });
    }
    +++++++++++++++++++++++++++
}

быть совершенным

ОК, пишите сюда, весь код про барражную функцию естьвсе кончено! ! ! Если вас попросят разработать функцию заграждения на работе, вы также легко сможете выполнить задание, набрав код несколько раз.

Однако всегда лучше делать полный набор вещей, и мы будем использовать его далее.WebSocketиredisДавайте сделаем еще несколько практических функций

Вы видели структуру каталогов раньше, и есть файл app.js, который на самом деле ничего не записывает, так что давайте приступим к его написанию дальше.

Связь WebSocket и хранилище Redis

Давно потерянный файл app.js, начните работать Прежде всего, нам нужно установить два пакета: один — модуль ws, который обрабатывает связь WebSocket на стороне сервера, а другой — модуль redis, в котором хранятся данные redis.

npm i ws redis -S

После завершения установки вы можете продолжать писать вещи

// app.js文件
const WebSocket = require('ws');
const redis = require('redis');
const clientRedis = redis.createClient(); // 创建redis客户端
const ws = new WebSocket.Server({ port: 9999 }); // 创建ws服务
// 用来存储不同的socket实例,区分不同用户
let clients = [];
// 监听连接
ws.on('connection', socket => {
    clients.push(socket); // 把socket实例添加到数组
    
    // 通过redis客户端的lrange方法来获取数据库中key为barrages的数据
    clientRedis.lrange('barrages', 0, -1, (err, data) => {
        // 由于redis存储的是key value类型,因此需要JSON.parse转成对象
        data = data.map(item => JSON.parse(item));
        
        // 发送给客户端,send方法传递的是字符串需要JSON.stringify
        // type为init是用来初始化弹幕数据的
        socket.send(JSON.stringify({
            type: 'init',
            data
        }));
    });
    // 监听客户端发来的消息
    socket.on('message', data => {
        // redis客户端通过rpush的方法把每个消息都添加到barrages表的最后面
        clientRedis.rpush('barrages', data);
        
        // 每个socket实例(用户)之间都可以发弹幕,并显示在对方的画布上
        // type为add表示此次操作为添加处理
        // 你可以打开两个index.html,分别发弹幕试试吧
        clients.forEach(sk => {
            sk.send(JSON.stringify({
                type: 'add',
                data: JSON.parse(data)
            }));
        });
        
    });
    // 当有socket实例断开与ws服务端的连接时
    // 重新更新一下clients数组,去掉断开的用户
    socket.on('close', () => {
        clients = clients.filter(client => client !== socket);
    });
});

Наполнение сервера завершено, далее немного изменим код клиента и вернемся к уже знакомому index.js.

// index.js文件
class CanvasBarrage {
    ...省略
}
+++++++++++++++++++++++++++++++
// 创建CanvasBarrage实例
// let canvasBarrage = new CanvasBarrage(canvas, video, { data });
let canvasBarrage;
let ws = new WebSocket('ws://localhost:9999');

// 监听与ws服务端的连接
ws.onopen = function () {
    // 监听ws服务端发来的消息
    ws.onmessage = function (e) {
        let msg = JSON.parse(e.data); //e.data里是真正的数据
        
        // 判断如果type为init就初始化弹幕的数据
        if (msg.type === 'init') {
            canvasBarrage = new CanvasBarrage(canvas, video, { data: msg.data });
        } else if (msg.type === 'add') { // 添加弹幕数据
            canvasBarrage.add(msg.data);
        }
    }
};
+++++++++++++++++++++++++++++++

// 发送弹幕的方法
function send() {
    let value = $txt.value;
    let time = video.currentTime;
    let color = $color.value;
    let fontSize = $range.value;
    let obj = { value, time, color, fontSize };
    // 添加弹幕数据
    // canvasBarrage.add(obj);
    +++++++++++++++++++++++++++++++
    // 把添加的弹幕数据发给ws服务端
    // 由ws服务端拿到后添加到redis数据库中
    ws.send(JSON.stringify(obj));
    +++++++++++++++++++++++++++++++
    $txt.value = '';
}

Передняя и задняя части готовы, осталось толькосоединятьнемногобаза данных Redisпросто хорошо

Правильный способ подключения к базе данных Redis

Прежде всего, будь то Windows или Mac, вам нужно сначала установить его

оконная система

Windows подключается к базе данных Redis

Войдите в загруженный и распакованный каталог redis и введите следующую команду в инструменте командной строки, чтобы установить соединение.

redis-server.exe redis.windows.conf

Появляется, как показано на рисунке ниже, указывая на то, что соединение было успешно установлено.

Инструмент визуализации Redis (Redis Desktop Manager) под окнами

система Mac

  • макинтош:brew install redis
  • соединять:brew services start redis

redisЕсли база данных успешно подключена, то вы можете напрямуюзапустить app.jsОткройте файл index.html, и вы обнаружите, что можете получить данные заграждения, хранящиеся в базе данных.

Что ж, теперь все довольны, это потрясающе, каждый из нас может выбить свой шквал.

Непрерывное обучение заставит нас прогрессировать понемногу, впереди еще очень долгий путь, мы все медленно движемся вперед

Кстати, забыл самое главное, если есть вопросы, можете прочитатьАдрес источникасделать ссылку

это конец

Я планирую провести некоторые исследования в будущем.рисунок на холстеЗнания указывают, и я надеюсь, что после исследования я смогу разобраться и поделиться ими со всеми, чтобы учиться вместе.

У нас, как у большого фронтенда, слишком много вещей, которым нужно научиться. Один главный и несколько талантов — это королевский путь. Давайте усердно работать вместе! Всем спасибо за просмотр

Ссылаться на