От автора: Адаптивные изображения были одной из сложнейших проблем адаптивного веб-дизайна, и остаются ею по сей день. Пока у браузерных вендоров имеется «родное» решение, нам приходится думать «на ходу» и находить собственные решения. Изображения «Retina» особенно проблемны, потому что если вы масштабировали свою разметку em’ами или в процентах (как и должно быть!), то не можете быть уверены в точных пиксельных размерах каждого показываемого изображения.
В этой статье мы рассмотрим одно из решений проблемы, реализованное нами в своем сайте-портфолио на Etch, где можно видеть раннюю работающую версию во всей ее красе.
Требования
Для Etch мы применили подход content-first. Мы знали, что нужно использовать множество изображений, чтобы быстро передать атмосферу компании. Они будут сопровождаться маленькими фрагментами, или «фразами», текста.
Следующее решение было принято касательно размера изображений и их формата. Чтобы максимально контролировать дизайн, нужен был идеальный контроль над изображениями. Мы решили использовать Instagram* в качестве базы для своих изображений по следующим причинам:
Фиксированное соотношение размеров.
Большая часть служащих здесь его уже используют.
Такие прекрасные фильтры.
Instagram* позволяет максимальный размер изображения в 600 пикселей, поэтому у нас для работы уже имелся первый набор контентных ограничений: изображения с соотношением размеров 1:1 и максимальным размером изображений 600 × 600. Жесткие требования касательно содержимого облегчили процесс дизайна, потому что ограничили наши возможности, делая, таким образом, все решения вынужденными.
Покончив с контентом, мы начали рассматривать дизайн. И снова для сохранения максимального контроля сошлись на стиле адаптивного дизайна с фиксированными размерами колонок. Мы применили блочные элементы сетки, соответствующие максимальному размеру наших изображений. Каждый блок сетки будет либо 600 × 600, либо 300 × 300, что тоже удобно подходило к черновому наброску минимальной ширины окна просмотра вебсайта в 320 пикселей.
Во время тестирования дизайнерского процесса было решено, что нам потребуются еще два размера изображений: контрольные 100 × 100 и основные образы, простирающиеся во всю ширину содержимого (300, 600, 900, 1200, 1600, 1800). Кроме того, все изображения придется подготовить к совместимости с Retina — или, по-другому, к высокоплотным экранам. Так мы добрались до последнего набора требований к адаптивным изображениям этого вебсайта:
Потенциальная ширина изображений (в пикселях): 100, 300, 600, 900, 1200, 1600, 1800
Подготовленность к экранам Retina
Должны быть четкими при минимальном изменении размеров (некоторые замечают отклонения в качестве даже уменьшенных изображений)
Необходимость менять размеры такого количества изображений вручную, даже с помощью скрипта Photoshop, казалась слишком огромным трудом. Все подобные вещи следует автоматизировать для того, чтобы вместо них можно было сосредоточиться на захватывающе интересном кодировании. Кроме того, автоматизация удаляет риск совершения человеческой ошибки, типа «забыл это сделать». Идеальное для нас решение – это один раз добавить файл изображения и забыть о нем.
Обычные решения
Перед переходом к нашему собственному решению давайте рассмотрим некоторые из используемых в настоящее время. Чтобы угнаться за популярными сегодня методами и работой, проделываемой веб-сообществом для поиска решения проблемы адаптивных изображений, перейдем к группе адаптивных изображений сообщества W3C.
ЭЛЕМЕНТ PICTURE
Первый по счету – элемент picture. Хотя в настоящее время у него отсутствует «родная» поддержка, а браузерные вендоры еще раздумывают над применением picture против srcset и против чего бы еще ни обсуждалось, его можно использовать с полифилом.
1 2 3 4 5 |
<picture alt="description"> <source src="small.jpg"> <source src="medium.jpg" media="(min-width: 40em)"> <source src="large.jpg" media="(min-width: 80em)"> </picture> |
Элемент picture отлично подходит, если помимо простого изменения размера вам требуется передавать изображения различной формы, фокальной точки или других свойств. Однако придется предварительно масштабировать все изображения до того, как перейти прямиком к HTML. Это решение, кроме того, соединяет HTML с медиазапросами, а мы знаем, что объединение CSS с HTML плохо отражается на поддержке. Также оно не касается дисплеев высокой четкости.
Для этого проекта элемент picture требовал слишком много конфигурирования и ручного создания и хранения различных размеров изображений и их файловых путей.
SRCSET
Еще одно популярное решение — srcset, в последнее время стало «родным» в некоторых браузерах на базе WebKit. Во время создания нашего плагина оно еще не было доступным и, похоже, придется немного подождать, пока кроссбраузерная совместимость не станет достаточно хорошей для того, чтобы можно было пользоваться им без альтернативного JavaScript’а. На момент написания этого текста srcset пригоден к употреблению только в «ночных» сборках Chrome и Safari.
1 |
<img src="fallback.jpg" srcset="small.jpg 640w 1x, small-hd.jpg 640w 2x, large.jpg 1x, large-hd.jpg 2x" alt="..."> |
Приведенный выше фрагмент кода демонстрирует употребление srcset. И снова видно, что оно сильно добавляет количество встроенных в HTML медиазапросов, и весьма меня достает. Кроме того, нам пришлось бы создавать различные размеры изображений до времени исполнения, что означает либо установку скрипта, либо утомительную работу вручную.
АНАЛИЗ НА СТОРОНЕ СЕРВЕРА
Если вы склонные не пользоваться JavaScript’ом для того, чтобы решить, какое изображение следует передать, то можете попытаться проанализировать агента пользователя на серверной стороне и автоматически послать изображение подходящего размера. Как правило, мы почти всегда говорим: не полагайтесь на анализ с серверной стороны. Он очень ненадежен, а многие браузеры содержат неточные строки пользовательского агента. Кроме того, безумное количество выпускаемых каждый месяц новых устройств и размеров экранов устроят вам адову пропасть работы по поддержанию проекта.
ПРОЧИЕ РЕШЕНИЯ
Мы решили создать собственный плагин потому, что включение кода разметки в HTML казалось нам нежелательным, а необходимость заранее создавать разные размеры изображений совершенно не привлекала.
Если хотите проанализировать другие традиционные решения с целью решить, которое из них лучше всего подойдет для вашего проекта, в Сети имеется несколько отличных статей и примеров, включая одну на этом самом вебсайте.
Выбираем решение проблемы адаптивных изображений Шерри Александер (Sherri Alexander) на Smashing Magazine. Александер рассматривает высокие требования к адаптивным изображениям, а затем критически разбирает множество доступных на данный момент решений.
Какое решение проблемы адаптивных изображений следует применять Криса Койера (Chris Coyier) на CSS-Tricks. Койер объясняет требования к изображениям, одновременно предлагая подходящие решения.
Адаптивные изображения. Решение, очень сходное по своей реализации с Etch. В нем используется скрипт PHP для изменения размеров и подачи подходящих изображений. К сожалению, когда мы кодировали вебсайт, его еще не было.
Picturefill. Это – замена разметки в стилях элемента picture JavaScript’ом.
Применение куки для адаптивных изображений Кейта Кларка (Keith Clark). Кларк пользуется куки для хранения размера экрана, а затем изображения вызываются через скрипт PHP. И снова это решение похоже на наше, но в то время оно было еще недоступно.
Переходим к собственному решению.
Наше решение
Так как HTML-синтаксисы picture и srcset казались слишком значительным и ненужным усилием, мы стали искать более простое решение. Нам требовалось добавить путь отдельного изображения и дать возможность CSS, JavaScript и PHP самим справиться с подачей правильного изображения — вместо HTML, которому требовалось всего лишь иметь в наличии верную информацию.
На момент создания вебсайта нашим требованиям не отвечало ни одно общеизвестное решение. Большинство их были сосредоточены на эмуляции picture или srcset, которые мы уже определили как неподходящие. Вебсайт Etch сильно перегружен изображениями, что сделало бы изменение их размеров вручную очень длительным, склонным к совершению ошибок процессом. Даже выполнение автоматического скрипта Photoshop было сочтено требующим слишком большой поддержки.
Наше решение с помощью JavaScript’а при загрузке страницы должно было определить визуализируемую ширину изображения, а затем передать src и width скрипту PHP, который изменит размер изображений и кэширует их «на ходу» до их помещения обратно в DOM.
Давайте рассмотрим отвлеченный пример кода, написанного в HTML, JavaScript, PHP и LESS. Работающий демо-пример есть на моем вебсайте. Если хотите воспользоваться файлами демо-примера, их можно найти на GitHub.
Разметка
Разметку демо-примера можно найти в файле index.html на GitHub. Мы оборачиваем версию изображения в самом высоком разрешении в тэги noscript для тех браузеров, где отключен JavaScript. Причина в том, что если считать производительность свойством, а JavaScript – его улучшением, то пользователи без JavaScript’а все равно получат содержимое, просто не самый оптимизированный его вариант. Затем эти элементы noscript обертываются в элемент div, где свойства изображения src и alt служат атрибутами данных. Так обеспечивается та информация, которую JavaScript’у требуется послать на сервер.
1 2 3 |
<div data-src="img/screen.JPG" data-alt="crispy" class="img-wrap js-crispy"> <noscript><img src="img/screen.JPG" alt="Crispy"></noscript> </div> |
Фон упаковщика изображения установлен как загружаемый GIF с целью показать, что изображения еще загружаются, а не неисправно работают. Альтернативное решение (которое мы использовали в одном из своих побочных проектов, PhoSho) состоит в использовании самого низкого разрешения изображения, которое вы будете показывать (если оно известно), вместо загружаемого GIF’а. Оно требует немного большей ширины полосы пропускания, потому что загружается более одного изображения, но при загрузке страницы оно создает вид, как у одного из прогрессивных JPEG’ов. Как обычно, смотрите, что диктуют ваши требования.
JavaScript
JavaScript связывается за нас между HTML и сервером. Он доставляет массив изображений соответствующей ширины из DOM и возвращает с сервера кэшированный файл подходящего изображения.
Наш первоначальный скрипт посылал на сервер по одному запросу на каждое изображение, но это вызывало множество лишних запросов. Объединив изображения вместе в один массив, мы сократили количество запросов и осчастливили этим свой сервер.
Плагин JavaScript можно отыскать в /js/resize.js хранилища GitHub. Во-первых, мы устанавливаем в плагине массив контрольных точек, таких же, как контрольные точки в CSS, где меняются размеры изображений. Для точек мы воспользовались значениями em, потому что они базируются на основе размера шрифта дисплея. Это верный подход, так как пользователи с плохим зрением могут менять размер шрифта по умолчанию своего дисплея. Кроме того, это облегчает сопоставление контрольных точек CSS с точками JavaScript. Если нужно иначе, то плагин отлично работает с контрольными точками на основе пикселей.
1 2 3 4 5 6 |
breakpoints: [ "32em" "48em" "62em" "76em" ] |
По мере передачи каждой из этих точек требуется проверять изображения, чтобы убедиться, что они правильного размера. Во время загрузки мы сначала устанавливаем текущую точку, отображаемую для пользователя с помощью функции JavaScript matchMedia. Если нужна поддержка старых браузеров (Internet Explorer 7, 8 и 9), то вам может понадобиться полифил matchMedia Пола Айриша (Paul Irish).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
getCurrentBreakpoint: function() { var bp, breakpoint, _fn, _i, _len, _ref, _this = this; bp = this.breakpoints[0]; _ref = this.breakpoints; _fn = function(breakpoint) { // Проверьте, передается ли контрольная точка if (window.matchMedia && window.matchMedia("all and (min-width: " + breakpoint + ")").matches) { return bp = breakpoint; } }; for (_i = 0, _len = _ref.length; _i < _len; _i++) { breakpoint = _ref[_i]; _fn(breakpoint); } return bp; } |
После установки текущей точки мы собираем изображения, требующие изменения размера, из DOM путем их просмотра и добавления в массив плагина images.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
gather: function() { var el, els, _i, _len; els = $(this.els); this.images = []; for (_i = 0, _len = els.length; _i < _len; _i++) { el = els[_i]; this.add(el); } this.grabFromServer(); } |
Скрипту PHP на сервере требуется src изображения и текущая ширина для того, чтобы правильно изменить его размер, поэтому мы создали для отсылки на сервер некие последовательные данные POST. Для быстрого преобразования изображения в пригодную строку запроса мы пользуемся методом jQuery param.
1 2 3 4 |
buildQuery: function() { var image = { image: this.images } return $.param(image); } |
Затем изображения отсылаются через запрос AJAX на сервер для изменения размера. Обратите внимание – единственным запросом для сведения загрузки сервера до минимума.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
grabFromServer: function() { var data, _this = this; data = this.buildQuery(); $.get("resize.php", data, function(data) { var image, _i, _len; for (_i = 0, _len = data.length; _i < _len; _i++) { image = data[_i]; _this.loadImage(image); } } ); } |
Получив изображения с сервера, мы можем добавить их в DOM или заменить изображение, которое уже находится на своем месте, если оно изменилось. Если это одно и то же изображение, то ничего не происходит, и его не придется загружать заново, потому что оно уже находится в кэше браузера.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
loadImage: function(image) { var el, img, _this = this; el = $("[data-src='" + image.og_src + "']"); img = $(""); img.attr("src", image.src).attr("alt", el.attr("data-alt")); if (el.children("img").length) { el.children("img").attr("src", image.src); } else { img.load(function() { el.append(img); el.addClass('img-loaded'); }); } } |
PHP
Если JavaScript просто запрашивает массив изображений разных размеров, то PHP находится в самой гуще событий.
Мы пользуемся двумя скриптами. Один из них – это класс resize (есть в /php/lib/resize-class.php демо-примера), который создает кэшированные версии изображения нужных нам размеров. Второй скрипт находится в корневом каталоге Сети, подсчитывает самый подходящий размер отображения и действует как интерфейс между JavaScript’ом и ресайзером. Запуская изменение размеров и скрипт интерфейса, мы сначала устанавливаем массив пиксельных размеров изображений, которые нужно отобразить, а также маршрут к папке кэшированных изображений. Размеры изображений определены в пикселях, так как сервер еще ничего не знает о текущем уровне увеличения текста пользователем, а только о том, какие направляются физические размеры изображений.
1 2 3 4 5 6 7 8 9 |
$sizes = array( '100', '300', '600', '1200', '1500', ); $cache = 'img/cache/'; |
Далее создаем маленькую функцию, возвращающую размер изображения, самый близкий к текущему размеру дисплея.
1 2 3 4 5 6 7 8 9 10 11 |
function closest($search, $arr) { $closest = null; foreach($arr as $item) { // расстояние от ширины изображения -> текущего ближайшего входа больше, чем расстояние от if ($closest == null || abs($search - $closest) > abs($item - $search)) { $closest = $item; } } $closest = ($closest == null) ? $closest = $search : $closest; return $closest; } |
Наконец, можно пройтись по путям отправленных скрипту изображений и передать их классу resize для получения пути кэшированного файла изображения (а при необходимости создать этот файл).
1 2 |
$crispy = new resize($image,$width,$cache); $newSrc = $crispy->resizeImage(); |
Мы возвращаем исходный путь изображения для того, чтобы снова отыскать его в DOM, и путь к кэшированному файлу изображения с правильно измененным размером. Все маршруты изображений отсылаются обратно в виде массива, чтобы можно было пройтись по ним и добавить в HTML.
1 |
$images[] = array('og_src' => $src, 'src' => '/'.$newSrc); |
В класс resize для процесса изменения размеров вначале нужно собрать некую информацию об изображении. Мы пользуемся Exif для определения вида изображения, потому что у файла может оказаться неправильное расширение или вообще не оказаться никакого расширения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function __construct($fileName, $width, $cache) { $this->src = $fileName; $this->newWidth = $width; $this->cache = $cache; $this->path = $this->setPath($width); $this->imageType = exif_imagetype($fileName); switch($this->imageType) { case IMAGETYPE_JPEG: $this->path .= '.jpg'; break; case IMAGETYPE_GIF: $this->path .= '.gif'; break; case IMAGETYPE_PNG: $this->path .= '.png'; break; default: // *** Не опознан break; } } |
Вышеприведенное свойство $this->path, содержащее путь кэшированного изображения, устанавливается с помощью сочетания ширины дисплея, хэша времени последней трансформации файла и src, а также исходного имени файла.
Вызывая метод resizeImage, мы проверяем, существует ли уже путь, установленный в $this->path, и если это так, то просто возвращаем путь кэшированного файла. Если файла не существует, то с помощью GD открываем изображение для изменения размера. Когда он готов к использованию, мы считаем соотношение ширины к высоте исходного изображения и применяем его, чтобы получить высоту кэшированного изображения после изменения размера до требуемой ширины.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
if ($this->image) { $this->width = imagesx($this->image); $this->height = imagesy($this->image); } $ratio = $this->height/$this->width; $newHeight = $this->newWidth*$ratio; Затем с помощью GD меняем исходное изображение до новых размеров и возвращаем скрипту интерфейса путь файла кэшированного изображения. $this->imageResized = imagecreatetruecolor($this->newWidth, $newHeight); imagecopyresampled($this->imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $newHeight, $this->width, $this->height); $this->saveImage($this->newWidth); return $this->path; |
Чего мы добились?
Этот плагин дает возможность получить одну партию изображений для вебсайта. Нам не приходится думать об изменении их размеров, потому что этот процесс автоматизирован. Это сильно облегчает поддержку и обновление сайта и освобождает от множества усилий, которые лучше посвятить более важным задачам. Включите его однажды и забудьте.
Давайте еще раз резюмируем функциональность.
В своей разметке мы обеспечиваем упаковщик изображений, содержащий альтернативный вариант noscript. У этого упаковщика как эталон имеется атрибут данных нашего исходного изображения в высоком разрешении. Мы пользуемся JavaScript’ом, чтобы послать запрос AJAX к файлу PHP на сервер, запрашивая версию этого изображения правильного размера. Файл PHP либо меняет размер и доставляет путь изображения нужного размера, либо просто возвращает путь, если изображение уже создано. Как только запрос AJAX выполняется, мы прикрепляем в DOM новое изображение или просто обновляем src, если оно уже добавлено. Если пользователь меняет размер своего браузера, то мы снова проверяем, нужно ли использовать изображение более подходящего размера.
«За» и «Против»
У всех решений проблемы адаптивных изображений имеются аргументы «за» и «против», и вам следует внимательно исследовать несколько из них перед тем, как выбрать какое-либо для своего проекта. Наше решение отвечает набору специальных требований, и не должно оказаться решением по умолчанию. Насколько можно сказать, в настоящее время такого решения не существует, поэтому мы рекомендуем вам опробовать как можно большее их количество.
Как оценить данное решение?
«ЗА»
Изначальная быстрая загрузка страницы благодаря меньшему весу изображений
Однажды установив, им легко пользоваться
Не требует много поддержки
Быстро работает при созданных кэшированных файлах
Подает изображения правильного пиксельного размера (в пределах допуска)
Поставляет новое изображение при изменении размера дисплея (в пределах допуска)
«ПРОТИВ»
Невозможно выбрать область фокусирования изображения
Для полной функциональности требуются PHP и JavaScript
Неспособно обеспечить все возможные размеры, если используются «текучие» изображения
Может оказаться несовместимым с некоторыми системами управления контентом
Изменение размера всех изображений одним запросом означает, что при пустом кэше вам придется ждать, пока не поменяют размер они все, а не всего одно изображение
Скрипт PHP привязан к контрольным точкам, поэтому его нельзя вставить без соответствующих настроек
Решения проблемы адаптивных изображений за последние месяцы проделали огромный путь, и если бы нам пришлось делать все заново, мы, вероятно, рассмотрели бы что-нибудь вроде этого решения, потому что оно удаляет со страницы несемантический HTML путем модифицирования .htaccess.
Резюме
Пока не появится «родное» решение проблемы адаптивных изображений, не будет найдено и «верного» способа. Всегда исследуйте несколько возможностей перед тем, как остановиться на одном из них для своего проекта. Приведенный здесь пример хорошо работает для вебсайтов с несколькими обычными размерами изображений по контрольным точкам, но ни в коем случае не является окончательным решением. А до тех пор почему бы вам не попытаться создать собственное решение или попробовать обыграть данное решение на GitHub?
Автор: Gavyn McKenzie
Источник: //www.smashingmagazine.com/
Редакция: Команда webformyself.
* Признана экстремистской организацией и запрещена в Российской Федерации.