Pull to refresh

Как создать визуальную библиотеку изображений с HTML5 Canvas

Reading time14 min
Views4.4K
Original author: David Catuhe
Сегодня утром, открыв почту, получил очередную рассылку от Code Project, в которой был описан интересный путь создания галереи изображений при помощи Canvas элемента. Статья показалась достаточно интересной и я решил опубликовать ее перевод

Прим. от переводчика: Из статьи были убраны некоторые предложения, рекламирующие 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.

В заключение скажу, что чтобы убедиться в чем-то — нужно это попробовать!
Tags:
Hubs:
+31
Comments13

Articles