На HTML5 Camp в рамках открытия мы показывали несколько демок с использованием новых веб-технологий. Там были как сторонние проекты и решения вроде Disney Tron Legacy и Santa's Media Queries, так и ряд примеров, подготовленных специально под мероприятие.
Одним из таких примеров был чат, работающий поверх веб-сокетов с расширенным функционалом, позволяющим совместно рисовать в реальном времени. Это в чем-то напоминает известную детскую игру, когда участники по-очереди или одновременно рисуют разные части какого-то животного, а потом складывают фрагменты вместе и смотрят, что получилось.
В этой статье я вкратце расскажу, как такая рисовалка устроена и с какими сложностями пришлось столкнуться. Сразу отмечу, что это не готовый продукт, а всего лишь прототип.
Все началось с того, что нужно было наладить работу веб-сокетов. В нашем случае мы обошлись малой кровью, взяв в качестве основы готовый чат-клиент с
html5labs.com. Обратите внимание, что там есть реализация как через нативные веб-сокеты, если они поддерживаются браузером, так и с fallback через Silverlight в противном случае.
Пример, который вы можете скачать по ссылке выше содержит как серверную реализацию поддержки веб-сокетов, так и клиентскую часть. В рамках данной статьи мы ограничимся только клиентом. О сервере в демонстрационном примере нам надо знать только то, что он поддерживает взаимодействие через веб-сокеты по определенному адресу и рассылает полученное сообщение всем клиентам.
Несколько важных деталей:
Чтобы разобраться, как все работает, давайте начнем с простого текстового чата:
На отправку формы вешаем событие, которое будет перехватывать сообщение и отправлять его через веб-сокет:
В коде выше мы проверяем, что соединение готово для использования и отправляем объект в виде json-строки, одновременно добавляя сообщение в лог чата. (Страшная функция с регулярными выражениями просто экранирует "&", "<" и ">".
Теперь давайте разбираться, откуда берется соединение с веб-сокетом. Чтобы начать работать с веб-сокетами, нужно создать соответствующий объект:
Далее (внутри) необходимо повесить несколько обработчиков событий от открытого сокета — как минимум на открытие, закрытие и получение сообщения (и, желательно, на возникновение ошибки):
Так как новые сообщения рассылаются всем массово, то хорошо бы отфильтровывать собственные, для этого в коде проверяется sender (случайно генерируемый id).
К этому моменту, если вы еще не были знакомы с веб-сокетами (опять-таки, оставляя за рамками статьи серверную часть), вы уже должны вдохновиться легкостью реализации текстового взаимодействия. Ну а самое интересное заключается в том, что написать совместную рисовалку поверх текстового чата тоже оказывается очень просто.
Останавливаться на том, как рисовать поверх canvas, пожалуй, будет излишним, так как примеров в сети и так пруд пруди. Остановлюсь только на трех интересных моментах.
Во-первых, довольно неожиданно выяснилось, что Firefox все еще не поддерживает offsetX/offsetY в MouseEvent для определения координат мыши относительно блока, над которым находится курсор. См. также мою статью "T&P. Canvas и Offset". Это не то, чтобы очень большая проблема, но код, конечно, усложняет, если необходимо сделать его полностью кроссбраузерным.
Во-вторых, тоже неожиданно, оказалось, что Chrome при отрисовке в Canvas не включает Shadow и в целом в разных браузерах используется разное сглаживание и чуть разные алгоритмы.
(кликабельно)
И, в-третьих, для моделирования игры в примере используется два холста, соответственно, при передаче сообщений нужно было понимать, на каком именно canvas и что именно нужно будет нарисовать.
Теперь, собственно, к передаче рисования через веб-сокеты. Если в текстовом чате отправка сообщения осуществлялась при отправке формы, то при рисовании разумной представляется отправка непосредственно в процессе отрисовки, поэтому добавляем необходимый код в событие onmousemove, повешенное на canvas:
Обратите внимание на код отправки сообщения — по структуре он ничем не отличается от кода для текста.
Осталось обновить прием сообщений:
Здесь тоже все идентично с той лишь разницей, что вместо добавления текста в лог, мы просто отрисовываем линию из сокета тем же механизмом, что и собственные линии (при желании вы можете поменять цвет):
Еще немного лоска добавляется через box-shadow и скрытие второй половинки при рисовании, чтобы не видеть, что рисует другой игрок:
Готовый пример можно скачать c Я.Диска.
Одним из таких примеров был чат, работающий поверх веб-сокетов с расширенным функционалом, позволяющим совместно рисовать в реальном времени. Это в чем-то напоминает известную детскую игру, когда участники по-очереди или одновременно рисуют разные части какого-то животного, а потом складывают фрагменты вместе и смотрят, что получилось.
В этой статье я вкратце расскажу, как такая рисовалка устроена и с какими сложностями пришлось столкнуться. Сразу отмечу, что это не готовый продукт, а всего лишь прототип.
Веб-сокеты
Все началось с того, что нужно было наладить работу веб-сокетов. В нашем случае мы обошлись малой кровью, взяв в качестве основы готовый чат-клиент с
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 Я.Диска.