
Одним из таких примеров был чат, работающий поверх веб-сокетов с расширенным функционалом, позволяющим совместно рисовать в реальном времени. Это в чем-то напоминает известную детскую игру, когда участники по-очереди или одновременно рисуют разные части какого-то животного, а потом складывают фрагменты вместе и смотрят, что получилось.
В этой статье я вкратце расскажу, как такая рисовалка устроена и с какими сложностями пришлось столкнуться. Сразу отмечу, что это не готовый продукт, а всего лишь прототип.
Веб-сокеты
Все началось с того, что нужно было наладить работу веб-сокетов. В нашем случае мы обошлись малой кровью, взяв в качестве основы готовый чат-клиент с
html5labs.com. Обратите внимание, что там есть реализация как через нативные веб-сокеты, если они поддерживаются браузером, так и с fallback через Silverlight в противном случае.
Пример, который вы можете скачать по ссылке выше содержит как серверную реализацию поддержки веб-сокетов, так и клиентскую часть. В рамках данной статьи мы ограничимся только клиентом. О сервере в демонстрационном примере нам надо знать только то, что он поддерживает взаимодействие через веб-сокеты по определенному адресу и рассылает полученное сообщение всем клиентам.
Несколько важных деталей:
- Internet Explorer. В IE веб-сокеты поддерживаются нативно, начиная с 10й версии, мы использовали IE10 PP4.
- Firefox. В Firefox веб-сокеты доступны через вендорный префикс, поэтому вместо объекта WebSocket нужно использовать MozWebSocket.
- Opera. В Opera веб-сокеты потребовалось включить вручную через настройки: opera:config -> websockets -> enable.
Чтобы разобраться, как все работает, давайте начнем с простого текстового чата:
<form id="myform"> <input type="text" id="chat" placeholder="type and press enter to chat" /> </form> <ul id="log"></ul>
На отправку формы вешаем событие, которое будет перехватывать сообщение и отправлять его через веб-сокет:
$("#myform").submit(function (event) { event.preventDefault(); // if we're connected // conn -- opened websocket connection if (conn.readyState === 1) { conn.send(JSON.stringify({ sender:sender, // sender ID type:'chat', chat:$('#chat').val() })); log.html('<li class="you">' + $('#chat').val().replace(/[<>&]/g, function (m) { return entities[m]; }) + '</li>' + log[0].innerHTML); $('#chat').value = ''; } });
В коде выше мы проверяем, что соединение готово для использования и отправляем объект в виде json-строки, одновременно добавляя сообщение в лог чата. (Страшная функция с регулярными выражениями просто экранирует "&", "<" и ">".
Теперь давайте разбираться, откуда берется соединение с веб-сокетом. Чтобы начать работать с веб-сокетами, нужно создать соответствующий объект:
if (conn.readyState === undefined || conn.readyState > 1) { // ws -- WebSocket or MozWebSocket conn = new ws('ws://yousite.com:port/chat'); ... }
Далее (внутри) необходимо повесить несколько обработчиков событий от открытого сокета — как минимум на открытие, закрытие и получение сообщения (и, желательно, на возникновение ошибки):
conn.onopen = function () { state.toggleClass('success'); state.text('Socket open'); }; conn.onmessage = function (event) { var message = JSON.parse(event.data); if (message.type == 'chat') { // filter own messages if (message.sender != sender) { log.html('<li class="them">' + message.chat.replace(/[<>&]/g, function (m) { return entities[m]; }) + '</li>' + log[0].innerHTML); } } else { $('#connected').text(message); } }; conn.onclose = function (event) { state.toggleClass('fail'); state.text('Socket closed'); };
Так как новые сообщения рассылаются всем массово, то хорошо бы отфильтровывать собственные, для этого в коде проверяется sender (случайно генерируемый id).

К этому моменту, если вы еще не были знакомы с веб-сокетами (опять-таки, оставляя за рамками статьи серверную часть), вы уже должны вдохновиться легкостью реализации текстового взаимодействия. Ну а самое интересное заключается в том, что написать совместную рисовалку поверх текстового чата тоже оказывается очень просто.
Рисование
Останавливаться на том, как рисовать поверх canvas, пожалуй, будет излишним, так как примеров в сети и так пруд пруди. Остановлюсь только на трех интересных моментах.
Во-первых, довольно неожиданно выяснилось, что Firefox все еще не поддерживает offsetX/offsetY в MouseEvent для определения координат мыши относительно блока, над которым находится курсор. См. также мою статью "T&P. Canvas и Offset". Это не то, чтобы очень большая проблема, но код, конечно, усложняет, если необходимо сделать его полностью кроссбраузерным.
Во-вторых, тоже неожиданно, оказалось, что Chrome при отрисовке в Canvas не включает Shadow и в целом в разных браузерах используется разное сглаживание и чуть разные алгоритмы.
(кликабельно)
И, в-третьих, для моделирования игры в примере используется два холста, соответственно, при передаче сообщений нужно было понимать, на каком именно canvas и что именно нужно будет нарисовать.
Теперь, собственно, к передаче рисования через веб-сокеты. Если в текстовом чате отправка сообщения осуществлялась при отправке формы, то при рисовании разумной представляется отправка непосредственно в процессе отрисовки, поэтому добавляем необходимый код в событие onmousemove, повешенное на canvas:
// canvas - "canvas" object // canvas.source - jquery object for canvas // canvas.context - canvas.source[0].getContext("2d") canvas.source.bind("mousemove", function(e) { if (canvas.isPainting) { var line = {x1:canvas.lastPoint.x, y1:canvas.lastPoint.y, x2: e.offsetX, y2: e.offsetY}; drawLine(canvas.part, line); if (conn.readyState === 1) { conn.send(JSON.stringify({ sender:sender, type:"canvas", part:canvas.part, line:line })); } canvas.lastPoint = {x: e.offsetX, y: e.offsetY}; } });
Обратите внимание на код отправки сообщения — по структуре он ничем не отличается от кода для текста.
Осталось обновить прием сообщений:
conn.onmessage = function (event) { var message = JSON.parse(event.data); if (message.type == 'chat') { ... } else if (message.type == 'canvas') { if (message.sender != sender) { drawLine(message.part, message.line); } } else { ... } };
Здесь тоже все идентично с той лишь разницей, что вместо добавления текста в лог, мы просто отрисовываем линию из сокета тем же механизмом, что и собственные линии (при желании вы можете поменять цвет):
// c1 and c2 - global vars function drawLine(part, line) { var ctx = (part == 1) ? c1.context : c2.context; ctx.beginPath(); ctx.moveTo(line.x1, line.y1); ctx.lineTo(line.x2, line.y2); ctx.closePath(); ctx.stroke(); };
Еще немного лоска добавляется через box-shadow и скрытие второй половинки при рисовании, чтобы не видеть, что рисует другой игрок:

Готовый пример можно скачать c Я.Диска.

