В моей
статье о Суперпоезде я продемонстрировал, как использовать элемент HTML
canvas для того, чтобы отобразить динамически изменяющуюся
информацию с серверной стороны, используя AJAX. В приведённом тогда примере
пользователь всего лишь пассивно принимал инфомацию. А в этой статье я
продемонстрирую, как воспользоваться вводом пользователя для того, чтобы
перевести приложения с использованием canvas на новый уровень
интерактивности.
Но сперва нам потребуется перехватить некоторые события в области
canvas. В моей
статье о Суперпоезде я отметил некоторые недостатки элемента
canvas: 1) он поддерживается только в Firefox и Safari и 2) не
предоставляет возможности отображать текст. За последние несколько месяцев
эти ограничения разметало растущее сообщество использующих
canvas разработчиков. На данный момент существует по крайней
мере два различных способа отобразить текст. Один был разработан
Бенджамином Джоффом, а другой
Михаем Парпаритой. Я
передал метод Бенджамина
drawString в
http://awordlike.com/ и немного
подправил, сделав его почти что клоном
Визуального Тезауруса. Другой
же недостаток меня добил: Internet Explorer не поддерживает элемент
canvas (и не будет). Некоторые использующие
canvas разработчики (включая
Emil Eklund) довольно шустро взялись за эту
проблему, и даже достигли некоторого прогресса, однако, благодаря последней
версии
проекта ExplorerCanvas, поддержанного Google, теперь
canvas может использоваться повсеместно.
В этой статье я не буду использовать текст, но пример будет поддерживаться в Internet Explorer (а также в Safari и Firefox). Ну, а теперь... шоу начинается...
Детский зоопарк Косли в Витоне, штат Иллинойс, заказал мне разработку интерактивной клиентской части, которая бы позволяла зоологам записывать перемещения недавно родившегося малыша красной белки в то время, как он носится по всему зоопарку. Зоологи использовали планшетные ПК с установленным на них Internet Explorer 6, а детский зоопарк был оборудован сетью WiFi. Все зоологи разделили между собой обязанность присматривать за малышом красной белки и желали иметь возможность знать его местоположение в любое время. Они хотели иметь возможность без проблем обновлять информацию о его перемещениях сразу же, как только малыш появлялся на глаза.
Я имел под рукой удобный AJAX-инструмент для
canvas, а потому моё решение не должно вас удивить. На
протяжении статьи я построю по кирпичикам первый вариант, готовый для
установки на веб-сервер Косли и демонстрации зоологам (здесь можно посмотреть
работающий пример). Я начну с HTML-страницы с элементом
canvas, который будет приблизительно отображать область, в
которой разгуливает малыш красной белки.
<html>
<head>
<script type="text/javascript" src="excanvas.js"></script>
</head>
<body>
<canvas id="zoo" width="500" height="300" style="border: 1px solid
black"></canvas>
</body>
</html>
Единственный момент, который может вызвать интерес, заключается во внедрении ExplorerCanvas, кроме которого мне ничего больше не понадобится, чтобы canvas отобразился в Internet Explorer. Далее я нарисую значок, который будет представлять собой белку. Поскольку я довольно ленив, это будет кружок.
<html>
<head>
<script type="text/javascript" src="excanvas.js"></script>
<script type="text/javascript" src="prototype-1.4.0.js"></script>
<script type="text/javascript">
window.onload = function() {
var context = $("zoo").getContext("2d");
context.beginPath();
context.arc(50, 50, 10, 0, 2*Math.PI, false);
context.closePath();
context.fill();
};
</script>
</head>
<body>
<canvas id="zoo" width="500" height="300" style="border: 1px solid
black"></canvas>
</body>
</html>
И вновь ничего выдающегося. Я подключил библиотеку
Prototype для JavaScript, поскольку
я ленив, и мне проще напечатать
$(), чем
document.getElementById(). Кроме того, чуть позже я воспользуюсь
Prototype для AJAX. А ещё я нарисовал окружность и закрасил её. Теперь я хочу
подвигать белкой.
<html>
<head>
<script type="text/javascript" src="excanvas.js"></script>
<script type="text/javascript" src="prototype-1.4.0.js"></script>
<script type="text/javascript">
window.onload = function() {
if ( document.addEventListener ) {
document.addEventListener("click", onClick, false);
} else if ( document.attachEvent ) {
document.attachEvent("onclick", onClick);
} else {
alert("Your browser will not work for this example.");
}
};
function onClick(e) {
var context = $("zoo").getContext("2d");
var position = getRelativePosition(e);
context.clearRect(0, 0, $("zoo").width, $("zoo").height);
context.beginPath();
context.arc(position.x, position.y, 10, 0, 2*Math.PI, false);
context.closePath();
context.fill();
}
function getRelativePosition(e) {
var t = $("zoo");
var x = e.clientX+(window.pageXOffset||0);
var y = e.clientY+(window.pageYOffset||0);
do
x-=t.offsetLeft+parseInt(t.style.borderLeftWidth||0),
y-=t.offsetTop+parseInt(t.style.borderTopWidth||0);
while (t=t.offsetParent);
return {x:x,y:y};
}
</script>
</head>
<body>
<canvas id="zoo" width="500" height="300" style="border: 1px solid
black"></canvas>
</body>
</html>
Наступили старые недобрые деньки для JavaScript... Internet Explorer
использует перехватчики событий иначе, чем другие браузеры, а потому мне
пришлось добавить немного условной логики в метод
onload. Различные браузеры также по-разному передают координаты
canvas, а потому мне потребовалось создать функцию
getRelativePosition (найдена с помощью
the canvas-developers group), чтобы получить нужные мне координаты. Я вынес
отрисовку в функцию
onClick и добавил вызов
clearRect, очищающий экран до перерисовки белки.
Таким образом, я разработал всё это без сервера и вообще без AJAX. Зоологи
могли бы начать использовать прогу хоть прямо сейчас, но они не могли бы
видеть поправки друг друга: каждый зоолог имел бы доступ лишь к своей
собственной информации о перемещениях. Настало время организовать работу с
сервером, чтобы позволить зоологам совместно отслеживать белку. Как и в моей
последней статье, я воспользуюсь сервером Руби
WEBrick для того, чтобы сохранить простоту. Начну с опроса
сервера о местоположении белки и обновления
canvas с учётом её координат.
require 'webrick'
include WEBrick
server = HTTPServer.new( :Port => 8053 )
server.mount("/", HTTPServlet::FileHandler, ".")
server.mount_proc("/squirrel/location") do |request, response|
response['Content-Type'] = "text/plain"
response.body = '({"x":50,"y":50})'
end
trap("INT") { server.shutdown }
server.start
Сервер будет использовать свою текущую директорию для хранения документов. Я также приделал прерывание, которое будет отвечать на обновление http://localhost:8053/squirrel/location информацией о жёстко заданных координатах JSON.
<html>
<head>
<script type="text/javascript" src="excanvas.js"></script>
<script type="text/javascript" src="prototype-1.4.0.js"></script>
<script type="text/javascript">
window.onload = function() {
startPolling();
setupClick();
};
function startPolling() {
new PeriodicalExecuter(function() {
new Ajax.Request('/squirrel/location',
{ onComplete: function(request) {
var jsonData = eval(request.responseText);
if (jsonData == undefined) { return; }
draw(jsonData);
}});
}, 1);
}
function setupClick() {
if ( document.addEventListener ) {
document.addEventListener("click", onClick, false);
} else if ( document.attachEvent ) {
document.attachEvent("onclick", onClick);
} else {
alert("Your browser will not work for this example.");
}
}
function onClick(e) {
draw(getRelativePosition(e));
}
function draw(position) {
var context = $("zoo").getContext("2d");
context.clearRect(0, 0, $("zoo").width, $("zoo").height);
context.beginPath();
context.arc(position.x, position.y, 10, 0, 2*Math.PI, false);
context.closePath();
context.fill();
}
function getRelativePosition(e) {
var t = $("zoo");
var x = e.clientX+(window.pageXOffset||0);
var y = e.clientY+(window.pageYOffset||0);
do
x-=t.offsetLeft+parseInt(t.style.borderLeftWidth||0),
y-=t.offsetTop+parseInt(t.style.borderTopWidth||0);
while (t=t.offsetParent);
return {x:x,y:y};
}
</script>
</head>
<body>
<canvas id="zoo" width="500" height="300" style="border: 1px solid
black"></canvas>
</body>
</html>
Мне потребовалось сделать пару изменений в коде клиента. Я переработал
метод
onload с использованием более декларативного стиля, поскольку
код в нём несколько разбух. Основным дополнением стала функция
startPolling. Она соединяет два класса Prototype для опроса
сервера с помощью AJAX насчёт положения белки раз в секунду. Она с помощью
eval обрабатывает асинхронный ответ JSON и перерисовывает белку
в новом месте. Отправляйтесь на
http://localhost:8053/InteractiveCanvas.html, и вы увидите белку,
сидящую в верхнем левом углу.
Проблема заключается в том, что зоологи могут видеть, где находилась белка, но не могут сообщить серверу, где она находится в данный момент (милая маленькая зверушка всё время возвращается в угол). Ещё один вызов AJAX, и работа будет завершена. Сперва я подправлю сервер, подкрутив ещё одно прерывание, которое будет работать с обновлением координат.
require 'webrick'
include WEBrick
server = HTTPServer.new( :Port => 8053 )
server.mount("/", HTTPServlet::FileHandler, ".")
$location = [50, 50]
def location_json
"({\"x\":#{$location[0]},\"y\"<WBR>:#{$location[1]}})"
end
server.mount_proc("/squirrel/location") do |request, response|
response['Content-Type'] = "text/plain"
response.body = location_json
end
server.mount_proc("/squirrel/update") do |request, response|
$location = [ request.query["x"].to_i, request.query["y"].to_i ]
response['Content-Type'] = "text/plain"
response.body = location_json
end
trap("INT") { server.shutdown }
server.start
Обновлённое местоположение извлекается из параметров запроса
"/squirrel/update", хранимых в глобальной переменной и
преобразовывается в JSON по пути назад к клиенту.
<html>
<head>
<script type="text/javascript" src="excanvas.js"></script>
<script type="text/javascript" src="prototype-1.4.0.js"></script>
<script type="text/javascript">
window.onload = function() {
startPolling();
setupClick();
};
function startPolling() {
new PeriodicalExecuter(function() {
new Ajax.Request('/squirrel/location', { onComplete: draw });
}, 1);
}
function setupClick() {
if ( document.addEventListener ) {
document.addEventListener("click", onClick, false);
} else if ( document.attachEvent ) {
document.attachEvent("onclick", onClick);
} else {
alert("Your browser will not work for this example.");
}
}
function onClick(e) {
var position = getRelativePosition(e);
new Ajax.Request('/squirrel/update',
{ parameters: "x=" + position.x + "&y=" + position.y,
onComplete: draw });
}
function draw(request) {
var position = eval(request.responseText);
if (position == undefined) { return; }
var context = $("zoo").getContext("2d");
context.clearRect(0, 0, $("zoo").width, $("zoo").height);
context.beginPath();
context.arc(position.x, position.y, 10, 0, 2*Math.PI, false);
context.closePath();
context.fill();
}
function getRelativePosition(e) {
var t = $("zoo");
var x = e.clientX+(window.pageXOffset||0);
var y = e.clientY+(window.pageYOffset||0);
do
x-=t.offsetLeft+parseInt(t.style.borderLeftWidth||0),
y-=t.offsetTop+parseInt(t.style.borderTopWidth||0);
while (t=t.offsetParent);
return {x:x,y:y};
}
</script>
</head>
<body>
<canvas id="zoo" width="500" height="300" style="border: 1px solid
black"></canvas>
</body>
</html>
Я добавил
Ajax.Request в функцию
onClick, который посылает обновлённые координаты серверу. Затем
я переделал обработку JSON в функцию
draw, чтобы я мог передавать функцию
draw обоим вызовам
onComplete в
Ajax.Request. Вот и всё! Зоологи теперь могут наблюдать за
малышом красной белки и обновлять информацию о его местоположении. Было бы
несложно расширить этот пример, чтобы помещать обновлённую информацию в базу
данных с целью иметь возможность отображать динамику перемещений и выявлять
тенденции во времени.