Сегодня утром, открыв почту, получил очередную рассылку от Code Project, в которой был описан интересный путь создания галереи из��бражений при помощи Canvas элемента. Статья показалась достаточно интересной и я решил опубликовать ее перевод
Прим. от переводчика: Из статьи были убраны некоторые предложения, рекламирующие IE, и некоторые явно очевидные вещи. Сам я не являюсь сторонником браузера IE и не все, описанные ниже способы, идеальны. Но в качестве обзора особенностей HTML5 и попыток нового применения Canvas статья достаточно интересна.
Ссылка на статью на code project
Ссылка на оригинал
Как фанат пользовательских интерфейсов я не мог упустить возможность разработать что-нибудь с HTML5 Canvas. Этот инструмент предоставляет большое множество новых путей отображения картинок и данных в веб. В этой статье мы пройдем по одному из них.
Мы сделаем приложение, которое позволит нам отображать коллекцию карт Magic the Gathering ©. Пользователям будут доступны прокрутка и зум при использовании мыши (например, как в Bing Maps).

Готовое приложение можно посмотреть здесь: bolaslenses.catuhe.com
Исходники можно скачать здесь: www.catuhe.com/msdn/bolaslenses.zip
Карты сохранены в Windows Azure Storage и используют Azure Content Distribution Network (CDN : сервис, предоставляющий/развертывающий данные рядом с конечными пользователями) для достижения максимальной производительности. ASP.NET сервис используется для возвращения списка карт (используя JSON формат).

Для написания нашего приложения мы будем использовать Visual Studio 2010 SP1 с Web Standards Update. Это расширение добавляет IntelliSense поддержку для HTML5 страниц (это по-настоящему важная вещь).
Наше решение будет содержать HTML5 страницу вместе с .js файлами. Про отладку: Visual Studio позволяет устанавливать точки остановки и работать с ними прямо в своей среде.

Отладка в Visual Studio 2010
И так у нас есть современная среда разработки с IntelliSense и поддержкой отладки. Поэтому мы готовы начать и для начала мы напишем HTML5 страницу.

Наша страница будет построена вокруг HTML5 canvas, который мы будем использовать для рисования карт: код
Если мы посмотрим на нашу страницу, то сможем заметить что она разделена на 2 части:
Также мы добавили файл стилей full.css: файл стилей. Таким образом мы получили следующую страницу:

Стили — это мощный инструмент, позволяющий создавать бесконечное число отображений.
Наш интерфейс теперь готов и мы можем посмотреть, как получать данные о картах для отображения.
Сервер предоставляет список карт, используя JSON формат, по следующей ссылке:
bolaslenses.catuhe.com/Home/ListOfCards/?colorString=0
URL принимает один параметр (colorString) для выбора нужного цвета (0 = all).
Когда разрабатываешь с JavaScript, хорошо бы посмотреть, что мы уже имеем на сегодняшний день (это справедливо и для других языков программирования, но очень важно именно для JavaScript): вы обязаны задаться вопросом о том, не было ли то, что мы собираемся разработать, уже создано в существующих фрэймворках?
И действительно, в мире существует много открытых проектов на JavaScript. Один из них jQuery, который предоставляет изобилие удобных функций.
Таким образом, в нашем случае для подключения к URL нашего сервера и получения списка карт мы можем использовать XmlHttpRequest и веселиться с парсингом возвращаемого JSON. Или мы можем использовать jQuery.
Мы будем использовать функцию getJSON, которая позаботиться обо всем за нас:
Как мы можем заметить, наша функция сохраняет список карт в переменную listOfCards и вызывает 2 функции jQuery:
Список listOfCards содержит объекты в формате:
Нужно заметить, что URL сервера вызывается с “?jsoncallback=?” суффиксом. Ajax вызовы ограничены безопасностью подключения к тому же самому адресу, что и вызываемый скрипт. Однако существует решение, называемое JSONP, которое позволит нам выполнять совместные вызовы к серверу. И к счастью, jQuery может обрабатывать все в одиночку, вам только нужно добавить правильный суффикс.
Как только мы получим наш список карт, мы можем настраивать загрузку и кеширование изображений.
Основной трюк нашего приложения в рисовании только карт, которые видны на экране. Окно отображения определено уровнем зума и отступом (x, y) всей системы.

Вся система определена 14819 картами, которые распространяются большем чем на 200 колонок и 75 строк.
Также мы должны знать, что каждая карта доступна в трех версиях:
Таким образом, в зависимости от уровня зума мы будем загружать нужную версию для оптимизации работы сети.
Чтобы это сделать мы разработаем функцию, которая будет отдавать нужное изображение для карты. В дополнение функция будет ссылаться на изображение качеством ниже, если карта для нужного уровня еще не загружена на сервер:
ImageCache отдает суффикс и нужный кеш.
Здесь представлены 2 важных функции:
Для обработки 3х уровней кеша мы объявим 3 переменные:
Выбор нужного кеша зависит от зума:
Для обратной связи с пользователем мы добавим таймер, который будет управлять тултипом, который отображает количество уже загруженных картинок:
Примечание: лучше использовать jQuery для упрощения анимирования.
А теперь перейдем к разговору об отображении карт.
Для рисования наших карт нам нужно заполнить элемент canvas используя его 2D контекст (который существует только если браузер поддерживает HTML5 canvas):
Рисование будет выполнено функцией processListOfCards (вызывается 60 раз в секунду):
Эта функция построена вокруг нескольких ключевых моментов:
Отрисовка карт в основном опирается на возможность браузера ускорять рендеринг canvas элемента. Для примера, это производительность на моей машине при минимальном уровне зума (0.05):

Сайт даже работает на мобильных телефонах и планшетах в том случае, если они поддерживают HTML5.
Здесь мы можем увидеть внутреннюю силу HTML5 браузеров, которые могут обработать полный экран карт больше чем 30 раз в секунду. Это возможно с аппаратным ускорением (hardware acceleration).
Для нормального просмотра коллекции наших карт нам нужно иметь возможность управления мышью (включая колесико).
Для прокрутки мы просто обрабатываем события onmouvemove, onmouseup и onmousedown.
Onmouseup и onmousedown события будут использоваться для слежения за нажатием мыши:
Событие onmousemove подключено к canvas элементу и используется для перемещения вида:
Эта функция (onMouseMove) высчитывает текущую позицию и предоставляет предыдущее значение в случае перемещения сдвига отображающего окна:
Напоминаю, что jQuery также предоставляет инструменты для управлениями событиями мыши.
Для управления колесиком придется подстраиваться под каждый браузер в отдельности, поскольку они все работают в этом случае по разному:
Функция регистрации события:
Наконец мы добавим немного инерции во время движения мышки (или во время зума) чтобы придать ощущение гладкости:
Подобную небольшую функции несложно реализовать, но зато она улучшит качество работы с пользователем.
Также, для того чтобы сделать просмотр удобнее, мы будем сохранять позицию отображающего окна и зум. Чтобы это осуществить мы используем сервис localStorage, который сохраняет пары ключ/значение на долгое время (данные сохраняются после закрытия браузера) и только доступны текущему window объекту:
Для добавления большего динамизма в наше приложение мы позволим нашим пользователям двойной щелчок (double-click) на карте для зума и фокусировки на ней.
Наша система должна анимировать 3 значения: два отступа (offsets (X, Y)) и зум. Чтобы это сделать, используем функцию, которая будет отвечать за анимацию переменной из исходного до конечного значения с заданной продолжительностю:
Использование функции:
Преимущество AnimationHelper в том, что она способна анимировать множество параметров как вы захотите.
Наконец мы убедимся, что наша страница может также быть просмотрена на планшетах, ПК и даже на телефонах.
Для этого мы используем свойство CSS 3: The media-queries. С этой технологией мы можем применять стили согласно некоторым запросам, таким как конкретный размер экрана:
Здесь мы видим что, если ширина экрана меньше, чем 480 пикселов, то следующий стиль будет добавлен:
Этот стиль будет уменьшать размер заголовка и будет сохранять сайт просматреваемым даже если ширина браузера меньше, чем 480 пикселов (например, на Windows Phone):

HTML5 / CSS 3 / JavaScript и Visual Studio 2010 позволяют разрабатывать портативные и эффективные решения (в пределах браузера, поддерживающего HTML5) с некоторыми отличными возможностями, такими как аппаратное ускорение рендеринга (hardware accelerated rendering).
Такой тип разработки упрощается использованием фреймворков, таких как jQuery.
В заключение скажу, что чтобы убедиться в чем-то — нужно это попробовать!
Прим. от переводчика: Из статьи были убраны некоторые предложения, рекламирующие IE, и некоторые явно очевидные вещи. Сам я не являюсь сторонником браузера IE и не все, описанные ниже способы, идеальны. Но в качестве обзора особенностей HTML5 и попыток нового применения Canvas статья достаточно интересна.
Ссылка на статью на code project
Ссылка на оригинал
Как фанат пользовательских интерфейсов я не мог упустить возможность разработать что-нибудь с HTML5 Canvas. Этот инструмент предоставляет большое множество новых путей отображения картинок и данных в веб. В этой статье мы пройдем по одному из них.
- Обзор приложения
- Инструменты
- Страница на HTML5
- Получение данных
- Загрузка карт & обработка кеша
- Отображение карт
- Управление мышкой
- Сохранение состояния
- Анимация
- Работа с разными устройствами
- Вывод
Обзор приложения
Мы сделаем приложение, которое позволит нам отображать коллекцию карт Magic the Gathering ©. Пользователям будут доступны прокрутка и зум при использовании мыши (например, как в Bing Maps).

Готовое приложение можно посмотреть здесь: bolaslenses.catuhe.com
Исходники можно скачать здесь: www.catuhe.com/msdn/bolaslenses.zip
Карты сохранены в Windows Azure Storage и используют Azure Content Distribution Network (CDN : сервис, предоставляющий/развертывающий данные рядом с конечными пользователями) для достижения максимальной производительности. ASP.NET сервис используется для возвращения списка карт (используя JSON формат).

Инструменты
Для написания нашего приложения мы будем использовать Visual Studio 2010 SP1 с Web Standards Update. Это расширение добавляет IntelliSense поддержку для HTML5 страниц (это по-настоящему важная вещь).
Наше решение будет содержать HTML5 страницу вместе с .js файлами. Про отладку: Visual Studio позволяет устанавливать точки остановки и работать с ними прямо в своей среде.

Отладка в Visual Studio 2010
И так у нас есть современная среда разработки с IntelliSense и поддержкой отладки. Поэтому мы готовы начать и для начала мы напишем HTML5 страницу.

HTML5 страница
Наша страница будет построена вокруг HTML5 canvas, который мы будем использовать для рисования карт: код
Если мы посмотрим на нашу страницу, то сможем заметить что она разделена на 2 части:
- Заголовок с названием, логотипом и специальными ссылками
- Основная часть, содержащая canvas элемент и тултипы, которые будут отображать статус приложения. И скрытое изображение (backImage), используемое как источник для еще не загруженных карт.
Также мы добавили файл стилей full.css: файл стилей. Таким образом мы получили следующую страницу:

Стили — это мощный инструмент, позволяющий создавать бесконечное число отображений.
Наш интерфейс теперь готов и мы можем посмотреть, как получать данные о картах для отображения.
Получение данных
Сервер предоставляет список карт, используя JSON формат, по следующей ссылке:
bolaslenses.catuhe.com/Home/ListOfCards/?colorString=0
URL принимает один параметр (colorString) для выбора нужного цвета (0 = all).
Когда разрабатываешь с JavaScript, хорошо бы посмотреть, что мы уже имеем на сегодняшний день (это справедливо и для других языков программирования, но очень важно именно для JavaScript): вы обязаны задаться вопросом о том, не было ли то, что мы собираемся разработать, уже создано в существующих фрэймворках?
И действительно, в мире существует много открытых проектов на JavaScript. Один из них jQuery, который предоставляет изобилие удобных функций.
Таким образом, в нашем случае для подключения к URL нашего сервера и получения списка карт мы можем использовать XmlHttpRequest и веселиться с парсингом возвращаемого JSON. Или мы можем использовать jQuery.
Мы будем использовать функцию getJSON, которая позаботиться обо всем за нас:
function getListOfCards() { var url = "http://bolaslenses.catuhe.com/Home/ListOfCards/?jsoncallback=?"; $.getJSON(url, { colorString: "0" }, function (data) { listOfCards = data; $("#cardsCount").text(listOfCards.length + " cards displayed"); $("#waitText").slideToggle("fast"); }); }
Как мы можем заметить, наша функция сохраняет список карт в переменную listOfCards и вызывает 2 функции jQuery:
- text — меняет текст тега
- slideToggle — прячет/показывает тег анимацией его высоты
Список listOfCards содержит объекты в формате:
- ID: идентификатор карты
- Path: относительный путь карты (без расширения)
Нужно заметить, что URL сервера вызывается с “?jsoncallback=?” суффиксом. Ajax вызовы ограничены безопасностью подключения к тому же самому адресу, что и вызываемый скрипт. Однако существует решение, называемое JSONP, которое позволит нам выполнять совместные вызовы к серверу. И к счастью, jQuery может обрабатывать все в одиночку, вам только нужно добавить правильный суффикс.
Как только мы получим наш список карт, мы можем настраивать загрузку и кеширование изображений.
Загрузка карт & Обработка кеша
Основной трюк нашего приложения в рисовании только карт, которые видны на экране. Окно отображения определено уровнем зума и отступом (x, y) всей системы.
var visuControl = { zoom : 0.25, offsetX : 0, offsetY : 0 };
Вся система определена 14819 картами, которые распространяются большем чем на 200 колонок и 75 строк.
Также мы должны знать, что каждая карта доступна в трех версиях:
- Высокое разрешение: 480x680 без компрессии (.jpg суффикс)
- Среднее разрешение: 240x340 со стандартной компрессией (.50.jpg суффикс)
- Низкое разрешение: 120x170 с сильной компрессией (.25.jpg суффикс)
Таким образом, в зависимости от уровня зума мы будем загружать нужную версию для оптимизации работы сети.
Чтобы это сделать мы разработаем функцию, которая будет отдавать нужное изображение для карты. В дополнение функция будет ссылаться на изображение качеством ниже, если карта для нужного уровня еще не загружена на сервер:
function imageCache(substr, replacementCache) { var extension = substr; var backImage = document.getElementById("backImage"); this.load = function (card) { var localCache = this; if (this[card.ID] != undefined) return; var img = new Image(); localCache[card.ID] = { image: img, isLoaded: false }; currentDownloads++; img.onload = function () { localCache[card.ID].isLoaded = true; currentDownloads--; }; img.onerror = function() { currentDownloads--; }; img.src = "http://az30809.vo.msecnd.net/" + card.Path + extension; }; this.getReplacementFromLowerCache = function (card) { if (replacementCache == undefined) return backImage; return replacementCache.getImageForCard(card); }; this.getImageForCard = function(card) { var img; if (this[card.ID] == undefined) { this.load(card); img = this.getReplacementFromLowerCache(card); } else { if (this[card.ID].isLoaded) img = this[card.ID].image; else img = this.getReplacementFromLowerCache(card); } return img; }; }
ImageCache отдает суффикс и нужный кеш.
Здесь представлены 2 важных функции:
- load: эта функция будет загружать нужную картинку и будет сохранять ее в кеш (msecnd.net url — это адрес карты в Azure CDN)
- getImageForCard: эта функция возвращает картинку из кеша, если она уже была загружена ранее или загружает ее по новой укладывает ее в кеш
Для обработки 3х уровней кеша мы объявим 3 переменные:
var imagesCache25 = new imageCache(".25.jpg"); var imagesCache50 = new imageCache(".50.jpg", imagesCache25); var imagesCacheFull = new imageCache(".jpg", imagesCache50);
Выбор нужного кеша зависит от зума:
function getCorrectImageCache() { if (visuControl.zoom <= 0.25) return imagesCache25; if (visuControl.zoom <= 0.8) return imagesCache50; return imagesCacheFull; }
Для обратной связи с пользователем мы добавим таймер, который будет управлять тултипом, который отображает количество уже загруженных картинок:
function updateStats() { var stats = $("#stats"); stats.html(currentDownloads + " card(s) currently downloaded."); if (currentDownloads == 0 && statsVisible) { statsVisible = false; stats.slideToggle("fast"); } else if (currentDownloads > 1 && !statsVisible) { statsVisible = true; stats.slideToggle("fast"); } } setInterval(updateStats, 200);
Примечание: лучше использовать jQuery для упрощения анимирования.
А теперь перейдем к разговору об отображении карт.
Отображение карт
Для рисования наших карт нам нужно заполнить элемент canvas используя его 2D контекст (который существует только если браузер поддерживает HTML5 canvas):
var mainCanvas = document.getElementById("mainCanvas"); var drawingContext = mainCanvas.getContext('2d');
Рисование будет выполнено функцией processListOfCards (вызывается 60 раз в секунду):
function processListOfCards() { if (listOfCards == undefined) { drawWaitMessage(); return; } mainCanvas.width = document.getElementById("center").clientWidth; mainCanvas.height = document.getElementById("center").clientHeight; totalCards = listOfCards.length; var localCardWidth = cardWidth * visuControl.zoom; var localCardHeight = cardHeight * visuControl.zoom; var effectiveTotalCardsInWidth = colsCount * localCardWidth; var rowsCount = Math.ceil(totalCards / colsCount); var effectiveTotalCardsInHeight = rowsCount * localCardHeight; initialX = (mainCanvas.width - effectiveTotalCardsInWidth) / 2.0 - localCardWidth / 2.0; initialY = (mainCanvas.height - effectiveTotalCardsInHeight) / 2.0 - localCardHeight / 2.0; // Clear clearCanvas(); // Computing of the viewing area var initialOffsetX = initialX + visuControl.offsetX * visuControl.zoom; var initialOffsetY = initialY + visuControl.offsetY * visuControl.zoom; var startX = Math.max(Math.floor(-initialOffsetX / localCardWidth) - 1, 0); var startY = Math.max(Math.floor(-initialOffsetY / localCardHeight) - 1, 0); var endX = Math.min(startX + Math.floor((mainCanvas.width - initialOffsetX - startX * localCardWidth) / localCardWidth) + 1, colsCount); var endY = Math.min(startY + Math.floor((mainCanvas.height - initialOffsetY - startY * localCardHeight) / localCardHeight) + 1, rowsCount); // Getting current cache var imageCache = getCorrectImageCache(); // Render for (var y = startY; y < endY; y++) { for (var x = startX; x < endX; x++) { var localX = x * localCardWidth + initialOffsetX; var localY = y * localCardHeight + initialOffsetY; // Clip if (localX > mainCanvas.width) continue; if (localY > mainCanvas.height) continue; if (localX + localCardWidth < 0) continue; if (localY + localCardHeight < 0) continue; var card = listOfCards[x + y * colsCount]; if (card == undefined) continue; // Get from cache var img = imageCache.getImageForCard(card); // Render try { if (img != undefined) drawingContext.drawImage(img, localX, localY, localCardWidth, localCardHeight); } catch (e) { $.grep(listOfCards, function (item) { return item.image != img; }); } } }; // Scroll bars drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY); // FPS computeFPS(); }
Эта функция построена вокруг нескольких ключевых моментов:
- Если список карт еще не загружен, мы отображаем подсказку, указывающую что загрузка еще в процессе:
var pointCount = 0; function drawWaitMessage() { pointCount++; if (pointCount > 200) pointCount = 0; var points = ""; for (var index = 0; index < pointCount / 10; index++) points += "."; $("#waitText").html("Loading...Please wait<br>" + points); }
- В последствии мы определяем позицию отображающего окна (в перерасчете на карты и координаты), затем мы очищаем canvas:
function clearCanvas() { mainCanvas.width = document.body.clientWidth - 50; mainCanvas.height = document.body.clientHeight - 140; drawingContext.fillStyle = "rgb(0, 0, 0)"; drawingContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height); }
- Затем мы отображаем список карт и вызываем функцию canvas контекста drawImage. Конкретное изображение предоставлено активным кешем (зависит от зума):
// Get from cache var img = imageCache.getImageForCard(card); // Render try { if (img != undefined) drawingContext.drawImage(img, localX, localY, localCardWidth, localCardHeight); } catch (e) { $.grep(listOfCards, function (item) { return item.image != img; });
- Нам также нужно нарисовать полосу прокрутки при помощи функции RoundedRectangle, которая использует квадратичные кривые:
function roundedRectangle(x, y, width, height, radius) { drawingContext.beginPath(); drawingContext.moveTo(x + radius, y); drawingContext.lineTo(x + width - radius, y); drawingContext.quadraticCurveTo(x + width, y, x + width, y + radius); drawingContext.lineTo(x + width, y + height - radius); drawingContext.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); drawingContext.lineTo(x + radius, y + height); drawingContext.quadraticCurveTo(x, y + height, x, y + height - radius); drawingContext.lineTo(x, y + radius); drawingContext.quadraticCurveTo(x, y, x + radius, y); drawingContext.closePath(); drawingContext.stroke(); drawingContext.fill(); } function drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY) { drawingContext.fillStyle = "rgba(255, 255, 255, 0.6)"; drawingContext.lineWidth = 2; // Vertical var totalScrollHeight = effectiveTotalCardsInHeight + mainCanvas.height; var scaleHeight = mainCanvas.height - 20; var scrollHeight = mainCanvas.height / totalScrollHeight; var scrollStartY = (-initialOffsetY + mainCanvas.height * 0.5) / totalScrollHeight; roundedRectangle(mainCanvas.width - 8, scrollStartY * scaleHeight + 10, 5, scrollHeight * scaleHeight, 4); // Horizontal var totalScrollWidth = effectiveTotalCardsInWidth + mainCanvas.width; var scaleWidth = mainCanvas.width - 20; var scrollWidth = mainCanvas.width / totalScrollWidth; var scrollStartX = (-initialOffsetX + mainCanvas.width * 0.5) / totalScrollWidth; roundedRectangle(scrollStartX * scaleWidth + 10, mainCanvas.height - 8, scrollWidth * scaleWidth, 5, 4); }
- И наконец нам нужно вычислить число фрэймов в секунду:
function computeFPS() { if (previous.length > 60) { previous.splice(0, 1); } var start = (new Date).getTime(); previous.push(start); var sum = 0; for (var id = 0; id < previous.length - 1; id++) { sum += previous[id + 1] - previous[id]; } var diff = 1000.0 / (sum / previous.length); $("#cardsCount").text(diff.toFixed() + " fps. " + listOfCards.length + " cards displayed"); }
Отрисовка карт в основном опирается на возможность браузера ускорять рендеринг canvas элемента. Для примера, это производительность на моей машине при минимальном уровне зума (0.05):

| Браузер |
FPS |
| Internet Explorer 9 | 30 |
| Firefox 5 | 30 |
| Chrome 12 | 17 |
| iPad (при уровне зума 0.8) | 7 |
| Windows Phone Mango (при уровне зума 0.8) | 20 (!!) |
Сайт даже работает на мобильных телефонах и планшетах в том случае, если они поддерживают HTML5.
Здесь мы можем увидеть внутреннюю силу HTML5 браузеров, которые могут обработать полный экран карт больше чем 30 раз в секунду. Это возможно с аппаратным ускорением (hardware acceleration).
Управление мышью
Для нормального просмотра коллекции наших карт нам нужно иметь возможность управления мышью (включая колесико).
Для прокрутки мы просто обрабатываем события onmouvemove, onmouseup и onmousedown.
Onmouseup и onmousedown события будут использоваться для слежения за нажатием мыши:
var mouseDown = 0; document.body.onmousedown = function (e) { mouseDown = 1; getMousePosition(e); previousX = posx; previousY = posy; }; document.body.onmouseup = function () { mouseDown = 0; };
Событие onmousemove подключено к canvas элементу и используется для перемещения вида:
var previousX = 0; var previousY = 0; var posx = 0; var posy = 0; function getMousePosition(eventArgs) { var e; if (!eventArgs) e = window.event; else { e = eventArgs; } if (e.offsetX || e.offsetY) { posx = e.offsetX; posy = e.offsetY; } else if (e.clientX || e.clientY) { posx = e.clientX; posy = e.clientY; } } function onMouseMove(e) { if (!mouseDown) return; getMousePosition(e); mouseMoveFunc(posx, posy, previousX, previousY); previousX = posx; previousY = posy; }
Эта функция (onMouseMove) высчитывает текущую позицию и предоставляет предыдущее значение в случае перемещения сдвига отображающего окна:
function Move(posx, posy, previousX, previousY) { currentAddX = (posx - previousX) / visuControl.zoom; currentAddY = (posy - previousY) / visuControl.zoom; } MouseHelper.registerMouseMove(mainCanvas, Move);
Напоминаю, что jQuery также предоставляет инструменты для управлениями событиями мыши.
Для управления колесиком придется подстраиваться под каждый браузер в отдельности, поскольку они все работают в этом случае по разному:
function wheel(event) { var delta = 0; if (event.wheelDelta) { delta = event.wheelDelta / 120; if (window.opera) delta = -delta; } else if (event.detail) { /** Mozilla case. */ delta = -event.detail / 3; } if (delta) { wheelFunc(delta); } if (event.preventDefault) event.preventDefault(); event.returnValue = false; }
Функция регистрации события:
MouseHelper.registerWheel = function (func) { wheelFunc = func; if (window.addEventListener) window.addEventListener('DOMMouseScroll', wheel, false); window.onmousewheel = document.onmousewheel = wheel; }; // Использование MouseHelper.registerWheel(function (delta) { currentAddZoom += delta / 500.0; });
Наконец мы добавим немного инерции во время движения мышки (или во время зума) чтобы придать ощущение гладкости:
// Инерция var inertia = 0.92; var currentAddX = 0; var currentAddY = 0; var currentAddZoom = 0; function doInertia() { visuControl.offsetX += currentAddX; visuControl.offsetY += currentAddY; visuControl.zoom += currentAddZoom; var effectiveTotalCardsInWidth = colsCount * cardWidth; var rowsCount = Math.ceil(totalCards / colsCount); var effectiveTotalCardsInHeight = rowsCount * cardHeight var maxOffsetX = effectiveTotalCardsInWidth / 2.0; var maxOffsetY = effectiveTotalCardsInHeight / 2.0; if (visuControl.offsetX < -maxOffsetX + cardWidth) visuControl.offsetX = -maxOffsetX + cardWidth; else if (visuControl.offsetX > maxOffsetX) visuControl.offsetX = maxOffsetX; if (visuControl.offsetY < -maxOffsetY + cardHeight) visuControl.offsetY = -maxOffsetY + cardHeight; else if (visuControl.offsetY > maxOffsetY) visuControl.offsetY = maxOffsetY; if (visuControl.zoom < 0.05) visuControl.zoom = 0.05; else if (visuControl.zoom > 1) visuControl.zoom = 1; processListOfCards(); currentAddX *= inertia; currentAddY *= inertia; currentAddZoom *= inertia; // Epsilon if (Math.abs(currentAddX) < 0.001) currentAddX = 0; if (Math.abs(currentAddY) < 0.001) currentAddY = 0; }
Подобную небольшую функции несложно реализовать, но зато она улучшит качество работы с пользователем.
Сохранение состояния
Также, для того чтобы сделать просмотр удобнее, мы будем сохранять позицию отображающего окна и зум. Чтобы это осуществить мы используем сервис localStorage, который сохраняет пары ключ/значение на долгое время (данные сохраняются после закрытия браузера) и только доступны текущему window объекту:
function saveConfig() { if (window.localStorage == undefined) return; // Zoom window.localStorage["zoom"] = visuControl.zoom; // Offsets window.localStorage["offsetX"] = visuControl.offsetX; window.localStorage["offsetY"] = visuControl.offsetY; } // Restore data if (window.localStorage != undefined) { var storedZoom = window.localStorage["zoom"]; if (storedZoom != undefined) visuControl.zoom = parseFloat(storedZoom); var storedoffsetX = window.localStorage["offsetX"]; if (storedoffsetX != undefined) visuControl.offsetX = parseFloat(storedoffsetX); var storedoffsetY = window.localStorage["offsetY"]; if (storedoffsetY != undefined) visuControl.offsetY = parseFloat(storedoffsetY); }
Анимация
Для добавления большего динамизма в наше приложение мы позволим нашим пользователям двойной щелчок (double-click) на карте для зума и фокусировки на ней.
Наша система должна анимировать 3 значения: два отступа (offsets (X, Y)) и зум. Чтобы это сделать, используем функцию, которая будет отвечать за анимацию переменной из исходного до конечного значения с заданной продолжительностю:
var AnimationHelper = function (root, name) { var paramName = name; this.animate = function (current, to, duration) { var offset = (to - current); var ticks = Math.floor(duration / 16); var offsetPart = offset / ticks; var ticksCount = 0; var intervalID = setInterval(function () { current += offsetPart; root[paramName] = current; ticksCount++; if (ticksCount == ticks) { clearInterval(intervalID); root[paramName] = to; } }, 16); }; };
Использование функции:
// Подготовка параметров анимации var zoomAnimationHelper = new AnimationHelper(visuControl, "zoom"); var offsetXAnimationHelper = new AnimationHelper(visuControl, "offsetX"); var offsetYAnimationHelper = new AnimationHelper(visuControl, "offsetY"); var speed = 1.1 - visuControl.zoom; zoomAnimationHelper.animate(visuControl.zoom, 1.0, 1000 * speed); offsetXAnimationHelper.animate(visuControl.offsetX, targetOffsetX, 1000 * speed); offsetYAnimationHelper.animate(visuControl.offsetY, targetOffsetY, 1000 * speed);
Преимущество AnimationHelper в том, что она способна анимировать множество параметров как вы захотите.
Работа с разными устройствами
Наконец мы убедимся, что наша страница может также быть просмотрена на планшетах, ПК и даже на телефонах.
Для этого мы используем свойство CSS 3: The media-queries. С этой технологией мы можем применять стили согласно некоторым запросам, таким как конкретный размер экрана:
<link href="Content/full.css" rel="stylesheet" type="text/css" /> <link href="Content/mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 480px)" /> <link href="Content/mobile.css" rel="stylesheet" type="text/css" media="screen and (max-device-width: 480px)" />
Здесь мы видим что, если ширина экрана меньше, чем 480 пикселов, то следующий стиль будет добавлен:
#legal { font-size: 8px; } #title { font-size: 30px !important; } #waitText { font-size: 12px; } #bolasLogo { width: 48px; height: 48px; } #pictureCell { width: 48px; }
Этот стиль будет уменьшать размер заголовка и будет сохранять сайт просматреваемым даже если ширина браузера меньше, чем 480 пикселов (например, на Windows Phone):

Вывод
HTML5 / CSS 3 / JavaScript и Visual Studio 2010 позволяют разрабатывать портативные и эффективные решения (в пределах браузера, поддерживающего HTML5) с некоторыми отличными возможностями, такими как аппаратное ускорение рендеринга (hardware accelerated rendering).
Такой тип разработки упрощается использованием фреймворков, таких как jQuery.
В заключение скажу, что чтобы убедиться в чем-то — нужно это попробовать!
