21. Работа с графикой и медиафайлами на стороне клиента


    21.1. Работа с готовыми изображениями
       21.1.1. Ненавязчивая реализация смены изображений
       21.2.1. Выбор типа и загрузка
    21.2. Работа с аудио- и видеопотоками
       21.2.2. Управление воспроизведением
       21.2.3. Определение состояния мультимедийных элементов
       21.2.4 События мультимедийных элементов
    21.3. SVG - масштабируемая векторная графика
    21.4. Создание графики с помощью элемента <canvas>
       21.4.1. Рисование линий и заливка многоугольников
       21.4.2. Графические атрибуты
       21.4.3. Размеры и система координат холста
       21.4.4. Преобразование системы координат
       21.4.5. Рисование и заливка кривых
       21.4.6. Прямоугольники
       21.4.7. Цвет, прозрачность, градиенты и шаблоны
       21.4.8. Атрибуты рисования линий
       21.4.9. Текст
       21.4.10. Отсечение
       21.4.11. Тени
       21.4.12. Изображения
       21.4.13. Композиция
       21.4.14. Манипулирование пикселами
       21.4.15. Определение попадания
       21.4.16. Пример использования элемента <canvas>: внутристрочные
диаграммы

В этой главе рассказывается о том, как манипулировать изображениями, управлять аудио- и видеопотоками и рисовать графику. В разделе 21.1 описываются традиционные приемы реализации визуальных эффектов на языке JavaScript, таких как смена изображений, когда одно статическое изображение сменяется другим при наведении указателя мыши. В разделе 21.2 описываются элементы <audio> и <video>, определяемые стандартом HTML5, и их прикладные интерфейсы в языке JavaScript.

После первых двух разделов, посвященных работе с изображениями, аудио- и видеопотоками, будет рассказано о двух мощных технологиях рисования графических изображений на стороне клиента. Способность воспроизводить на стороне клиента сложные графические изображения имеет важное значение по нескольким причинам:


содержание 21.2. Работа с аудио- и видеопотоками содержание

Стандарт HTML5 определяет новые элементы <audio> и <video>, которые теоретически так же просты в использовании, как элемент <img>. В браузерах с поддержкой стандарта HTML5 больше не нужно использовать дополнительные расширения (такие как Flash), чтобы внедрить в свои HTML-документы аудио- и видеоклипы:

<audio src="background_music.mp3"/> 

<video src="news.mov" width=320 height=240/>  

Однако на практике работать с этими элементами несколько сложнее, чем было показано выше. Производители браузеров не смогли прийти к соглашению о стандарте на аудио- и видеокодеки, которые поддерживались бы всеми браузерами, вследствие чего обычно приходится использовать элементы <source>, чтобы указать несколько источников мультимедийных данных в различных форматах:

<audio id="music">
<source src"music.mp3" type="audio/mpeg">
<source src="music.ogg" type='audio/ogg; codec="vorbis"'>
</audio>

Обратите внимание, что элементы <source> не имеют содержимого: они не имеют закрывающего тега </source>, и от вас не требуется завершать их последовательностью символов />.

Браузеры, поддерживающие элементы <audio> и <video>, не будут отображать их содержимое. Тогда как браузеры, не поддерживающие их, отобразят это содержимое. Чтобы решить эту проблему, можно вставить внутрь содержимое для обратной совместимости (например, элемент <object>, который вызывает расширение Flash):

<video id="news" width=640 height=480 controls preload> 
  <!-- В формате WebM для Firefox и Chrome -->  
  <souгcе src="'news.webm" type='video/webm; codecs="vp8, vorbis""> 
  <!-- В формате Н.264 для IE и Safari -->  
  <source src="news.mp4" type="video/mp4; codecs="avc1.42E01E, mp4a.40.2""> 
  <!-- Для совместимости с расширением Flash -->  
  <object width=640 height=480 type="'application/x-shockwave-flash"
      data="flash_movie_player.swf">  
    <!-- Здесь можно указать параметры настройки проигрывателя Flash --> 
    <!-- Текстовое содержимое, используемое в самом худшем случае -->
     <div>Элемент <video> не поддерживается 
		            и расширение Flash не установлено.</div>
   </object>  
</video>  

Элементы <audio> и <video> поддерживают атрибут controls. Если он присутствует (или соответствующее JavaScript-свойство имеет значение true), они будут отображать элементы управления, включая кнопки запуска на воспроизведение и паузы, регулятор громкости и т.д. Но кроме этого, элементы <audio> и <video> предоставляют прикладной интерфейс, обеспечивающий широкие возможности управления воспроизведением, с помощью которого вы можете добавлять простые звуковые эффекты в свои веб-приложения или создавать собственные панели управления воспроизведением. Несмотря на различия во внешнем виде, элементы <audio> и <video> предоставляют практически один и тот же прикладной интерфейс (единственное отличие между которыми состоит в том, что элемент <video> имеет свойства width и height), поэтому большая часть того, что рассказывается далее в этом разделе, в равной степени относится к обоим элементам. Несмотря на раздражающую необходимость определять мультимедийные данные в нескольких форматах, возможность воспроизводить звук и видеоизображение родными средствами браузера без использования дополнительных расширений является новой мощной особенностью, добавленной стандартом HTML5. Обратите внимание, что обсуждение проблемы поддержки кодеков и совместимости браузеров выходит далеко за рамки этой книги. В следующих подразделах мы сосредоточимся исключительно на методах JavaScript, предназначенных для работы с аудио- и видеопотоками.

Конструктор Audio()

Элементы <audio> не имеют визуального представления в документе, если не установить атрибут controls . И так же, как имеется возможность создавать неотображаемые изображения с помощью конструктора Image(), механизм поддержки мультимедиа, определяемый стандартом HTML5, позволяет создавать аудиоэлементы с помощью конструктора Audio(), передавая ему аргумент с URL-адресом источника данных:

new Audio("chime.wav"*).play(); // Загрузить и проиграть звуковой эффект 
Конструктор Audio() возвращает тот же объект, который будет получен при обращении к элементу <audio> в документе или при создании нового аудиоэлемента вызовом document.createElement("audio"). Обратите внимание, что все вышесказанное относится только к аудиоэлементам: механизм поддержки мультимедиа не имеет соответствующего конструктора Video().

содержание 21.2.1. Выбор типа и загрузка

Если вам потребуется проверить, способен ли мультимедийный элемент воспроизводить мультмедийные данные в определенном формате, передайте MIME-тип этих данных (при необходимости с параметром codec) методу canPlayType(). Элемент вернет пустую строку (ложное значение), если он не способен проигрывать мультимедийные данные в этом формате. В противном случае он вернет строку «maybe» (возможно) или «probably» (вероятно). Из-за сложной природы аудио- и видеокодеков проигрыватель в общем случае не может сообщить ничего более определенного, чем «probably» (вероятно), не предприняв фактическую попытку загрузить и воспроизвести данные указанного типа:

var a = new Audio(); if (a.canPlayType("audio/wav")) { a.src = "soundeffect.wav"; a.play(); }

Когда свойству src мультимедийного элемента присваивается значение, он начинает процесс загрузки мультимедийных данных. (Этот процесс не продвинется слишком далеко, если не установить в свойстве preload значение «auto».) Присваивание нового значения свойству src во время загрузки или воспроизведения других мультимедийных данных прервет загрузку или воспроизведение старых данных. Если вместо настройки атрибута src вы будете добавлять в мультимедийный элемент элементы <source>, то он не сможет приступить к выбору нужного элемента, так как не будет знать, когда закончится формирование полного комплекта элементов <source>, и не сможет начать загрузку данных, пока явно не будет вызван метод load().

содержание 21.2.2. Управление воспроизведением

Самыми важными методами элементов <audio> и <video> являются методы play() и pause(), которые запускают и останавливают воспроизведение:

// Когда документ будет загружен, запустить фоновое проигрывание мелодии 
window.addEventListener("load", function() {
                          document.getElementById("music").play();
                       }, false);

Помимо возможности запустить и остановить проигрывание звука или видео имеется возможность выполнить переход к требуемому месту в мультимедийных данных установкой свойства currentTime. Это свойство определяет время в секундах, к которому должен быть выполнен переход, и его можно устанавливать в процессе проигрывания данных или во время паузы. (Свойства initialTime и duration ограничивают диапазон допустимых значений свойства currentTime; подробнее об этих свойствах рассказывается ниже.)

Свойство volume определяет уровень громкости как числовое значение в диапазоне от 0 (минимальная громкость) до 1 (максимальная громкость). Свойству muted может быть присвоено значение true, чтобы выключить звук, или false, чтобы продолжить воспроизведение с установленным уровнем громкости. Свойство playbackRate определяет скорость проигрывания. Значение 1,0 соответствует нормальной скорости. Значения выше 1 соответствуют «ускоренному воспроизведению вперед», а значения от 0 до 1 – «замедленному воспроизведению вперед». Отрицательные значения предполагают проигрывание звука или видео в обратном направлении, но на момент написания этих строк браузеры не поддерживали такую возможность. Элементы <audio> и <video> также имеют свойство defaultPlaybackRate. Всякий раз, когда вызывается метод play(), значение свойства defaultPlaybackRate присваивается свойству playbackRate.


Обратите внимание, что свойства currentTime, volume, muted и playbackRate не являются единственными средствами управления воспроизведением. Если элемент <audio> или <video> имеет атрибут controls, он отображает элементы управления проигрывателем, давая пользователю возможность управлять воспроизведением. В этом случае сценарий может читать значения таких свойств, как muted и currentTime, чтобы определить, как протекает воспроизведение мультимедийных данных. HTML-атрибуты controls, loop, preload и autoplay оказывают влияние на воспроизведение аудио и видео, а также доступны для чтения и записи как JavaScript-свойства. Атрибут controls определяет, должны ли отображаться элементы управления проигрывателем. Присвойте этому свойству значение true, чтобы отобразить элементы управления, или false, чтобы скрыть их. Логическое свойство loop определяет, должно ли воспроизведение начинаться сначала по достижении конца (true) или нет (false). Свойство preload определяет, какой объем мультимедийных данных должен быть загружен прежде, чем пользователь сможет запустить проигрывание. Значение «попе» означает, что предварительная загрузка данных не требуется. Значение «metadata» означает, что предварительно должны быть загружены такие метаданные, как продолжительность, битрейт и размер кадра, но предварительная загрузка самих данных не требуется. При отсутствии атрибута preload браузеры обычно загружают только метаданные. Значение « auto» означает, что браузер должен предварительно загрузить такой объем данных, какой он сочтет нужным. Наконец, свойство autoplay определяет, должно ли начаться воспроизведение автоматически, после загрузки достаточного объема данных. Присваивание свойству autoplay значения true подразумевает, что браузер должен предварительно загрузить некоторый объем данных.


содержание 21.2.3. Определение состояния мультимедийных элементов

Элементы <audio> и <video> имеют несколько свойств, доступных только для чтения, которые описывают текущее состояние данных и проигрывателя. Свойство paused имеет значение true, если проигрывание было приостановлено. Свойство seeking имеет значение true, если проигрыватель выполняет переход к новой позиции в проигрываемых данных. Свойство ended имеет значение true, если проигрыватель достиг конца и остановился. (Свойство ended никогда не приобретет значение true, если свойству loop было присвоено значение true.) Свойство duration определяет продолжительность проигрываемых данных в секундах. Если прочитать это свойство до того, как будут получены метаданные, оно вернет значение NaN. Для потоковых данных с неопределенной продолжительностью, например, при прослушивании Интернет-радио, это свойство возвращает значение Infinity.

Свойство initialTime определяет начальное время в проигрываемых данных в секундах. Для мультимедийных клипов с фиксированной продолжительностью это свойство обычно имеет значение 0. Для потоковых данных это свойство возвращает самое раннее время данных в буфере, к которому еще можно вернуться. Свойство currentTime не может быть установлено в значение меньше чем значение свойства initialTime.

Три других свойства позволяют получить более точное представление о временной шкале для проигрываемых данных и состоянии механизма буферизации. Свойство played возвращает диапазон или диапазоны времени, проигрываемые в настоящее время. Свойство buffered возвращает диапазон или диапазоны времени, которые в настоящее время находятся в буфере, а свойство seekable возвращает диапазон или диапазоны времени, куда проигрыватель может выполнить переход. (Эти свойства можно использовать для реализации индикатора, иллюстрирующего свойства currentTime и duration, а также продолжительность воспроизведенных данных и объем данных в буфере.)

Свойства played, buffered и seekable являются объектами TimeRanges. Каждый объект имеет свойство length, определяющее количество представляемых им диапазонов, и методы start() и end(), возвращающие начало и конец (в секундах) диапазона с указанным номером. В наиболее типичном случае, когда имеется всего один непрерывный диапазон, эти методы вызываются, как start(O) и end(O). Если, к примеру, предположить, что переходы не выполнялись и данные буферизованы с самого начала, то можно использовать следующий прием, чтобы определить, какая доля ресурса в процентах была загружена в буфер:

var percent_loaded = Math.floor(song.buffered.end(O) / song.duration * 100);

Наконец, имеются еще три свойства, readyState, networkState и error, позволяющие получить низкоуровневую информацию о состоянии элементов <audio> и <video>. Все эти свойства имеют числовые значения, и для каждого допустимого значения определена константа. Обратите внимание, что эти константы определены непосредственно в мультимедийном объекте (или в объекте ошибки). Эти константы можно использовать, как показано ниже:

if (song.readyState === song.HAVE_ENOUGH_DATA) song.play();

Свойство readyState определяет, как много мультимедийных данных было загружено, и, соответственно, готов ли элемент начать воспроизведение этих данных. Допустимые значения этих свойств и их смысл перечислены ниже:

< < < < <
Константа Значение Описание
HAVE_NOTHING 0 Мультимедийные данные и метаданные еще не были загружены.
HAVE_METADATA 1 Метаданные были загружены, но мультимедийные данные для текущей позиции воспроизведения еще не были загружены. Это означает, что имеется возможность узнать продолжительность воспроизведения или размеры кадра видеоклипа, а также выполнить переход к другой позиции, установив свойство currentTime, но браузер в настоящий момент не может воспроизводить в позиции currentTime.
HAVE_CURRENT_DATA 2 Мультимедийные данные для позиции currentTime были загружены, но объем загруженных данных еще недостаточен, чтобы начать воспроизведение. Для видеоклипов это обычно означает, что текущий кадр был загружен, а следующий еще нет. Это состояние возникает в конце аудио- или видеоклипа.
HAVE_FUTURE_DATA 3 Был загружен достаточный объем данных, чтобы начать воспроизведение, но их недостаточно, чтобы воспроизвести клип до конца без приостановки на загрузку дополнительных данных.
HAVE_EN0UGH_DATA 4 Был загружен достаточный объем данных, чтобы их, скорее всего, можно было воспроизвести до конца, без приостановки.

Свойство networkState определяет, использует ли (и если нет, то почему) сеть мультимедийный элемент:

Константа Значение Описание
NETW0RK_EMPTY 0 Элемент еще не приступил к использованию сети. Это состояние возникает, например, перед установкой атрибута src.
NETWORK_IDLE 1

В настоящее время элемент не загружает данные из сети. Возможно, он загрузил ресурс полностью или сохранил в буфере достаточный объем требуемых данных. Или, возможно, свойству preload было присвоено значение «none» и элементу еще не была дана команда загружать или воспроизводить клип.

NETWORK_LOADING 2 В настоящее время элемент загружает данные из сети
NETW0RK_N0_S0URCE 3 Элемент не может отыскать источник данных, которые требуется воспроизвести.

Когда при загрузке или воспроизведении возникает ошибка, браузер записывает определенное значение в свойство error элемента <audio> или <video>. При отсутствии ошибок свойство error имеет значение null. Иначе оно ссылается на объект с числовым свойством code, описывающим ошибку. Объект ошибки также определяет константы для возможных кодов ошибок:

Константа Значение Описание
MEDIA_ERR_ABORTED 1 Пользователь потребовал остановить загрузку клипа.
MEDIA_ERR_NETWORK 2 Мультимедийные данные имеют верный тип, но их загрузке препятствуют ошибки в сети.
MEDIA_ERR_DECODE 3 Мультимедийные данные имеют верный тип, но их декодированию и воспроизведению препятствуют ошибки кодирования.
MEDIA_ERR_SRC_NOT_SUPPORTED 4 Мультимедийные данные, на которые ссылается атрибут src, имеют тип, который не может воспроизводиться браузером.

Свойство error можно использовать следующим образом:

if (song.error.code == song.error.MEDIA_ERR_DECODE)
  alert("Невозможно воспроизвести песню: повреждены аудиоданные.");

содержание 21.2.4 События мультимедийных элементов

Элементы <audio> и <video> являются довольно сложными элементами – они должны откликаться на взаимодействие пользователя с их элементами управления, выполнять сетевые операции и даже, в процессе воспроизведения, реагировать на простое течение времени. Мы только что видели, что эти элементы имеют довольно много свойств, определяющих их состояние. Подобно большинству HTML-элементов, элементы <audio> и <video> генерируют события при изменении своего состояния. Поскольку состояние этих элементов описывается множеством характеристик, они могут генерировать довольно много различных событий. В таблице ниже перечислены 22 события мультимедийных элементов, примерно в том порядке, в каком они обычно возникают. Элементы не имеют свойств обработчиков этих событий, поэтому для регистрации функций обработчиков в элементах <audio> и <video> следует использовать метод addEventListener().

Тип события Возбуждается, когда элемент отправляет запрос на получение мультимедийных данных. Свойство networkState имеет значение NETWORK_LOADING.
Описание Загрузка мультимедийных данных продолжается. Свойство networkState имеет значение NETWORK_LOADING. Это событие обычно возбуждается от 2 до 8 раз в секунду.
loadstart Метаданные загружены, и доступна информация о продолжительности клипа и размерах кадра. Свойство readyState в первый раз получает значение HAVE_METADATA.
progress Данные для текущей позиции воспроизведения загружены, а свойство readyState получает значение HAVE_CURRENT_DATA.
loadedmetadata Загружен достаточный объем данных, чтобы начать воспроизведение, но, скорее всего, необходимо загрузить в буфер дополнительный объем данных. Свойство readyState имеет значение HAVE_FUTURE_DATA.
loadeddata Загружен достаточный объем данных, чтобы наверняка воспроизвести клип до конца без дополнительных пауз для буферизации дополнительных данных. Свойство readyState имеет значение HAVE_ENOUGH_DATA.
canplay Элемент загрузил в буфер достаточный объем данных и временно приостановил загрузку. Свойство networkState получает значение NETWORK_IDLE.
canplaythrough Элемент пытается загрузить данные, но данные не поступают из сети. Свойство networkState остается в состоянии NETWORK_LOADING.
suspend Был вызван метод play() или воспроизведение было автоматически запущено из-за наличия атрибута auto play. Если был загружен достаточный объем данных, вслед за этим событием последует событие «playing». Иначе последует событие «waiting».
stalled Воспроизведение не может быть начато или оно было приостановлено из-за недостатка данных в буфере. Когда будет загружен достаточный объем данных, последует событие «playing».
play Начато воспроизведение клипа.
waiting Изменилось значение свойства currentTime. В процессе воспроизведения это событие генерируется от 4 до 60 раз в секунду в зависимости от нагрузки на систему и скорости выполнения обработчиков событий.
playing Был вызван метод pause(), и воспроизведение было приостановлено.
timeupdate Сценарий или пользователь потребовал перейти к участку клипа, отсутствующему в буфере, и воспроизведение было остановлено до загрузки данных.
pause Свойство seeking получает значение true.
seeking Сценарий или пользователь потребовал перейти к участку клипа, отсутствующему в буфере, и воспроизведение было остановлено до загрузки данных. Свойство seeking получает значение true.
seeked Свойство seeking получает значение false.
ended Воспроизведение было остановлено по достижении конца клипа.
durationchange Изменилось значение свойства duration.
volumechange Изменилось значение свойства volume или muted.
ratechange Изменилось значение свойства playbackRate или defaultPlaybackRate.
abort Элемент прекратил загрузку данных. Обычно это происходит по требованию пользователя. Свойство error.code получает значение MEDIA_ERR_ABORTED.
error Сетевая или какая-либо другая ошибка не дает возможности загрузить данные. Свойство error.code получает любое другое значение, отличное от MEDIA_ERR_ABORTED.
emptied В результате события «error» или «abort» свойство networkState получило значение NETWORK EMPTY.

содержание 21.3. SVG – масштабируемая векторная графика содержание

Масштабируемая векторная графика (SVG) – это грамматика языка XML для описания графических изображений. Слово «векторная» в названии указывает на фундаментальное отличие от таких форматов растровой графики, как GIF, JPEG и PNG, где изображение задается матрицей пикселов. Формат SVG представляет собой точное, не зависящее от разрешения (отсюда слово «масштабируемая») описание шагов, которые необходимо выполнить, чтобы нарисовать требуемый рисунок. Вот пример простого SVG-изображения в текстовом формате:

<!-- Начало рисунка и объявление пространства имен -->  
<svg xmlns='"http://www.w3.org/2000/svg"
    viewBox="'0 0 1000 1000>" < !-- Система координат рисунка -->  
  <defs>                      < !-- Настройка некоторых определений -->  
    <linearGradient id="fade"> <!-- Цветовой градиент с именем "fade" --> 
    <stop offset="0%" stop-color="#008"/>   <!-- Начинаем с темно-синего --> 
    <stop offset="100%" stop-color="#ccf"/> <!--Заканчиваем светло-синим--> 
    </linearGradient>  
  </defs>  
<! —  
    Нарисовать прямоугольник с тонкой черной рамкой и заполнить его градиентом 
-->  
  <rect х='100" y="200" width="800" height="600">  
    stroke="black" stroke-width="25" fill="url(#fade)"/>  
</svg>  

На рис. 21.1 показано графическое представление этого SVG-файла.

Рис. 21.1. Простое изображение в формате SVG

SVG – это довольно обширная грамматика умеренной сложности. Помимо простых примитивов рисования она позволяет воспроизводить произвольные кривые, текст и анимацию. Рисунки в формате SVG могут даже содержать JavaScript-сценарии и таблицы CSS-стилей, что позволяет наделить их информацией о поведении и представлении. В этом разделе показано, как с помощью клиентского JavaScript-кода (встроенного в HTML-, а не в SVG-документ) можно динамически создавать графические изображения средствами SVG. Приводимые здесь примерь; SVG-изображений позволяют лишь отчасти оценить возможности формата SVG. Полное описание этого формата доступно в виде обширной, но вполне понятной спецификации, которая поддерживается консорциумом W3C и находится по адресу http://www.w3.org/TR/SVG/. Обратите внимание: эта спецификация включает в себя полное описание объектной модели документа (DOM) для SVG-документов. В данном разделе рассматриваются приемы манипулирования SVG-графикой с помощью стандартной модели XML DOM, а модель SVG DOM не затрагивается. К моменту написания этих строк все текущие веб-браузеры, кроме IE, имели встроенную поддержку формата SVG (она также будет включена в IE9). В последних версиях браузеров отображать SVG-изображения можно с помощью обычного элемента <img>. Некоторые немного устаревшие браузеры (такие как Firefox 3.6) не поддерживают такую возможность и требуют использовать для этих целей элемент <object>:

<object data="sample.svg" type="image/svg+xml" width="100" height="100"/>   

При использовании в элементе <img> или <object> SVG можно рассматривать как еще один формат представления графических изображений, который, с точки зрения программиста на языке JavaScript, ничем особенным не выделяется. Гораздо больший интерес представляет сама возможность встраивания SVG-изображений непосредственно в документы и выполнения операций над ними. Поскольку формат SVG является грамматикой языка XML, изображения в этом формате можно встраивать непосредственно в XHTML-документы, как показано ниже:


<?xml version="1.0"?>
<!-- Объявить HTML как пространство имен по умолчанию,
     a SVG - с префиксом "svg:" --> 
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:svg="http://www.w3.org/2000/svg">  
<body>  
Это красный квадрат: <svg:svg width="10" height="10">  
  <svg:rect x="0" y="0" width="10" height="10" fill="red"/>  
</svg:svg>  
Это голубой круг: <svg:svg width="10" height="10">  
  <svg:circle cx="5" cy="5" r="5" fill="blue"/>  
</svg:svg>  
</body>  
</html>  

Этот прием можно использовать во всех текущих браузерах, кроме IE. На рис. 21.2 показано, как Firefox отображает этот XHTML-документ.

Рис. 21.2. SVG-графика в XHTML-документе

Стандарт HTML5 сокращает количество различий между XML и HTML и позволяет вставлять разметку на языке SVG (и MathML) непосредственно в HTML-файлы без объявления пространств имен или префиксов тегов:

<!DOCTYPE html>
<html>
<body>
Это красный квадрат: <svg width="10" height="10">
  <rect x="0" y="0" width="10" height="10" fill="red"/>
</svg>
Это голубой круг: <svg width="10" height="10">
  <circle cx="5" cy="5" r="5" fill="blue"/>
</svg>
</body>
</html>

На момент написания этих строк непосредственное встраивание SVG-изображений в разметку HTML поддерживали только самые последние версии браузеров. Так как формат SVG – это грамматика языка XML, рисование SVG-изображений заключается просто в использовании модели DOM для создания соответствующих XML-элементов. В примере 21.2 приводится реализация функции pieChart(), которая создает SVG-элементы для воспроизведения круговой диаграммы, подобной той, что показана на рис. 21.3.

Рис. 21.3. Круговая диаграмма в формате SVG, построенная JavaScript-сценарием


Пример 21.2. Рисование круговой диаграммы средствами JavaScript и SVG

/** 21.02.PieChart
*  Создает элемент <svg> и рисует в нем круговую диаграмму.  
*  Аргументы:  
*    data: массив чисел для диаграммы, по одному для каждого сектора.
*    width,height: размеры SVG-изображения в пикселах  
*    cx, cy, r: координаты центра и радиус круга  
*    colors: массив цветов в формате HTML, по одному для каждого сектора
*    labels: массив меток для легенды, по одной для каждого сектора  
*    lx, ly: координаты левого верхнего угла легенды диаграммы  
*  Возвращает:  
*    Элемент <svg>, хранящий круговую диаграмму.  
*    Вызывающая программа должна вставить возвращаемый элемент в документ.
*/
function pieChart(data, width, height, cx, cy, r, colors, labels, lx, ly) {
    // Пространство имен XML для элементов svg  
    var svgns = "http://www.w3.org/2000/svg";  
    // Создать элемент <svg>, указать размеры в пикселах и координаты 
    var chart = document.createElementNS(svgns, "svg:svg");
    chart.setAttribute("width", width);
    chart.setAttribute("height", height);
    chart.setAttribute("viewBox", "0 0 " + width + " " + height);

    // Сложить вместе все значения, чтобы получить общую сумму всей диаграммы
    var total = 0;
    for(var i = 0; i < data.length; i++) total += data[i];
    
    // Определить величину каждого сектора. Углы измеряются в радианах.
    var angles = []
    for(var i = 0; i < data.length; i++) angles[i] = data[i]/total*Math.PI*2;

    // Цикл по всем секторам диаграммы.  
    startangle = 0;
    for(var i = 0; i < data.length; i++) {
        // Точка, где заканчивается сектор  
        var endangle = startangle + angles[i];

        // Вычислить координаты точек пересечения радиусов, образующих сектор,
        // с окружностью. В соответствии с выбранными формулами углу 0 радиан
        // соответствует точка в самой верхней части окружности,  
        // а положительные значения откладываются от нее по часовой стрелке.
        var x1 = cx + r * Math.sin(startangle);
        var y1 = cy - r * Math.cos(startangle);
        var x2 = cx + r * Math.sin(endangle);
        var y2 = cy - r * Math.cos(endangle);
        
        // Это флаг для углов, больших половины окружности.  
        // Он необходим SVG-механизму рисования дуг  
        var big = 0;
        if (endangle - startangle > Math.PI) big = 1;
        
        // Мы описываем сектор с помощью элемента <svg:path>.  
        // Обратите внимание, что он создается вызовом createElementNS()
        var path = document.createElementNS(svgns, "path");
        
        // Эта строка хранит информацию о контуре, образующем сектор  
        var d = "M " + cx + "," + cy +  // Начало в центре окружности
            " L " + x1 + "," + y1 +     // Нарисовать линию к точке (x1,y1)
            " A " + r + "," + r +       // Нарисовать дугу с радиусом r
            " 0 " + big + " 1 " +       // Информация о дуге...  
            x2 + "," + y2 +             // Дуга заканчивается в точке (x2,y2)  
            " Z";                       // Закончить рисование в точке (сх.су)  

        // Теперь установить атрибуты элемента <svg:path>  
        path.setAttribute("d", d);              // Установить описание контура  
        path.setAttribute("fill", colors[i]);   // Установить цвет сектора
        path.setAttribute("stroke", "black");   // Рамка сектора - черная
        path.setAttribute("stroke-width", "2"); // 2 единицы толщиной
        chart.appendChild(path);                // Вставить сектор в диаграмму  

        // Следующий сектор начинается в точке, где закончился предыдущий
        startangle = endangle;

        // Нарисовать маленький квадрат для идентификации сектора в легенде
        var icon = document.createElementNS(svgns, "rect");
        icon.setAttribute("x", lx);             // Координаты квадрата  
        icon.setAttribute("y", ly + 30*i);
        icon.setAttribute("width", 20);         // Размер квадрата  
        icon.setAttribute("height", 20);
        icon.setAttribute("fill", colors[i]);   // Тем же цветом, что и сектор 
        icon.setAttribute("stroke", "black");   // Такая же рамка.  
        icon.setAttribute("stroke-width", "2");
        chart.appendChild(icon);                // Добавить в диаграмму  

        // Добавить метку правее квадрата  
        var label = document.createElementNS(svgns, "text");
        label.setAttribute("x", lx + 30);       // Координаты текста  
        label.setAttribute("y", ly + 30*i + 18);
        // Стиль текста можно также определить посредством таблицы CSS-стилей
        label.setAttribute("font-family", "sans-serif");
        label.setAttribute("font-size", "16");
        // Добавить текстовый DOM-узел в элемент   
        label.appendChild(document.createTextNode(labels[i]));
        chart.appendChild(label);               // Добавить текст в диаграмму 
    }

    return chart;
} 

Программный код в примере 21.2 относительно прост. Здесь выполняются некоторые математические расчеты для преобразования исходных данных в углы секторов круговой диаграммы. Однако основную часть примера составляет программный код, создающий SVG-элементы и выполняющий настройку их атрибутов. Чтобы этот пример мог работать в браузерах, не полностью поддерживающих стандарт HTML5, здесь формат SVG интерпретируется как грамматика XML и вместо метода createElement() используются пространство имен SVG и метод createElementNS().

Самая малопонятная часть этого примера – программный код, выполняющий рисование сектора диаграммы. Для отображения каждого сектора используется тег <svg:path>. Этот SVG-элемент описывает рисование произвольных фигур, состоящих из линий и кривых. Описание фигуры определяется атрибутом d элемента <svg:path>. Основу описания составляет компактная грамматика символьных кодов и чисел, определяющих координаты, углы и прочие значения. Например, символ M означает «move to» (переместиться в точку), и вслед за ним должны следовать координаты X и Y точки. Символ L означает «line to» (рисовать линию до точки); он рисует линию от текущей точки до точки с координатами, которые следуют далее. Кроме того, в этом примере используется символьный код A, который рисует дугу (arc). Вслед за этим символом следуют семь чисел, описывающих дугу. Точное описание нас здесь не интересует, но вы можете найти его в спецификации по адресу http://www.wS.org/TR/SVG/.

Обратите внимание, что функция pieChart() возвращает элемент <svg>, содержащий описание круговой диаграммы, но она не вставляет этот элемент в документ. Предполагается, что это будет делать вызывающая программа. Диаграмма, изображенная на рис. 21.3, была создана с помощью следующего файла:


<html> 
<head>  
<!--p><script src="PieChart.js"x/scr.ipt>  </p-->
</head>  
<body onload="document.body.appendChild(  
                      pieChart([12, 23, 34, 45]. 640, 400, 200, 200, 150,  
                               ['красный','синий','желтый','зеленый'],  
                               ['Север','Юг', 'Восток', 'Запад'], 400, 100)); 
              ">  
</body>  
</html>  

В примере 21.3 демонстрируется создание еще одного SVG-изображения: в нем формат SVG используется для отображения аналоговых часов (рис. 21.4). Однако вместо создания дерева SVG-элементов с самого начала, в нем используется статическое SVG-изображение циферблата, встроенное в HTML-страницу. Это статическое изображение включает два SVG-элемента <line>, представляющих часовую и минутную стрелки. Обе линии направлены вверх, в результате чего статическое изображение часов показывает время 12:00. Чтобы превратить это изображение в действующие часы, в примере используется сценарий на языке JavaScript, устанавливающий атрибут transform каждого элемента <line> и поворачивающий их на углы, соответствующие текущему времени.

Рис. 21.4. SVG-часы

Обратите внимание, что в примере 21.3 разметка SVG встроена непосредственно в файл HTML5 и в ней не используются пространства имен XML, как в приведенном выше XHTML-файле. Это означает, что данный пример будет работать только в браузерах, поддерживающих возможность непосредственного встраивания разметки SVG. Однако если преобразовать HTML-файл в XHTML, тот же самый прием будет работать и в старых браузерах с поддержкой SVG.

Пример 21.3. Отображение времени посредством манипулирования SVG-изображением


<!DOCTYPE HTML>
<html>
<head>
<title>Analog Clock</title>
<script>
function updateTime() { // Обновляет SVG-изображение часов 
                        // в соответствии с текущим временем 
    var now = new Date();                       // Текущее время 
    var min = now.getMinutes();                 // Минуты 
    var hour = (now.getHours() % 12) + min/60;  // Часы с дробной частью 
    var minangle = min*6;                       // 6 градусов на минуту 
    var hourangle = hour*30;                    // 30 градусов на час 

    // Get SVG elements for the hands of the clock
    var minhand = document.getElementById("minutehand");
    var hourhand = document.getElementById("hourhand");

    // Установить в них SVG-атрибут для перемещения по циферблату 
    minhand.setAttribute("transform", "rotate(" + minangle + ",50,50)");
    hourhand.setAttribute("transform", "rotate(" + hourangle + ",50,50)");

    // Обновлять показания часов 1 раз в минуту 
    setTimeout(updateTime, 60000);
}
</script>
<style>
/* Все следующие CSS-стили применяются к SVG-элементам, объявленным ниже  */
#clock {                          /* общие стили для всех элементов часов  */
   stroke: black;                 /* черные линии  */
   stroke-linecap: round;         /* с закругленными концами  */
   fill: #eef;                    /* на светлом, голубовато-сером фоне  */
}
#face { stroke-width: 3px;}       /* рамка циферблата часов  */
#ticks { stroke-width: 2; }       /* метки часов на циферблате  */
#hourhand {stroke-width: 5px;}    /* широкая часовая стрелка  */
#minutehand {stroke-width: 3px;}  /* узкая минутная стрелка  */
#numbers {                        /* стиль отображения цифр на циферблате  */
    font-family: sans-serif; font-size: 7pt; font-weight: bold; 
    text-anchor: middle; stroke: none; fill: black;
}
</style>
</head>
<body onload="updateTime()">
  <!-- viewBox – система координат, width и height - экранные размеры   -->
  <svg id="clock" viewBox="0 0 100 100" width="500" height="500"> 
    <defs>   <!-- Определить фильтр для рисования теней  -->
     <filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
        <feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur" />
        <feOffset in="blur" dx="1" dy="1" result="shadow" />
        <feMerge>
          <feMergeNode in="SourceGraphic"/><feMergeNode in="shadow"/>
        </feMerge>
      </filter>
    </defs>
    <circle id="face" cx="50" cy="50" r="45"/>  <!-- циферблат -->
    <g id="ticks">                               <!-- 12 часовых меток  -->
      <line x1='50' y1='5.000' x2='50.00' y2='10.00'/>
      <line x1='72.50' y1='11.03' x2='70.00' y2='15.36'/>
      <line x1='88.97' y1='27.50' x2='84.64' y2='30.00'/>
      <line x1='95.00' y1='50.00' x2='90.00' y2='50.00'/>
      <line x1='88.97' y1='72.50' x2='84.64' y2='70.00'/>
      <line x1='72.50' y1='88.97' x2='70.00' y2='84.64'/>
      <line x1='50.00' y1='95.00' x2='50.00' y2='90.00'/>
      <line x1='27.50' y1='88.97' x2='30.00' y2='84.64'/>
      <line x1='11.03' y1='72.50' x2='15.36' y2='70.00'/>
      <line x1='5.000' y1='50.00' x2='10.00' y2='50.00'/>
      <line x1='11.03' y1='27.50' x2='15.36' y2='30.00'/>
      <line x1='27.50' y1='11.03' x2='30.00' y2='15.36'/>
    </g>
    <g id="numbers">                  <!-- Числа в основных направлениях -->
      <text x="50" y="18">12</text><text x="85" y="53">3</text>
      <text x="50" y="88">6</text><text x="15" y="53">9</text>
    </g>
    <!-- Нарисовать стрелки, указывающие вверх. Они вращаются сценарием. -->
    <g id="hands" filter="url(#shadow)">  <!-- Добавить тени к стрелкам  -->
      <line id="hourhand" x1="50" y1="50" x2="50" y2="24"/>
      <line id="minutehand" x1="50" y1="50" x2="50" y2="20"/>
    </g>
  </svg>
</body>
</html>


содержание 21.4. Создание графики с помощью элемента <canvas> содержание

Элемент <canvas> не имеет собственного визуального представления, но он создает поверхность для рисования внутри документа и предоставляет сценариям на языке JavaScript мощные средства рисования. Элемент <canvas> стандартизован спецификацией HTML5, но существует дольше его. Впервые он был реализован компанией Apple в браузере Safari 1.3 и поддерживался браузерами Firefox, начиная с версии 1.5, и Opera, начиная с версии 9. Он также поддерживается всеми версиями Chrome. Элемент <canvas> не поддерживался браузером IE до версии IE9, но он с успехом имитировался в IE6, 7 и 8 с помощью свободно распространяемого проекта ExplorerCanvas, домашняя страница которого находится по адресу http://code.google.eom/p/explorercanvas/.

Существенное отличие между элементом <canvas> и технологией SVG заключается в том, что при использовании элемента <canvas> изображения формируются вызовами методов, в то время как при использовании формата SVG изображения описываются в виде дерева XML-элементов. Функционально эти два подхода эквивалентны: любой из них может моделироваться с использованием другого. Однако внешне они совершенно отличаются, и каждый из них имеет свои сильные и слабые стороны. Например, из SVG-рисунков легко можно удалять элементы. Чтобы удалить элемент из аналогичного рисунка, созданного в элементе <canvas>, обычно требуется полностью ликвидировать рисунок, а затем создать его заново. Поскольку прикладной интерфейс Canvas основан на синтаксисе JavaScript, а реализация рисунков с его помощью получается более компактной (чем при использовании формата SVG), я решил описать его в этой книге. Подробные сведения вы найдете в части IV книги в справочных статьях Canvas, CanvasRenderingContext2D и родственных им.

Трехмерная графика в элементе <canvas>

На момент написания этих строк производители браузеров уже приступили к реализации прикладного интерфейса рисования трехмерной графики в элементе <canvas>. Этот прикладной интерфейс называется WebGL и является связующим звеном между JavaScript и стандартным прикладным интерфейсом OpenGL. Чтобы получить объект контекста для рисования трехмерной графики, методу getContext() элемента <canvas> следует передать строку «webgl». WebGL – это обширный, сложный и низкоуровневый прикладной интерфейс, и он не описывается в этой книге: веб-разработчики, скорее всего, предпочтут использовать вспомогательные библиотеки, основанные на WebGL, чем непосредственно сам прикладной интерфейс WebGL. Большая часть прикладного интерфейса Canvas определена не в элементе <canvas>, а в объекте «контекста рисования», получить который можно методом getContext() элемента, играющего роль «холста». Вызов метода getContext() с аргументом «2d» возвращает объект CanvasRenderingContext2D, который можно использовать для рисования двухмерной графики. Важно понимать, что элемент <canvas> и объект контекста рисования – это два совершенно разных объекта. Поскольку класс объекта контекста имеет такое длинное имя, я редко буду ссылаться на объект СапvasRenderingContext2D по имени, а буду просто называть его «объектом контекста». Аналогично, когда я буду употреблять термин «прикладной интерфейс Canvas», я буду подразумевать «методы объекта CanvasRenderingContext2D». Ниже приводится HTML-страница, которая может служить простым примером использования прикладного интерфейса Canvas. Сценарий в ней рисует красный квадрат и голубой круг в элементе <canvas>, как показано на рис. 21.2:

<body>
Это красный квадрат: <canvas id="square" width=10 height=10></canvas>. 
Это голубой круг: <canvas id="circle" width=10 height=10></canvas>. 
<script> 
var canvas = document.getElementById("square"); // Найти первый элемент canvas 
var context = canvas.getContext("2d");          // Получить 2-мерный контекст 
context.fillStyle = "#f00";                     // Цвет заливки - красный
context.fillRect(0,0,10,10);                    // Залить квадрат  

canvas = document.getElementById("circle");     // Второй элемент canvas 
context = canvas.getContext("2d");              // Получить его контекст  
context. beginPath();                            // Начать новый "контур"  
context.arc(5, 5, 5, 0, 2*Math.PI, true);       // Добавить круг  
context.fillStyle = "#00f";                     // Цвет заливки - синий  
context.fill();                                 // Залить круг  
</script> 
</body> 

Мы видели, что грамматика SVG позволяет описывать сложные фигуры из прямых отрезков и кривых линий, которые могут быть нарисованы или залиты цветом. В прикладном интерфейсе объекта Canvas тоже используется понятие контура. Однако контур в данном случае описывается не как строка из символов и чисел, а как последовательность вызовов методов, таких как beginPath() и агс(), использованных в примере выше. После того как контур будет определен, к нему можно применять различные операции, выполняемые такими методами, как fill(). Особенности выполнения операций определяются различными свойствами объекта контекста, такими как fillStyle. В следующих подразделах рассказывается:

В конце этого раздела будет представлен практический пример, в котором элементы <canvas> будут использоваться для отображения небольших внутристрочных диаграмм (sparklines).

Во многих примерах работы с элементом <canvas>, которые приводятся ниже, используется переменная c. Эта переменная хранит объект CanvasRenderingContext2D элемента <canvas>, но инициализация этой переменной в самих примерах обычно не показана. Если у вас появится желание опробовать эти примеры, добавьте разметку HTML, определяющую элемент <canvas> с соответствующими атрибутами width и height, и следующий программный код, инициализирующий переменную c:

var canvas = document.getElementById("my_canvas_id");
var c = canvas.getContext('2d");

Рисунки, которые встретятся вам далее, были созданы сценариями JavaScript, использующими элемент <canvas> – обычно с очень большими размерами, чтобы создать изображения с высоким разрешением, пригодные для печати.


содержание 21.4.1. Рисование линий и заливка многоугольников

Чтобы нарисовать прямые линии в элементе <canvas> и залить внутреннюю область замкнутой фигуры, образуемой этими линиями, необходимо сначала определить контур (path). Контур – это последовательность из одного или более фрагментов контура. Фрагмент контура – это последовательность из двух или более точек, соединенных прямыми линиями (или, как будет показано ниже, кривыми). Создается новый контур с помощью метода beginPath(). А фрагмент контура – с помощью метода moveTo(). После установки начальной точки фрагмента контура вызовом метода moveTo() можно соединить эту точку с новой точкой прямой линией, вызвав метод LineTo(). Следующий фрагмент определяет контур, состоящий из двух прямых линий:

c.beginPath();      // Новый контур  
c.moveTo(100, 100); // Новый фрагмент контура с начальной точкой (100,100) 
c.lineTo(200, 200); // Добавить линию, соединяющую точки (100,100) и (200,200) 
c.lineTo(100, 200); // Добавить линию, соединяющую точки (200,200) и (100,200)

Этот фрагмент просто определяет контур – он ничего не рисует. Чтобы фактически нарисовать две линии, следует вызвать метод stroke(), а чтобы залить область, ограниченную этими линиями, следует вызвать метод fill():

c.fill(); // Залить область треугольника 
c.stroke(); // Нарисовать две стороны треугольника 

Этот фрагмент (плюс некоторый дополнительный программный код, устанавливающий толщину линий и цвет заливки) воспроизводит рисунок, изображенный на рис. 21.5.

Рис. 21.5. Простой путь, нарисованный и залитый

Обратите внимание, что фрагмент контура, определяемый выше, является «открытым». Он содержит всего две прямые линии, и его конечная точка не совпадает с начальной точкой. То есть он образует незамкнутую область. Метод fill() выполняет заливку открытых фрагментов контуров, как если бы конечная и начальная точка фрагмента контура были соединены прямой линией. Именно поэтому пример выше выполняет заливку треугольной области, но рисует только две стороны этого треугольника.

Если бы потребовалось нарисовать все три стороны треугольника выше, можно было бы вызвать метод closePath(), чтобы соединить конечную и начальную точки фрагмента контура. (Можно было бы также вызвать метод lineTo(100,100), но в этом случае получились бы три прямые линии с общей начальной и конечной точками, не образующие в действительности замкнутый фрагмент контура. При рисовании толстыми линиями результат визуально выглядит лучше, если используется метод closePath().)

Следует сделать еще два важных замечания, касающиеся методов stroke() и fill(). Во-первых, оба метода оперируют всеми элементами в текущем контуре. Допустим, что в примере выше был добавлен еще один фрагмент контура:

c.moveTo(300,100); // Новый фрагмент контура с начальной точкой (300,100); 
c.lineTo(300,200); // Нарисовать вертикальную линию вниз до точки (300,200); 

Если затем вызвать метод stroke(), получились бы две соединенные вместе стороны треугольника и не связанная с ними вертикальная линия.

Во-вторых, обратите внимание, что методы stroke() и fill() никогда не изменяют текущий контур: можно вызвать метод fill(), и от этого контур никуда не денется, когда вслед за этим будет вызван метод stroke(). Когда вы закончили работу с текущим контуром и приступаете к работе с новым контуром, нужно не забыть вызывать метод beginPath(). В противном случае вы просто добавите новый фрагмент контура в существующий контур, и старые фрагменты контура будут рисоваться снова и снова.

Пример 21.4 содержит определение функции рисования правильных многоугольников и демонстрирует использование методов moveTo(), lineTo() и closePath() для определения фрагментов контура и методов fill() и stroke() для рисования контуров. Он воспроизводит рисунок, изображенный на рис. 21.6.

Рис. 21.6. Правильные многоугольники

Пример 21.4. Рисование правильных многоугольников с помощью методов moveTo(), UneTo()UneTo() и closePath()

// Определяет правильный многоугольник с n сторонами, вписанный в окружность
// с центром в точке (x,y) и радиусом г. Вершины многоугольника находятся
// на окружности, на равном удалении друг от друга. Первая вершина помещается
// в верхнюю точку окружности или со смещением на указанный угол angle. Поворот 
// выполняется по часовой стрелке, если в последнем аргументе не передать
// значение true, 
function polygon(c,n,x,y,r,angle,counterclockwise) {
    angle = angle || 0;
    counterclockwise = counterclockwise || false;
    c.moveTo(x + r*Math.sin(angle),  // Новый фрагмент контура  
             y - r*Math.cos(angle)); // Исп. тригонометрию для выч. координат 
    var delta = 2*Math.PI/n;         // Угловое расстояние между вершинами 
    for(var i = 1; i < n; i++) {  // Для каждой из оставшихся вершин 
        angle += counterclockwise?-delta:delta; // Скорректировать угол 
        c.lineTo(x + r*Math.sin(angle),         // Линия к след. вершине  
                 y - r*Math.cos(angle));
    }
    c.closePath();                   // Соединить первую вершину с последней  
}

// Создать новый контур и добавить фрагменты контура,
// соответствующие многоугольникам
c.beginPath();
polygon(c, 3, 50, 70, 50);                   // Треугольник 
polygon(c, 4, 150, 60, 50, Math.PI/4);       // Квадрат
polygon(c, 5, 255, 55, 50);                  // Пятиугольник
polygon(c, 6, 365, 53, 50, Math.PI/6);       // Шестиугольник
polygon(c, 4, 365, 53, 20, Math.PI/4, true); // Квадрат в шестиугольнике

// Установить некоторые свойства, определяющие внешний вид рисунка
c.fillStyle = "#ccc";    // Светло-серый фон внутренних областей
c.strokeStyle = "#008";  // темно-синие контуры  
c.lineWidth = 5;         // толщиной пять пикселов.  

// Нарисовать все многоугольники (каждый создается в виде отдельного фрагмента
// контура) следующими вызовами  
c.fill();                // Залить фигуры  
c.stroke();              // И нарисовать контур  

Обратите внимание, что в этом примере внутри шестиугольника рисуется квадрат. Квадрат и шестиугольник являются отдельными фрагментами контура, но они перекрываются. Когда это происходит (или когда один фрагмент контура пересекается с самим собой), элементу <canvas> приходится выяснять, какая область является внутренней для фрагмента контура, а какая – внешней. Для этого элемент <canvas> использует алгоритм, известный как «правило ненулевого числа оборотов» («nonzero winding rule»). В данном случае внутренняя область квадрата не заливается светло-серым цветом, потому что квадрат и шестиугольник рисовались в противоположных направлениях: вершины шестиугольника соединялись линиями в направлении по часовой стрелке, а вершины квадрата – против часовой стрелки. Если бы рисование квадрата также выполнялось в направлении по часовой стрелке, метод fill() залил бы внутреннюю область квадрата.

Правило ненулевого числа оборотов

Чтобы определить, находится ли точка P внутри контура, используя правило правило ненулевого числа оборотов, представьте луч, исходящий из точки P в любом направлении и уходящий в бесконечность (или, если говорить практическим языком, до некоторой точки за пределами фигуры, образуемой рассматриваемым контуром). Теперь инициализируйте счетчик нулевым значением и проследите за всеми пересечениями контура с лучом. Каждый раз, когда луч пересекает контур, рисуемый в направлении по часовой стрелке, увеличивайте счетчик на единицу. Каждый раз, когда луч пересекает контур, рисуемый в направлении против часовой стрелки, уменьшайте счетчик на единицу. Если после учета всех пересечений в счетчике получается значение, не равное нулю, следовательно, точка P находится внутри контура. Если счетчик оказывается равным нулю, следовательно, точка P находится снаружи.


содержание 21.4.2. Графические атрибуты

В примере 21.4 устанавливаются свойства fillStyle, strokeStyle и lineWidth объекта контекста элемента <canvas>. Эти свойства являются графическими атрибутами, определяющими цвет, используемый методом fill(); цвет, используемый методом stroke(); и толщину линий, рисуемых методом stroke(). Обратите внимание, что эти параметры не передаются методам fill() и stroke(), а являются общими графическими свойствами элемента <canvas>. Если определяется метод, рисующий некоторую фигуру, который не устанавливает эти свойства, программа, использующая его, сможет сама определять цвет фигуры, устанавливая свойства strokeStyle и fillStyle перед вызовом этого метода. Такое отделение графических свойств от команд рисования является фундаментальной особенностью прикладного интерфейса объекта Canvas и сродни отделению представления от содержимого, достигаемого за счет применения таблиц стилей CSS к HTML-документам. Прикладной интерфейс объекта Canvas содержит 15 свойств графических атрибутов в объекте CanvasRenderingContext2D. Эти свойства перечислены в табл. 21.1 и подробно описываются ниже в соответствующих разделах.

Таблица 21.1. Графические атрибуты прикладного интерфейса объекта Canvas

СвойствоОписание
fillStyleцвет, градиент или шаблон, используемый для заливки
fontопределение шрифта в формате CSS для команд рисования текста
globalAlphaуровень прозрачности, назначаемый для всех пикселов при рисовании
globalCompositeOperationспособ объединения новых пикселов с существующими
lineCapформа концов линий
lineJoinформа вершин
lineWidthтолщина рисуемых линий
miterLimitмаксимальная длина острых вершин
textAlignвыравнивание текста по горизонтали
textBaselineвыравнивание текста по вертикали
shadowBlurчеткость теней
shadowColorцвет теней
shadowOffsetXгоризонтальное смещение теней
shadowOffsetYвертикальное смещение теней
strokeStyleцвет, градиент или шаблон, используемый для рисования линий

Так как прикладной интерфейс объекта Canvas определяет графические атрибуты в объекте контекста, может появиться идея вызвать метод getContext() несколько раз, чтобы получить несколько объектов контекста. Если бы это удалось, можно было бы определить для каждого из них различные атрибуты и использовать их как различные кисти разного цвета и разной толщины. К сожалению, элемент <canvas> нельзя использовать таким способом. Каждый элемент <canvas> имеет только один объект контекста, и каждый вызов метода getContext() возвращает один и тот же объект CanvasRenderingContext2D.

Тем не менее, несмотря на то что прикладной интерфейс объекта Canvas позволяет определить только один набор графических атрибутов, он предусматривает возможность сохранять текущие графические свойства, чтобы их можно было изменить и позднее легко восстановить прежние значения. Метод save() помещает текущие значения графических свойств в стек. Метод restore() выталкивает их со стека и восстанавливает самые последние сохраненные значения. В множество сохраняемых свойств входят все свойства, перечисленные в табл. 21.1, а также текущее преобразование системы координат и область отсечения (обе особенности рассматриваются ниже). Важно отметить, что текущее определение контура и координаты текущей точки не входят в множество сохраняемых графических свойств и не могут сохраняться и восстанавливаться.

Если вам потребуется больше гибкости, чем может обеспечит простой стек графических свойств, вы можете определить вспомогательные методы, такие как в примере 21.5.

Пример 21.5. Утилиты управления графическими свойствами

<!--21.05.graphics_state_utils.js-->
// Восстанавливает последние сохраненные значения графических свойств, 
// но не выталкивает их со стека. 
CanvasRenderingContext2D.prototype.revert = function() { 
  this.restore(); // Восстановить прежние значения графических свойств. 
  this.save(); // Сохранить их снова, чтобы можно было вернуться к ним. 
  return this; // Позволить составление цепочек вызовов методов. 
}; 

// Устанавливает графические атрибуты в соответствии со значениями свойств 
// объекта o. Или, при вызове без аргументов, возвращает текущие значения 
// атрибутов в виде объекта. Обратите внимание, что этот метод не обслуживает 
// преобразование или область отсечения. 
CanvasRenderingContext2D.prototype.attrs = function(o) { 
if (o) { 
        for(var a in o)       // Для каждого свойства объекта o 
            this[a] = o[a];   // Установить его как графический атрибут 
        return this;          // Позволить составление цепочек вызовов методов 
    }
    else return {
        fillStyle: this.fillStyle, font: this.font,
        globalAlpha: this.globalAlpha,
        globalCompositeOperation: this.globalCompositeOperation,
        lineCap: this.lineCap, lineJoin: this.lineJoin,
        lineWidth: this.lineWidth, miterLimit: this.miterLimit,
        textAlign: this.textAlign, textBaseline: this.textBaseline,
        shadowBlur: this.shadowBlur, shadowColor: this.shadowColor,
        shadowOffsetX: this.shadowOffsetX, shadowOffsetY: this.shadowOffsetY,
        strokeStyle: this.strokeStyle
    };
};

содержание 21.4.3. Размеры и система координат холста

Атрибуты width и height элемента <canvas> и соответствующие им свойства width и height объекта Canvas определяют размеры холста. По умолчанию начало системы координат холста (0,0) находится в его левом верхнем углу. Координата X увеличивается в направлении слева направо, а координата Y – сверху вниз. Координаты точек на холсте могут определяться вещественными значениями, и они не будут автоматически округляться до целых – для имитации частично заполненных пикселов объект Canvas использует приемы сглаживания. Размеры холста являются настолько фундаментальными характеристиками, что они не могут изменяться без полного сброса холста в исходное состояние. Изменение значения свойства width или height объекта Canvas (даже присваивание им текущих значений) вызывает очистку холста, стирание текущего контура и переустановку всех графических атрибутов (включая текущее преобразование и область отсечения) в исходное состояние.

Несмотря на фундаментальность размеров холста, они необязательно совпадают с размерами холста на экране или с количеством пикселов, образующих поверхность для рисования. Размеры холста (а также система координат по умолчанию) измеряются в CSS-пикселах. CSS-пикселы обычно совпадают с обычными пикселами. Однако на устройствах высокого разрешения реализациям разрешено отображать несколько аппаратных пикселов в один CSS-пиксел. Это означает, что размеры прямоугольной области в пикселах, отведенной для холста, могут оказаться больше номинальных размеров холста. Об этом следует помнить при работе с механизмами холста, манипулирующими пикселами (раздел 21.4.14), но в других случаях различия между виртуальными CSS-пикселами и фактическим аппаратными пикселами не оказывают влияния на программный код, выполняющий операции с холстом.

По умолчанию элемент <canvas> отображается на экране с размерами (в CSS-пикселах), указанными в его HTML-атрибутах width и height. Однако, подобно любым другим HTML-элементам, элемент <canvas> может иметь экранные размеры, определяемые CSS-атрибутами стиля width и height. Если экранные размеры холста отличаются от его фактических размеров, пикселы холста автоматически будут масштабироваться в соответствии с экранными размерами, указанными в CSS-атрибутах. Экранные размеры холста никак не влияют на количество CSS- или аппаратных пикселов, зарезервированных в растровом отображении холста, а масштабирование выполняется как обычная операция масштабирования изображений. Если экранные размеры оказываются существенно больше фактических размеров холста, это приводит к появлению мозаичного эффекта. Однако это проблема скорее для художников и никак не влияет на программирование холста.

содержание 21.4.4. Преобразование системы координат

Как отмечалось выше, начало системы координат холста по умолчанию находится в левом верхнем углу, значение координаты X увеличивается в направлении слева направо, а координаты Y – сверху вниз. В этой системе координат по умолчанию координаты точки отображаются непосредственно в CSS-пикселы (которые затем отображаются в один или более аппаратных пикселов). Некоторые операции с холстом и атрибутами (такие как извлечение параметров пикселов и установка смещения теней) всегда используют систему координат по умолчанию. Однако помимо системы координат по умолчанию каждый холст имеет в составе графических свойств «текущую матрицу преобразований». Эта матрица определяет текущую систему координат холста. В большинстве операций с холстом, где указываются координаты точки, используется текущая система координат, а не система координат по умолчанию. Текущая матрица преобразований используется для преобразования указанных вами координат в эквивалентные им координаты в системе координат по умолчанию.

Метод setTransform() позволяет напрямую определять матрицу преобразований холста, но обычно преобразования системы координат проще определять как последовательность операций смещения, вращения и масштабирования. Влияние этих операций на систему координат холста иллюстрируются на рис. 21.7. Программа, с помощью которой был получен этот рисунок, семь раз подряд использовала одно и то же изображение осей координат. Единственное, что изменялось каждый раз, – это текущее преобразование. Обратите внимание, что преобразования оказывают влияние не только на рисование линий, но и на вывод текста. Метод translate() просто смещает начало системы координат влево, вправо, вверх или вниз. Метод rotate() выполняет вращение осей координат по часовой стрелке на указанный угол. (Прикладной интерфейс объекта Canvas всегда измеряет углы в радианах. Чтобы преобразовать градусы в радианы, необходимо разделить значение в градусах на 180 и умножить на Math.PI.) Метод scale() растягивает или сжимает расстояния по оси X или Y.

Передача отрицательного коэффициента масштабирования методу scale() переворачивает соответствующую ему ось относительно начала системы координат, создавая зеркальное ее отражение. Именно это преобразование можно наблюдать внизу слева на рис. 21.7: вызов метода translate() сместил начало координат в левый нижний угол холста, а вызов метода scale() перевернул ось Y так, что значения координаты Y стали увеличиваться в направлении снизу вверх. Такая перевернутая система координат должна быть знакома вам по школьному курсу алгебры, и она может пригодиться для рисования графиков и диаграмм. Отметьте, однако, что текст после такого преобразования очень трудно читать!

Рис. 21.7. Преобразования системы координат

21.4.4.1. Математический смысл преобразований

На мой взгляд, проще всего разбираться с преобразованиями, имея их геометрическое представление, когда действие методов translate(), rotate() и scale() можно представить в виде преобразований координатных осей, как показано на рис. 21.7. Преобразования можно также представить алгебраически, в виде системы уравнений, отображающих координаты точки (x,y) в преобразованной системе координат в координаты той же точки (x',y') в исходной системе координат.


Вызов метода c.translate(dx,dy) можно описать следующими уравнениями:
x' = x + dx; // Значение 0 координаты X в новой системе координат 
             // соответствует значению dx в старой системе координат
y' = y + dy;

Операцию масштабирования также можно представить в виде простых уравнений. Вызов метода c.scale(sx,sy) можно описать следующим образом:

x' = sx * x;  
y' = sy * y; 

Операция вращения выглядит несколько сложнее. Вызов c.rotate(a) описывается следующими тригонометрическими уравнениями:

x' = x * cos(a) - y * sin(a);
y' = y * cos(a) + x * sin(a);

Обратите внимание, что порядок выполнения преобразований имеет большое значение. Пусть изначально используется система координат холста по умолчанию, после чего выполняется смещение и затем масштабирование. Чтобы отобразить координаты точки (x,y) в текущей системе координат обратно в координаты (x'',y'') в системе координат по умолчанию, необходимо сначала применить уравнения масштабирования, чтобы отобразить координаты точки в промежуточные координаты (x',y') точки в смещенной, но не масштабированной системе координат, а затем применить уравнения смещения, чтобы отобразить эти промежуточные координаты точки в координаты (x'',y''). В результате получим следующую систему уравнений:

x'' = sx*x + dx; 
y'' = sy*y + dy;

Если же к исходной системе координат сначала применялся метод scale(), а затем translate(), мы придем к другой системе уравнений:

x'' = sx*(x + dx); 
y'' = sy*(y + dy); 

При использовании алгебраических представлений последовательностей преобразований важно помнить, что в уравнениях они должны следовать в обратном порядке – от последнего преобразования к первому. Однако при использовании геометрических представлений вы работаете с последовательностями преобразований в прямом порядке, от первого к последнему.

Преобразования, поддерживаемые холстом, известны как аффинные преобразования. Аффинные преобразования могут изменять расстояния между точками и углы между линиями, но параллельные линии всегда остаются параллельными после любых аффинных преобразований – с помощью аффинных преобразований нельзя, например, создать эффект искажения типа «рыбий глаз». Любое аффинное преобразование можно описать с помощью шести параметров от a до f, как показано в следующих уравнениях:

x' = ax + cy + e 
y' = bx + dy + f 

К текущей системе координат можно применять любые преобразования, передавая эти шесть параметров методу transform(). На рис. 21.7 показаны два типа преобразований – сдвиг и вращение вокруг указанной точки – которые можно реализовать с помощью метода transfоrm(), как показано ниже:

// Сдвиг: 
// x' = x + kx*y; 
//y'' = y + ky*x;
function shear(c,kx,ky) { с.transform(1, ky, kx, 1, 0, 0); }  
// Вращение на theta радиан по часовой стрелке вокруг точки (x,y). 
// Это преобразование можно выполнить 
// с помощью последовательности вызовов методов 
translate,rotate,translate function rotateAbout(c,theta,x,y) { 
  var ct = Math.cos(theta), st = Math.sin(theta); 
  c.transform(ct, -st, st, ct, -x*ct-y*st+x, x*st-y*ct+y); 
}

Метод setTransform() принимает те же аргументы, что и метод transform(), но вместо преобразования текущей системы координат он выполняет преобразование системы координат по умолчанию и делает результат новой текущей системой координат. Метод setTransform() удобно использовать для временного возврата к системе координат по умолчанию:

c.save();                    // Сохранить текущую систему координат  
c.setTransform(1,0,0,1,0,0); // Вернуться к системе координат по умолчанию 
// Выполнить операции с использованием координат по умолчанию CSS-пикселов 
c.restore();                 // Восстановить сохраненную систему координат 

21.4.4.2. Примеры преобразований

Пример 21.6 демонстрирует мощь, которую дает возможность преобразования системы координат, где за счет рекурсивного применения методов translate(), rotate() и scale() реализовано рисование фракталов – снежинок Коха. Результат работы этого примера представлен на рис. 21.8, где показаны снежинки Коха с количеством уровней рекурсии О, 1, 2, 3 и 4.

Рис. 21.8. Снежинки Коха

Эти фигуры воспроизводятся весьма изящным программным кодом, но в нем используются рекурсивные преобразования системы координат, что делает его сложным для понимания. Даже если вы не собираетесь углубляться в изучение всех тонкостей примера, обратите все же внимание, что в нем имеется всего один вызов метода lineTo(). Каждый отдельный сегмент на рис. 21.8 рисуется следующим образом:

c.lineTo(len, 0);

Значение переменной len не изменяется в ходе выполнения программы, поэтому позиция, ориентация и длина каждой линии определяется операциями смещения, вращения и масштабирования.

Пример 21.6. Рисование снежинок Коха посредством преобразований системы координат


<script>
var deg = Math.PI/180;  // Для преобразования градусов в радианы

// Рисует n-уровневый фрактал снежинки Коха в контексте холста c, левый нижний
// угол которого имеет координаты (x,y), а длина стороны равна len.
function snowflake(c, n, x, y, len) {
    c.save();           // Сохранить текущее преобразование
    c.translate(x,y);   // Сместить начало координат в начальную точку
    c.moveTo(0,0);      // Новый фрагмент контура в новом начале координат
    leg(n);             // Нарисовать первую ветвь снежинки
    c.rotate(-120*deg); // Поворот на 120 градусов против часовой стрелки
    leg(n);             // Нарисовать вторую ветвь
    c.rotate(-120*deg); //Поворот
    leg(n);             // Нарисовать последнюю ветвь
    c.closePath();      // Замкнуть фрагмент контура
    c.restore();        // Восстановить прежнее преобразование

    // Рисует одну ветвь n-уровневой снежинки Коха. Эта функция оставляет
    // текущую позицию в конце нарисованной ветви и смещает начало координат так,
    // что текущая точка оказывается в позиции (0,0).
    // Это означает, что после рисования ветви можно вызвать rotate().
    function leg(n) {
        c.save();               // Сохранить текущее преобразование
        if (n == 0) {           // Нерекурсивный случай:
            c.lineTo(len, 0);   // Просто нарисовать горизонтальную линию
        }                       
        else {                  // Рекурсивный случай: 4 подветви вида: \/
            c.scale(1/3,1/3);   // Подветви в 3 раза меньше этой ветви
            leg(n-1);           // Рекурсия для первой подветви
            c.rotate(60*deg);   // Поворот на 60 градусов по часовой стрелке
            leg(n-1);           // Вторая подветвь
            c.rotate(-120*deg); // Поворот на 120 градусов назад
            leg(n-1);           // Третья подветвь
            c.rotate(60*deg);   // Поворот обратно к началу
            leg(n-1);           // Последняя подветвь
        }
        c.restore();            // Восстановить преобразование
        c.translate(len, 0);    // Но сместить в конец ветви (0,0)
    }
}


содержание 21.4.5. Рисование и заливка кривых

Контур – это последовательность фрагментов контура, а фрагмент контура – это последовательность точек, соединенных линиями. В контурах, которые определялись в разделе 21.4.1, эти точки соединялись прямыми линиями, но это не обязательно должно быть так. Объект CanvasRenderingContext2D определяет несколько методов, которые добавляют во фрагмент контура новую точку и соединяют ее кривой с текущей точкой:

агс()
Этот метод добавляет во фрагмент контура дугу. Он соединяет текущую точку с началом дуги прямой линией и затем соединяет начало дуги с концом дуги сегментом окружности, при этом конечная точка дуги становится новой текущей точкой. Дуга определяется шестью параметрами: координаты X и Y центра окружности, радиус окружности, угол начала и конца дуги и направление (по часовой стрелке или против часовой стрелки) рисования дуги между этими двумя углами.
агсТо()
Этот метод так же рисует прямую линию и круговую дугу, как и метод агс(), но при его использовании дуга определяется другим набором параметров. Аргументы метода агсТо() определяют точки P1 и P2 и радиус. Дуга, добавляемая в контур, имеет указанный радиус и строится так, что линии, соединяющие текущую точку с точкой P1 и точку P1 с точкой P2, являются касательными к ней. Такой необычный, на первый взгляд, способ определения дуг в действительности является весьма удобным при рисовании закругленных углов. Если указать радиус равный 0, этот метод просто нарисует прямую линию, соединяющую текущую точку и точку P1. Однако если указан ненулевой радиус, он нарисует прямую линию от текущей точки в направлении точки P1 до начала дуги, затем начнет рисовать круговую дугу, пока направление рисования не совпадет с направлением на точку P2.
bezierCurveTo()
Этот метод добавит во фрагмент контура новую точку P и соединит ее с текущей точкой кубической кривой Безье. Форма кривой определяется двумя «контрольными точками» C1 и C2. В начале кривой (в текущей точке) рисование начинается в направлении точки C1. В свой конец (в точке P) кривая приходит в направлении из точки C2. Между этими точками кривая плавно изгибается. Точка P становится новой текущей точкой для фрагмента контура.
quadraticCurveTo()
Этот метод похож на метод bezierCurveTo(), но рисует квадратичные кривые Безье и имеет всего одну контрольную точку.

С помощью этих методов можно рисовать контуры, подобные тем, что изображены на рис. 21.9.

>

Рис. 21.9. Контуры, состоящие из кривых

В примере 21.7 представлен программный код, с помощью которого было создано изображение на рис. 21.9. Методы, используемые в этом примере, являются одними из самых сложных в прикладном интерфейсе объекта Canvas. Полное описание методов и их аргументов приводится в справочном разделе книги.

Пример 21.7. Добавление кривых в контур

<!--21.07.curves.js-->
// Вспомогательная функция для преобразования градусов в радианы 
function rads(x) { return Math.PI*x/180; }  

// Нарисовать окружность. Используйте масштабирование и вращение, если требуется 
// получить эллипс. Здесь не используется текущая точка, поэтому окружность 
// рисуется без прямой линии, соединяющей текущую точку с началом окружности. 
c.beginPath();
c.arc(75,100,50,          // Центр в точке (75,100), радиус 50 
      0,rads(360),false); // По часовой стрелке от 0 до 360 градусов 

// Нарисовать сектор. Углы откладываются по часовой стрелке 
// от положительной оси x. Обратите внимание, что метод агс() добавляет линию 
// от текущей точки к началу дуги. 
c.moveTo(200, 100);       // Перейти в центр окружности 
c.arc(200, 100, 50,       // Центр окружности и радиус 
      rads(-60), rads(0), // Начальный угол -60 градусов, конечный 0 градусов 
      false);             // false означает по часовой стрелке 
c.closePath();            // Добавить прямую линю к центру окружности 

// Тот же сектор, в противоположном направлении 
c.moveTo(325, 100);
c.arc(325, 100, 50, rads(-60), rads(0), true); // Против часовой стрелки 
c.closePath();

// Использовать агсТо() для закругления углов. Здесь рисуется квадрат 
// с верхним левым углом в точке (400,50), с закруглениями углов дугами 
// с разными радиусами. 
c.moveTo(450, 50);           // Середина верхней стороны. 
c.arcTo(500,50,500,150,30);  // Часть верхней стороны и правый верхний угол, 
c.arcTo(500,150,400,150,20); // Добавить правую сторону и правый нижний угол, 
c.arcTo(400,150,400,50,10);  // Добавить нижнюю сторону и левый нижний угол. 
c.arcTo(400,50,500,50,0);    // Добавить левую сторону и левый верхний угол. 
c.closePath();               // Замкнуть контур, чтобы добавить 
                             // остаток верхней стороны. 

// Квадратичная кривая Безье: одна контрольная точка 
c.moveTo(75, 250);                      // Начало в точке (75,250) 
c.quadraticCurveTo(100,200, 175, 250);  // Соединить с точкой (175,250) 
c.fillRect(100-3,200-3,6,6);            // Метка контрольной точки (100,200) 

// Кубическая кривая Безье 
c.moveTo(200, 250);                       // Начало в точке (200,250) 
c.bezierCurveTo(220,220,280,280,300,250); // Соединить с точкой (300,250) 
c.fillRect(220-3,220-3,6,6);              // Метки контрольных точек 
c.fillRect(280-3,280-3,6,6);

// Определить некоторые графические атрибуты и нарисовать кривые 
c.fillStyle = "#aaa";  // Серый цвет заливки 
c.lineWidth = 5;       // Черные (по умолчанию) линии толщиной 5 пикселов 
c.fill();              // Залить фигуры 
c.stroke();            // Нарисовать контуры 

содержание 21.4.6. Прямоугольники

Объект CanvasRenderingContext2D определяет четыре метода рисования прямоугольников. Один из них, fillRect(), использовался в примере 21.7 для создания меток контрольных точек кривых Безье. Все четыре метода рисования прямоугольников принимают два аргумента, определяющих координаты одного угла прямоугольника, и два аргумента, определяющих ширину и высоту. Обычно указывается верхний левый угол и положительные значения ширины и высоты, но можно также указать другие углы и передать отрицательные размеры. Метод fillRect() выполняет заливку внутренней области прямоугольника в соответствии со значением атрибута fillStyle. Метод strokeRect() рисует контур прямоугольника, используя текущее значение атрибута strokeStyle и других атрибутов линий. Метод clearRect() подобен методу fillRect(), но он игнорирует текущее значение стиля заливки и заполняет прямоугольник прозрачными черными пикселами (цвет по умолчанию всех пустых холстов). Важно отметить, что все эти три метода не оказывают влияния ни на текущий контур, ни на текущую точку внутри этого контура.

Последний метод рисования прямоугольников называется rect(), и он изменяет текущий контур: он добавляет указанный прямоугольник в виде отдельного фрагмента контура. Подобно другим методам определения контуров, сам по себе он не производит ни заливку, ни рисование контура.


содержание 21.4.7. Цвет, прозрачность, градиенты и шаблоны

Атрибуты strokeStyle и fillStyle определяют параметры рисования линий и заливки областей. Чаще всего эти атрибуты используются, чтобы определить непрозрачный или полупрозрачный цвет, но им также можно присваивать объекты CanvasPattern и CanvasGradient, чтобы рисование или заливка выполнялась с использованием повторяющегося изображения или линейного или радиального градиента. Кроме того, можно воспользоваться свойством globalAlpha, чтобы сделать полупрозрачным все, что будет рисоваться.

Чтобы определить сплошной цвет, можно использовать имена цветов, определяемые стандартом HTML4¹, или использовать строки в формате CSS:

¹Aqua, black, blue, fuchsia, gray, green, lime, maroon, navy, olive, purple, red, silver, teal, white и yellow.
context.strokeStyle = "blue";  // Рисовать синие линии 
context.fillStyle = "#aaa";    // Заливку выполнять серым цветом

По умолчанию свойства strokeStyle и fillStyle имеют значение «#000000», соответствующее непрозрачному черному цвету.

Текущие браузеры поддерживают цвета CSS3 и позволяют использовать форматы RGB, RGBA, HSL и HSLA определения цветов вдобавок к базовому формату RGB. Например:

var colors = [
  "#f44",                       // Шестнадцатеричное значение RGB: красный
  "#44ff44",                    // Шестнадцатеричное значение RRGGBB: зеленый
  "rgb(60, 60, 255)",           // RGB в виде целых 0-255: синий
  "rgb(100%, 25%, 100%)",       // RGB в виде процентов: пурпурный
  "rgba(100%, 25%, 100%, 0.5)", // RGB плюс альфа 0-1: полупрозрачный пурпурный
  "rgba(0,0,0,0)",              // Совершенно прозрачный черный
  "transparent",                // Синоним предыдущего цвета
  "hsl(60, 100%, 50%)",         // Насыщенный желтый
  "hsl(60, 75%, 50%)",          // Менее насыщенный желтый
  "hsl(60, 100%, 75%)",         // Насыщенный желтый, немного светлее
  "hsl(60, 100%, 25%)",         // Насыщенный желтый, немного темнее
  "hsla(60, 100%, 50%, 0.5)",   // Насыщенный желтый, прозрачный на 50%
];

При использовании формата HSL цвет описывается тремя числами, определяющими тон (hue), насыщенность (saturation) и яркость (lightness). Тон (hue) – это величина угла в цветовом круге. Значение 0 соответствует красному цвету, 60 – желтому, 120 – зеленому, 180 – бирюзовому, 240 – синему, 300 – сиреневому и 360 – опять красному. Насыщенность описывает интенсивность цвета и определяется в процентах. Цвета с насыщенностью 0% являются оттенками серого. Яркость описывает степень яркости цвета и также определяется в процентах. Любой цвет в формате HSL со 100-процентной яркостью является белым цветом, а любой цвет с яркостью 0% – черным. В формате HSLA цвет описывается так же, как в формате HSL, но с дополнительным значением альфа-канала, которое изменяется в диапазоне от 0.0 (прозрачный) до 1.0 (непрозрачный).

Если необходимо использовать полупрозрачные цвета, но нежелательно явно указывать значение альфа-канала для каждого цвета, или если необходимо добавить полупрозрачность к непрозрачному изображению или шаблону (например), требуемое значение непрозрачности можно присвоить свойству globalAlpha. Значение альфа-канала каждого пиксела, рисуемого вами, будет умножаться на значение свойства globalAlpha. По умолчанию это свойство имеет значение 1 и не добавляет прозрачности. Если свойству globalAlpha присвоить значение 0, все нарисованное вами станет полностью прозрачным и на холсте ничего не будет видно. Если присвоить этому свойству значение 0.5, непрозрачные пикселы станут наполовину прозрачными. А пикселы, степень непрозрачности которых была равна 50%, станут непрозрачными на 25%. Изменение значения свойства globalAlpha оказывает влияние на степень непрозрачности всех пикселов, поэтому вам, вероятно, потребуется учесть, как эти пикселы объединяются (или «составляются») с пикселами, поверх которых они нарисованы – режимы объединения, поддерживаемые объектом Canvas, описываются в разделе 21.4.13. Вместо сплошного цвета (пусть и полупрозрачного), заливку и рисование контуров можно также выполнять с использованием градиентов и повторяющихся изображений. На рис. 21.10 изображен прямоугольник, контур которого нарисован толстыми линиями с использованием шаблонного изображения поверх заливки линейным градиентом и под заливкой радиальным градиентом. Ниже описывается, как было реализовано рисование шаблонным изображением и заливка градиентами.

Рис. 21.10. Рисование шаблоном и заливка градиентом

Чтобы выполнить заливку или рисование с применением шаблонного изображения вместо цвета, следует присвоить свойству fillStyle или strokeStyle объект CanvasPattern, возвращаемый методом createPattern() объекта контекста:

var image = document.getElementById("myimage");
с.fillStyle = c.createPattern(image, "repeat");

Первый аргумент метода createPattern() определяет изображение, которое будет использовано как шаблон. Это должен быть элемент документа <img>, <canvas> или <video> (или объект Image, созданный конструктором Image()). Во втором аргументе обычно передается строка «repeat», если требуется повторять изображение при заполнении, независимо от его размера, но можно также использовать значения «repeat-x», «repeat-y» или «no-repeat».

Обратите внимание, что в качестве шаблонного изображения для одного элемента <canvas> можно использовать другой элемент <canvas> (даже если он не включен в состав документа и невидим на экране):

var offscreen = document.createElement("canvas");  // Невидимый холст 
offscreen.width = offscreen.height = 10;           // Установить его размеры 
offscreen.getContext("2d").strokeRect(0,0,6,6);    // Получить контекст 
                                                   // и нарисовать прямоугольник
var pattern = с.createPattern(offscreen,"repeat"); // И использовать как шаблон

Чтобы выполнить заливку (или нарисовать контур) градиентом, следует присвоить свойству fillStyle (или strokeStyle) объект CanvasGradient, возвращаемый методом createLinearGradient() или createRadialGradient() объекта контекста. Создание градиентов выполняется в несколько этапов, и в использовании они несколько сложнее, чем шаблонные изображения.

Первый этап – создание объекта CanvasGradient. В качестве аргументов методу createRadialGradient() передаются координаты двух точек, определяющих линию (она необязательно должна быть горизонтальной или вертикальной), вдоль которой будет изменяться цвет. Аргументы метода createRadialGradient() определяют центры и радиусы двух окружностей. (Они необязательно должны быть концентрическими, но первая окружность обычно полностью располагается внутри второй.) Области внутри малой окружности и за пределами большой окружности будут заполняться сплошным цветом, а область между окружностями – градиентом. После того как объект CanvasGradient создан и определены области холста для заливки, необходимо определить цвета градиента вызовом метода addColorStop() объекта CanvasGradient. В первом аргументе этому методу передается число в диапазоне от 0.0 до 1.0. Во втором – цвет в формате, поддерживаемом CSS. Этот метод должен вызываться как минимум два раза, чтобы определить простой градиент, но его можно вызвать большее число раз. Цвет, соответствующий значению 0.0, будет использоваться в начале градиента, а цвет, соответствующий значению 1.0, – в конце. Если вы решите указать дополнительные цвета, они будут использоваться в промежуточных позициях градиента. В любой другой точке градиента значение цвета будет вычисляться методом интерполяции. Например:


// Линейный градиент, по диагонали холста 
// (предполагается, что преобразования отсутствуют)
var bgfade = c.createLinearGradient(0,0,canvas.width,canvas.height);
bgfade.addColorStop(0.0, "#88f");                 // От светло-синего вверху слева
bgfade.addColorStop(1.0, "#fff");                 // До белого внизу справа
// Градиент между двумя концентрическими окружностями. Прозрачный в середине
// до полупрозрачного серого и опять до прозрачного.
var peekhole = c.createRadialGradient(300,300,100, 300,300,300);
peekhole.addColorStop(0.0, "transparent");          // Прозрачный
peekhole.addColorStop(0.7, "rgba(100,100,100,.9)"); // Полупрозрачный серый
peekhole.addColorStop(1.0, "rgba(0,0,0,0)");        // Опять прозрачный

Важно понимать, что градиенты тесно связаны с местоположением в холсте. При создании градиента вы указываете его границы. Если после этого попытаться выполнить заливку области за этими границами, будет использован сплошной цвет, соответствующий одному или другому концу градиента. Если, к примеру, вы определите градиент вдоль линии, соединяющей точки (0,0) и (100, 100), этот градиент можно будет использовать только для заливки объектов, находящихся внутри прямоугольной области (0,0,100,100).

Рисунок, изображенный на рис. 21.10, был создан с использованием шаблона pattern и градиентовbgfade и peekhole с помощью следующего программного кода:

c.fillStyle = bgfade; // Сначала использовать линейный градиент 
с.fillRect(0,0,600.600); // Залить весь холст
с.strokeStyle = pattern; // Использовать шаблон для рисования линий 
c.lineWidth = 100; // Очень толстые линии
с.strokeRect(100,100,400,400); // Нарисовать большой квадрат
с.fillStyle = peekhole; // Использовать радиальный градиент
с.fillRect(0,0,600.600); // Покрыть холст этой полупрозрачной заливкой

содержание 21.4.8. Атрибуты рисования линий

Вы уже знакомы со свойством lineWidth, которое определяет толщину линий, рисуемых методами stroke() и strokeRect(). Кроме свойства lineWidth (и конечно же, strokeStyle) существует еще три графических атрибута, влияющих на рисование линий.

По умолчанию свойство lineWidth имеет значение 1, и ему можно присвоить любое положительное целое число, и даже дробное число меньше 1. (Линии толщиной менее одного пиксела рисуются полупрозрачными цветами, поэтому они выглядят менее темными по сравнению с линиями толщиной в 1 пиксел). Чтобы полностью понять действие свойства lineWidth, представьте контур как комбинацию бесконечно тонких одномерных линий. Прямые линии и кривые, рисуемые методом stroke(), центрируются по этому контуру, выступая на половину lineWidth в обе стороны. Если при рисовании замкнутого контура необходимо, чтобы видимы были только части линий за пределами контура, нарисуйте сначала контур, а затем залейте его непрозрачным цветом, чтобы скрыть части линий, которые вторгаются внутрь контура. Или, если необходимо, чтобы видимы были только части линий внутри замкнутого контура, вызовите сначала методы save() и clip() (раздел 21.4.10), а затем методы stroke() и restore().

Из-за имеющейся возможности изменять масштаб по осям координат, как показано на рис. 21.7, толщина линий зависит от текущего преобразования. Если выполнить вызов scale(2,1), чтобы изменить масштаб по оси X и оставить нетронутым масштаб по оси Y, вертикальные линии будут получаться в два раза толще горизонтальных, нарисованных с одним и тем же значением свойства lineWidth. Важно понимать, что толщина линий определяется значением свойства lineWidth и текущим преобразованием, имевшимися на момент вызова метода stroke(), а не на момент вызова метода LineTo() или другого метода конструирования контура. Три других атрибута рисования линий определяют внешний вид несоединенных концов контура и вершин, где соединяются два фрагмента контура. Они оказывают весьма несущественное влияние на внешний вид тонких линий, но обеспечивают существенные отличия при рисовании толстых линий. Действие двух этих свойств иллюстрируется изображением на рис. 21.11. Здесь контур показан как тонкая черная линия, а результат рисования линий – как окружающая ее серая область.

>

Рис. 21.11. Действие атрибутов lineCap и lineJoin

Свойство lineCap определяет, как будут выглядеть концы незамкнутых фрагментов контуров. Значение «butt» (по умолчанию) соответствует завершению линий непосредственно в конечной точке. При значении «square» линия будет продолжена за конечную точку на половину толщины и будет иметь квадратный конец. А при значении «round» линия будет продолжена за конечную точку на половину толщины и будет иметь закругленный конец (с радиусом закругления в половину толщины линии).

Свойство lineJoin определяет внешний вид вершин, соединяющих фрагменты контура. По умолчанию это свойство имеет значение «miter», при котором внешние края линий двух фрагментов контура будут продолжены, пока они не встретятся. При значении «round» вершины получают закругленную форму, а при значении «bevel» вершины обрезаются прямыми линиями.

Последнее свойство, связанное с рисованием линий, – это свойство miterLimit, которое используется, только когда свойство lineJoin имеет значение «miter». Когда две линии соединяются под острым углом, сопряжение между ними может оказаться довольно протяженным, и эти протяженные сопряжения могут нарушать визуальную гармонию. Свойство miterLimit определяет верхнюю границу протяженности сопряжений. Если сопряжение в некоторой вершине оказывается длиннее половины длины линии, умноженной на значение miterLimit, эта вершина будет нарисована с обрезанным сопряжением.

содержание 21.4.9. Текст

Для рисования текста в холсте обычно используется метод fillText(), который рисует текст, используя цвет (градиент или шаблон), определяемый свойством fillStyle. Если необходимо использовать специальные эффекты при выводе текста крупными символами, для рисования контуров отдельных символов можно применить метод strokeText() (пример вывода контуров символов приводится на рис. 21.13). Оба метода принимают в первом аргументе текст, который требуется нарисовать, и координаты X и Y вывода текста во втором и третьем аргументах. Ни один из этих методов не вносит изменений в текущий контур и не смещает текущую точку. Как видно на рис. 21.7, при выводе текста учитывается текущее преобразование системы координат.

Свойство font определяет шрифт, который будет использоваться для рисования текста. Значение этого свойства должно быть строкой с соблюдением синтаксиса CSS-атрибута font. Например:

"48pt sans-serif"
"bold 18px Times Roman"
"italic 12pt monospaced"
"bolder smaller serif" // жирнее и меньше, чем шрифт элемента <canvas> 

Свойство textAlign определяет способ выравнивания текста по горизонтали с учетом координаты X, переданной методу fillText() или strokeText(). Свойство textBaseline определяет способ выравнивания текста по вертикали с учетом координаты Y. На рис. 21.12 показано действие всех допустимых значений этих свойств. Тонкая линия рядом с каждой строкой текста – это опорная линия шрифта, а маленький квадратик обозначает точку (x,y), координаты которой были переданы методу fillText().


По умолчанию свойство textAlign имеет значение «start». Обратите внимание, что для текста, который записывается слева направо, выравнивание, определяемое значением «start», совпадает с выравниванием, определяемым значением «left», аналогично совпадает и действие значений «end» «right». Однако если в элементе <canvas> определить атрибут dir со значением «rtl» (right-to-left – справа налево), действие значения «start» выравнивания будет совпадать с действием значения «right», а действие значения «end» – с действием значения «left».

Рис. 21.12. Действие свойств textAlign и textBaseline

Свойство textBaseline по умолчанию имеет значение «alphabetic», которое соответствует алфавиту Latin и подобным ему. Значение «ideographic» используется совместно с идеографическими алфавитами, такими как китайский и японский. Значение «hanging» предназначено для использования со слоговыми и подобными им алфавитами (которые используются во многих языках в Индии). Значения «top», «middle» и «bottom» определяют исключительно геометрическое положение опорной линии шрифта, исходя из размеров «кегельной площадки» шрифта. Методы fillText() и strokeText() принимают четвертый необязательный аргумент. Этот аргумент определяет максимальную ширину отображаемого текста. Если текст окажется шире указанного значения, при заданном значении свойства font будет выполнено его масштабирование или будет использован более узкий или более мелкий шрифт.

Если вам потребуется узнать размеры текста до его вывода, передайте его методу measureText(). Этот метод возвращает объект TextMetrics, определяющий размеры для текущего значения свойства font. На момент написания этих строк объект TextMetrics позволял определить только ширину текста. Определить ширину текстовой строки на экране можно следующим образом:

var width = с.measureText(text).width;

содержание 21.4.10. Отсечение

После определения контура обычно вызывается метод stroke() или fill() (или оба). Можно также вызвать метод clip(), чтобы определить область отсечения. После того как область отсечения будет определена, рисование будет выполняться только в ее пределах. На рис. 21.13 изображен сложный рисунок, полученный с использованием областей отсечения. Вертикальная полоса в середине и текст вдоль нижнего края рисунка были нарисованы до определения области отсечения, а заливка была выполнена после определения треугольной области отсечения.

Рис. 21.13. Рисование контуров выполнено до, а заливка – после определения области отсечения

Изображение на рис. 21.13 было получено с помощью метода polygon() из примера 21.4 и следующего программного кода:

// Определить некоторые графические атрибуты  
c.font = "bold 60pt sans-serif";         // Большой шрифт  
c.lineWidth = 2;                         // Узкие  
c.strokeStyle = "#000";                  // и черные линии  

// Контур прямоугольника и текст  
c.strokeRect(175, 25, 50, 325);          // Вертикальная полоса в середине 
c.strokeText("<canvas>", 15. 330); // strokeText() вместо fillText() 

// Определить сложный контур, внутренняя область которого является внешней. 
polygon(c,3,200,225,200);                // Большой треугольник  
polygon(c,3,200,225,100,0,true);         // Нарисовать маленький треугольник 
// в обратном направлении  

// Превратить этот контур в область отсечения. 
c.clip();  

// Нарисовать контур линиями толщиной 5 пикселов, внутри области отсечения. 
c.lineWidth = 10;   // Половина этой линии толщиной 10 пикселов окажется 
                    // за пределами области отсечения 
c.stroke();

// Залить область контура прямоугольника и текста, попавшую в область отсечения 
c.fillStyle = "Иааа"                   // Светло-серый
с.fillRect(175, 25, 50, 325);          // Залить вертикальную полосу
c.fillStyle = "#888" // Темно-серый
c.fillText("<canvas>", 15, 330); // Залить текст

Важно отметить, что при вызове метода clip() выполняется усечение самого текущего контура, и этот новый усеченный контур становится новой областью отсечения. Это означает, что метод clip() может только сжимать область отсечения и не способен расширять ее. Метода, который позволил бы сбросить область отсечения, не существует, поэтому перед вызовом метода clip() следует вызвать метод save(), чтобы позднее можно было вернуться к неусеченному контуру вызовом метода restore().


содержание 21.4.11. Тени

Объект CanvasRenderingContext2D имеет четыре свойства графических атрибутов, управляющих рисованием теней. Если присвоить этим свойствам соответствующие значения, любые линии, области, текст или изображения будут отбрасывать тени, что создаст эффект расположения этих элементов над поверхностью холста. На рис. 21.14 показано, как выглядят тени, отбрасываемые закрашенным прямоугольником, контуром прямоугольника и закрашенным текстом.

Рис. 21.14. Автоматически сгенерированные тени

Свойство shadowColor определяет цвет тени. Значением по умолчанию является полностью прозрачный черный цвет, и тени остаются невидимыми, если только не присвоить этому свойству значение, соответствующее полупрозрачному или непрозрачному цвету. Этому свойству допускается присваивать только строковые значения цвета: для рисования теней не могут использоваться шаблоны и градиенты. Использование полупрозрачных цветов дает более реалистичное изображение теней из-за просвечивания фона.

Свойства shadowOffsetX и shadowOffsetY определяют смещение тени по осям X и Y. По умолчанию оба свойства имеют значение 0, что соответствует размещению тени непосредственно под рисунком, где она невидима. Если присвоить обоим свойствам положительные значения, тени будут нарисованы правее и ниже рисунка, как если бы источник света, освещающий холст, находился за пределами экрана левее и выше. Чем больше смещение, тем длиннее отбрасываемая тень и тем «выше» над холстом будет казаться нарисованный объект.

Свойство shadowBlur определяет, насколько размытым будет выглядеть край тени. По умолчанию это свойство имеет значение 0, которому соответствую четкие, неразмытые тени. Большие значения производят более сильный эффект размытия, до определенной степени, определяемой реализацией. Значение этого свойства используется как параметр Гауссовой функции размытия и не является размером или длиной в пикселах.

В примере 21.8 представлен программный код, который использовался для получения изображения на рис. 21.14 и демонстрирующий использование всех четырех свойств, управляющих отображением теней.

Пример 21.8. Установка параметров тени

// Определить узкую тень  
с.shadowColor = "rgba(100,100,100,.4)"; // Полупрозрачный серый цвет 
с.shadowOffsetX = с.shadowOffsetY =3;   // Тень смещена вправо вниз 
с.shadowBlur = 5;                       // Размытые границы тени  

// Нарисовать текст в синем прямоугольнике с этими параметрами тени 
c.lineWidth =10;  
c.strokeStyle = "blue";  
c.strokeRect(100, 100, 300, 200);       // Нарисовать прямоугольник 
с.font = "Bold 36pt Helvetica";  
c.fillTextCHello World", 115, 225);     // Нарисовать текст  

// Определить более широкую тень. Большие значения смещений создают эффект более 
// высокого расположения объекта. Обратите внимание, как полупрозрачная тень 
// смешивается с синим контуром прямоугольника.  
c.shadowOffsetX = c.shadowOffsetY = 20;  
с.shadowBlur =10;  
c.fillStyle = "red";      // Нарисовать сплошной красный прямоугольник, 
с.fillRect(50,25,200,65); // располагающийся выше синего прямоугольника
Значения свойств shadowOffsetX и shadowOffsetY всегда определяются в системе координат по умолчанию и не подвержены действию методов rotate() и scale(). Допустим, к примеру, что вы повернули систему координат на 90 градусов, чтобы нарисовать текст по вертикали и затем вернулись к прежней системе координат, чтобы нарисовать текст по горизонтали. Обе текстовые надписи, вертикальная и горизонтальная, будут отбрасывать тень в одном направлении, что обычно соответствует нашим представлениям. Аналогично фигуры, нарисованные с применением различных преобразований, будут иметь тени с одинаковой «высотой».¹

¹ На момент написания этих строк в версии 5 браузера Google Chrome тени были реализованы с ошибкой, и их смещения были подвержены действию преобразований.

содержание 21.4.12. Изображения

Помимо векторной графики (контуры, линии и прочее), прикладной интерфейс объекта Canvas поддерживает также растровые изображения. Метод drawImage() копирует в холст пикселы из исходного изображения (или из прямоугольной области исходного изображения), выполняя операции масштабирования и вращения, если они необходимы.

Метод drawImage() может вызываться с тремя, пятью или девятью аргументами. Во всех случаях в первом аргументе ему передается исходное изображение. Часто в этом аргументе передается элемент <img> или неотображаемый объект Image, созданный с помощью конструктора Image(). Однако точно так же в первом аргументе можно передать другой элемент <canvas> или даже элемент <video>. Если методу drawImage() передать элемент <img> или <video>, который к этому моменту еще не завершил загрузку изображения, он ничего не скопирует. При вызове с тремя аргументами во втором и третьем аргументах методу drawImage() передаются координаты X и Y верхнего левого угла области, в которую должно быть скопировано изображение. В этом случае в холст будет скопировано изображение целиком. Координаты X и Y будут интерпретироваться как координаты в текущей системе координат, поэтому при необходимости изображение будет масштабировано или повернуто.

При вызове метода drawImage() с пятью аргументами к аргументам с координатами X и Y, описанным выше, добавляются ширина и высота. Эти четыре аргумента определяют прямоугольную область внутри холста. Верхний левый угол исходного изображения будет помещен в точку (x,y), а правый нижний – в точку (x+width, y+height). Опять же в холст будет скопировано изображение целиком. Прямоугольная область назначения определяется в текущей системе координат. Эта версия метода выполнит масштабирование изображение, чтобы уместить его в отведенную область, даже если к исходной системе координат не применялось преобразование масштабирования.

При вызове метода drawImage() с девятью аргументами ему передаются координаты и размеры области в исходном изображении и области в холсте, и он скопирует только указанную область исходного изображения. В аргументах со второго по пятый указываются координаты и размеры прямоугольной области в исходном изображении. Они измеряются в пикселах CSS. Если исходное изображение представлено другим элементом <canvas>, координаты и размеры исходного изображения будут измеряться в системе координат по умолчанию и никакие преобразования, применявшиеся к системе координат исходного холста, учитываться не будут. В аргументах с шестого по девятый указываются координаты и размеры области в текущей (а не по умолчанию) системе координат, куда будет скопирован указанный фрагмент изображения.

Пример 21.9 демонстрирует простой случай применения метода drawImage(). В нем используется версия метода с девятью аргументами, чтобы скопировать фрагмент холста, увеличенный и повернутый, обратно в тот же самый холст. Как видно на рис. 21.15, изображение было увеличено достаточно, чтобы проявилась его растровая структура и можно было наблюдать полупрозрачные пикселы, которые использованы для сглаживания краев линии.

Рис. 21.15. Копия изображения была увеличена методом drawlmage()

Пример 21.9. Использование метода drawlmage()

						
// Нарисовать линию в верхнем левом углу
c.moveTo(5,5);
c.lineTo(45,45);
c.lineWidth = 8;
c.lineCap = "round";
c.stroke();

// Определить преобразование системы координат 
c.translate(50,100);       
c.rotate(-45*Math.PI/180); // Разгладить линию
c.scale(10,10);            // Увеличить ее, чтобы были видны отдельные пикселы

// С помощью draw image скопировать линию
c.drawImage(c.canvas,
            0, 0, 50, 50,  // исходная область: непреобразованная
            0, 0, 50, 50); // область назначения: преобразованная						

Помимо возможности копировать изображение в холст, имеется также возможность извлекать содержимое холста в виде изображения с помощью метода toDataURL(). В отличие от других методов, описанных выше, метод toDataURL() – это метод самого элемента Canvas, а не объекта CanvasRenderingContext2D. Обычно метод toDataURL() вызывается без аргументов и возвращает содержимое холста как PNG- изображение, закодированное в виде строки в формате URL data:. Возвращаемая строка URL подходит для использования в элементе <img>, благодаря чему можно создать статический снимок холста, как показано ниже:

var img = document.createElement("img"); // Создать элемент <img> 
img.src = canvas.toDataURL();            // Установить его атрибут src  
document.body.appendChild(img);          // Добавить элемент в документ 

Все браузеры в обязательном порядке поддерживают формат PNG изображений. Некоторые реализации могут также поддерживать другие форматы, и вы можете указать желаемый MIME-тип в необязательном первом аргументе в вызове метода toDataURL(). Подробности смотрите в справочном разделе книги. Существует одно важное ограничение, связанное с безопасностью, о котором следует знать, планируя применять метод toDataURL(). Чтобы предотвратить утечку информации между доменами, метод toDataURL() не работает с элементами <canvas>, имеющими «неясное происхождение». Считается, что холст имеет неясное происхождение, если в него вставлялось изображение (непосредственно, вызовом метода drawImage(), или косвенно, с помощью метода CanvasPattern), имеющее происхождение, отличное от происхождения документа, содержащего элемент <canvas>.

содержание 21.4.13. Композиция

При рисовании линий и текста, заливке областей или копировании изображений может получиться так, что новые пикселы будут накладываться сверху на уже существующие в холсте. При рисовании непрозрачных пикселов они просто будут замещать уже имеющиеся пикселы. Но при рисовании полупрозрачных пикселов новые («исходные») пикселы будут объединяться (комбинироваться) со старыми («целевыми») пикселами так, что старые пикселы будут видны сквозь новые, с учетом степени прозрачности этих пикселов.

Этот процесс объединения новых полупрозрачных исходных пикселов с существующими целевыми пикселами называется композицией, а процесс композиции, описанный выше, используется по умолчанию при объединении пикселов. Однако композиция нужна не всегда. Представьте, что вы нарисовали в холсте рисунок, использовав полупрозрачный цвет, и теперь хотите внести в холст временные изменения, а позднее восстановить оригинальный рисунок. Самый простой способ реализовать это состоит в том, чтобы холст (или его область) скопировать в другой, неотображаемый холст с помощью метода drawlmage(). А затем, когда придет время восстановить холст, можно скопировать обратно в него фрагмент, который был сохранен в неотображаемом холсте. Напомню, что скопированные пикселы были полупрозрачными. Если режим композиции будет действовать, они не затрут полностью временные изменения. В подобных ситуациях необходимо иметь способ отключать композицию: чтобы вставлять исходные пикселы, игнорируя целевые, независимо от степени прозрачности исходных.

Установить тип композиции можно с помощью свойства globalCompositeOperation. По умолчанию оно имеет значение «source-over», в соответствии с которым исходные пикселы накладываются «поверх» («over») целевых пикселов и объединяются с ними, если исходные пикселы являются полупрозрачными. Если присвоить этому свойству значение «сору», композиция будет отключена: исходные пикселы будут скопированы в холст без изменений и затрут целевые пикселы. Иногда может оказаться полезным еще одно значение свойства globalCompositeOperation – «destination-over». При использовании этого вида композиции пикселы объединяются так, как будто исходные пикселы добавляются под существующие целевые пикселы. Если целевые пикселы будут иметь прозрачный или полупрозрачный цвет, некоторые или все исходные пикселы будут видны сквозь этот цвет.


«source-over», «destination-over» и «сору» – это три наиболее часто используемых вида композиции, однако прикладной интерфейс объекта Canvas поддерживает 11 значений для атрибута globalCompositeOperation. Названия этих видов композиции достаточно красноречиво объясняют, что они призваны делать, и вам может потребоваться немало времени, чтобы разобраться в особенностях разных видов композиции, подставляя их названия в примеры, демонстрирующие их действие.

На рис. 21.16 показаны все 11 видов композиции пикселов с «предельными» значениями прозрачности: все пикселы являются полностью непрозрачными или полностью прозрачными. В каждом из 11 прямоугольников сначала рисовался квадрат, который представляет целевые пикселы. Затем устанавливалось свойство globalCompositeOperation и рисовался круг, представляющий исходные пикселы.

Рис. 21.16. Виды композиции пикселов с предельными значениями прозрачности

На рис. 21.17 изображен похожий пример, в котором использовались пикселы с «промежуточными» значениями прозрачности. В этой версии исходный круг и целевой квадрат были залиты градиентами, вследствие чего в рисунке присутствуют пикселы с различной степенью прозрачности.

Вы можете обнаружить, что понять действие того или иного вида композиции совсем непросто, когда в операцию вовлечены полупрозрачные пикселы, как в данном примере. Если у вас есть желание более глубоко разобраться с композицией, в справочной статье CanvasRenderingContext2D вы найдете уравнения вычисления конечного значения цвета пиксела, исходя из значений цвета исходного и целевого пикселов для всех 11 видов композиции.

Рис. 21.17. Виды композиции пикселов с промежуточными значениями прозрачности

На момент написания этих строк производители браузеров не смогли прийти к единству в реализации 5 из 11 видов композиции: «copy», «source-in», «source-out», «destination-atop» и «destination-in» действуют по-разному в разных браузерах и не могут использоваться переносимым образом. Далее приводится подробное описание этих различий, но если вы не планируете использовать эти виды композиций, то можете сразу перейти к следующему разделу.

При использовании пяти видов композиции, перечисленных выше, цвет целевых пикселов либо игнорируется при вычислении результатов, либо получающиеся пикселы делаются прозрачными, если прозрачными являются исходные пикселы. Различия в реализациях связаны с определением исходных пикселов. Браузеры Safari и Chrome выполняют композицию «локально»: в расчетах участвуют только фактические исходные пикселы, которые выводятся методами fill(), stroke() и другими. IE9, вероятно, последует этому примеру. Браузеры Firefox и Opera выполняют композицию «глобально»: в расчетах участвуют все пикселы в текущей области отсечения при выполнении любой операции рисования. Если для данного целевого пиксела отсутствует исходный пиксел, считается, что исходный пиксел имеет черный прозрачный цвет. В Firefox и Opera это означает, что пять видов композиции, перечисленные выше, фактически стирают целевые пикселы внутри области отсечения там, где отсутствуют исходные пикселы. Изображения на рис. 21.16 и рис. 21.17 были получены в браузере Firefox. Это объясняет, почему рамки, окружающие примеры применения видов композиции «copy», «source-in», «source-out», «destination-atop» и «destination-in», тоньше, чем рамки вокруг других примеров: прямоугольник, окружающий каждый пример, является областью отсечения, и применение этих пяти видов композиции приводит к стиранию части рамки (половина lineWidth), попадающей внутрь контура. Для сравнения на рис. 21.18 показаны те же результаты, что и на рис. 21.17, но полученные в браузере Chrome.

Рис. 21.18. Локальная композиция

На момент написания этих строк проект стандарта HTML5 утвердил глобальный подход к композиции, реализованный в браузерах Firefox и Opera. Производители браузеров знают об имеющейся несовместимости и не удовлетворены текущим состоянием спецификации. Велика вероятность, что спецификация будет пересмотрена в пользу локальной композиции.

Наконец, обратите внимание, что в браузерах, таких как Safari и Chrome, реализующих локальную композицию, существует возможность реализовать глобальную композицию. Сначала нужно создать пустой неотображаемый холст с теми же размерами, что и отображаемый. Затем нарисовать исходное изображение на неотображаемом холсте и вызвать метод drawlmage(), чтобы скопировать неотображаемый рисунок в отображаемый холст и применить глобальную композицию в пределах области отсечения. В браузерах, таких как Firefox, реализующих глобальную композицию, нет универсального приема выполнения локальной композиции, но нередко можно получить достаточно близкий эффект, определив соответствующую область отсечения перед выполнением операции рисования, для которой композиция должна выполняться локально.


содержание 21.4.14. Манипулирование пикселами

Метод getImageData() возвращает объект ImageData, представляющий массив пикселов (в виде компонентов R, G, В и А) из прямоугольной области холста. Создать пустой объект ImageData можно с помощью метода createImageData(). Пикселы в объекте ImageData доступны для записи, благодаря чему их можно изменить как угодно и затем скопировать обратно в холст вызовом метода putImageData().

Эти методы манипулирования пикселами предоставляют низкоуровневый доступ к холсту. Координаты и размеры области, передаваемой методу getImageData(), задаются в системе координат по умолчанию: ее размеры измеряются в CSS-пикселах и без учета текущего преобразования. При вызове метода putImageData() координаты также задаются в системе координат по умолчанию. Кроме того, метод putImageData() игнорирует все графические атрибуты. Он не использует механизм композиции, не умножает пикселы на значение свойства globalAlpha и не рисует тени.

Методы манипулирования пикселами могут пригодиться для реализации обработки изображений. Пример 21.10 демонстрирует, как создать простейший эффект размытия или «смазывания» быстро движущегося объекта в элементе <canvas>. Пример демонстрирует применение методов getImageData() и putImageData() и показывает, как выполнять итерации по пикселам в объекте ImageData и изменять их значения, но без подробного описания. Полная информация о методах getImageData() и putImageData() приводится в справочной статье CanvasRenderingContext2D, а подробное описание объекта ImageData – в его собственной справочной статье.

Пример 21.10. Создание эффекта размытия быстродвижущегося объекта с помощью объекта ImageData


// "Смазать" пикселы прямоугольной области вправо, чтобы воспроизвести эффект 
// быстрого движения объекта справа налево. Значение п должно быть равно или 
// больше 2. Чем больше значение, тем сильнее эффект смазывания. Координаты 
// и размеры прямоугольной области задаются в системе координат по умолчанию.
function smear(c, n, х, у, w, h) { 
  // Получить объект ImageData, представляющий пикселы области эффекта 
  var pixels = c.getImageData(x,y,w,h); 


  // Смазывание выполняется на месте, и потому требуется получить только исходный
  // объект ImageData. Некоторые алгоритмы обработки изображений требуют 
  // использования дополнительного объекта ImageData для сохранения 
  // трансформированных значений пикселов. Если бы потребовался промежуточный
  // буфер вывода, можно было бы создать новый объект ImageData 
  // с теми же размерами следующим способом: 
  // var output_pixels = c.createImageData(pixels); 


  // Эти размеры могут отличаться от значений аргументов w и h: на каждый 
  // CSS-пиксел может приходиться несколько аппаратных пикселов, 
  var width = pixels.width, height = pixels.height; 


  // Это массив байтов, хранящий информацию о пикселах, слева направо и сверху
  // вниз. Для каждого пиксела отводится 4 последовательных байта,
  // в порядке R.G.B.A. 
  var data = pixels.data; 


  // Смазать каждый пиксел после первого в каждой строке, заменив его суммой 
  // 1/n-й доли его собственного значения и m/n-й доли
  // значения предыдущего пиксела
  var m = n-1; 


  for(var row = 0; row < height; row++) { // Для каждой строки
    var i = row*width*4 +4;               // Индекс второго пиксела в строке
    for(var col = 1; col < width; col++, i += 4) { // Для каждого столбца 
      data[i] = (data[i] + data[i-4]*m)/n;         // Красная составляющая 
      data[i+1] = (data[i+1] + data[i-3]*m)/n;     // Зеленая
      data[i+2] = (data[i+2] + data[i-2]*m)/n;     // Синяя
      data[i+3] = (data[i+3] + data[i-1]*m)/n;     // Альфа-составляющая
    }
  } 

  // Скопировать смазанное изображение обратно в ту же позицию в холсте 
  c.putImageData(pixels, x, у);
}

Обратите внимание, что на метод getImageData() накладываются те же ограничения политики общего происхождения, что и на метод toDataURL(): он не будет работать с холстами, в которые вставлялись изображения (непосредственно, вызовом метода drawlmage(), или косвенно, с помощью метода CanvasPattern), имеющие происхождение, отличное от происхождения документа, содержащего элемент <canvas>.

содержание 21.4.15. Определение попадания

Метод isPointInPath() позволяет узнать, находится ли указанная точка внутри (или на границе) текущего контура, и возвращает true, если это так, и false – в противном случае. Метод принимает координаты точки в не преобразованной системе координат по умолчанию. Это позволяет использовать метод для определения попадания: определения принадлежности точки, где был выполнен щелчок мышью, некоторой определенной фигуре.

Однако значения свойств clientX и clientY объекта MouseEvent нельзя передать непосредственно методу isPointlnPath(). Во-первых, координаты события мыши следует преобразовать из координат объекта Window в относительные координаты элемента <canvas>. Во-вторых, если экранные размеры холста отличаются от его фактических размеров, координаты мыши необходимо перевести в соответствующий масштаб. В примере 21.11 показана вспомогательная функция, используемая для определения попадания точки события MouseEvent в текущий контур.


Пример 21.11. Проверка попадания точки события мыши в текущий контур


// Возвращает true, если указанное событие мыши возникло в текущем контуре
// в указанном объекте CanvasRenderingContext2D.
function hitpath(context, event) {
    // Получить элемент <canvas> из объекта контекста
    var canvas = context.canvas;              

    // Получить координаты и размеры холста
    var bb = canvas.getBoundingClientRect();  
    
    // Преобразовать и масштабировать координаты события мыши
    // в координаты холста
    var x = (event.clientX-bb.left)*(canvas.width/bb.width);
    var y = (event.clientY-bb.top)*(canvas.height/bb.height);

    // Вызвать isPointlnPath с преобразованными координатами
    return context.isPointInPath(x,y);
}

Эту функцию hitpath() можно использовать в обработчиках событий, как показано ниже:

canvas.onclick = function(event) {
  if (hitpath(this.getContext("2d"), event) {
    alert("Есть попадание!"); // Щелчок в пределах текущего контура
  }
};

Вместо проверки попадания в текущий контур с помощью метода getImageData() можно определить, окрашен ли пиксел в точке события мыши. Если пиксел (или пикселы) под указателем мыши оказался полностью прозрачным, следовательно, под указателем мыши ничего не было нарисовано и щелчок был выполнен за пределами какой-либо фигуры. Пример 21.12 демонстрирует, как реализовать подобную проверку попадания.

Пример 21.12. Проверка наличия окрашенного пиксела в точке события мыши


// Возвращает true, если указанное событие мыши возникло в точке,
 // где находится непрозрачный пиксел.
function hitpaint(context, event) {
  // Преобразовать и масштабировать координаты события мыши
  //	в координаты холста
  var canvas = context.canvas;       
  var bb = canvas.getBoundingClientRect(); 
  var x = (event.clientX-bb.left)*(canvas.width/bb.width);
  var y = (event.clientY-bb.top)*(canvas.height/bb.height);

  // Получить пиксел (или пикселы, если одному CSS-пикселу соответствует
	// несколько аппаратных пикселов)
  var pixels = c.getImageData(x,y,1,1);
  
  // Если хотя бы один пиксел имеет ненулевое значение альфа-канала,
	// вернуть true (попадание)
  for(var i = 3; i < pixels.data.length; i+=4) {
    if (pixels.data[i] !== 0) return true;
  }
  
  // Otherwise it was a miss.
  return false;
}

содержание 21.4.16. Пример использования элемента <canvas>: внутристрочные диаграммы

Закончим главу практическим примером рисования внутристрочных диаграмм. Внутристрочная диаграмма (sparkline) – это маленькое изображение (обычно некий график) предназначенное для отображения в потоке текста, например: Server load: . 8. Термин «sparkline» был введен их автором Эдвардом Тафти (Edward Tufte), который описывает внутристрочные диаграммы так: «Маленькие графические изображения с высоким разрешением, встроенные в контекст окружающих их слов, чисел, изображений. Внутристрочная диаграмма – это простой в создании, тесно связанный с данными график, размер которого сопоставим с размером слова». (Подробнее о создании внутристрочных диаграмм см. в книге Эдварда Тафти «Beautiful Evidence» [Graphics Press].) В примере 21.13 приводится относительно простой модуль на языке JavaScript, позволяющий вставлять в веб-страницы внутристрочные диаграммы. Порядок работы модуля описывается в комментариях. Обратите внимание, что в нем используется функция onLoad() из примера 13.5.

Пример 21.13. Реализация внутристрочных диаграмм с помощью элемента <canvas>: внутристрочные диаграммы

/*
 * Отыскивает все элементы с CSS-классом "sparkline", анализирует их содержимое
 * как последовательность чисел и замещает их графическим представлением.
 *  
 * Определить внутристрочную диаграмму в разметке можно так:  
 *   <span class="sparkline">3 5 7 6 6 9 11 15</span>
 *
 * Определить визуальное оформление диаграмм средствами CSS можно так: 
 *   .sparkline { background-color: #ddd; color: red; }
 *  
 * - Цвет кривой графика определяется CSS-свойством color вычисленного стиля.
 * - Сами диаграммы являются прозрачными, поэтому
 *   сквозь них просвечивает фон страницы.
 * - Высота диаграммы определяется атрибутом data-height, если он указан,
 *   или свойством font-size вычисленного стиля в противном случае.
 * - Ширина диаграммы определяется атрибутом data-width, если он указан,
 *   или числом точек данных, умноженным на значение атрибута data-dx, если он
 *   указан, или числом точек данных, умноженным на высоту, деленную на 6
 * - Минимальное и максимальное значение по оси у извлекаются из атрибутов
 *   data-ymin и data-ymax, если они указаны, иначе отыскиваются минимальное
 *   и максимальное значение в данных.  
 */
onLoad(function() {   // Когда документ будет загружен  
  //Отыскать все элементы с классом "sparkline" 
  var elts = document.getElementsByClassName("sparkline");
  main: for(var e = 0; e < elts.length; e++) { // Для каждого элемента
    var elt = elts[e];

    // Получить содержимое элемента и преобразовать его в массив чисел.
    // Если преобразование не удалось, пропустить этот элемент. 
    var content = elt.textContent || elt.innerText;  // Содержимое
    var content = content.replace(/^\s+|\s+$/g, ""); // Удалить пробелы
    var text = content.replace(/#.*$/gm, "");        // Удалить комментарии
    text = text.replace(/[\n\r\t\v\f]/g, " ");       // Преобр. \п и др. в пробел
    var data = text.split(/\s+|\s*,\s*/);            // По пробелам и запятым 
    for(var i = 0; i < data.length; i++) {        // Каждый фрагмент 
      data[i] = Number(data[i]);                     // Преобразовать в число 
      if (isNaN(data[i])) continue main;             // Прервать при неудаче 
    }

    // Определить цвет, ширину, высоту и границы по оси у для диаграммы
    // из данных, из атрибутов data- элемента и из вычисленного стиля.
    //
    var style = getComputedStyle(elt, null); 
    var color = style.color;
    var height = parseInt(elt.getAttribute("data-height")) ||
      parseInt(style.fontSize) || 20;
    var width = parseInt(elt.getAttribute("data-width")) ||
      data.length * (parseInt(elt.getAttribute("data-dx")) || height/6);
    var ymin = parseInt(elt.getAttribute("data-ymin")) ||
      Math.min.apply(Math, data);
    var ymax = parseInt(elt.getAttribute("data-ymax")) ||
      Math.max.apply(Math, data);
    if (ymin >= ymax) ymax = ymin + 1;

    // Создать элемент <canvas>. 
    var canvas = document.createElement("canvas"); 
    canvas.width = width;    // Установить размеры холста 
    canvas.height = height;
    canvas.title = content;  // Содержимое использовать как подсказку
    elt.innerHTML = "";      // Стереть содержимое элемента 
    elt.appendChild(canvas); // Вставить холст в элемент 

    // Нарисовать график по точкам (i,data[i]), преобразовав в координаты холста.
    var context = canvas.getContext(′2d′);
    for(var i = 0; i < data.length; i++) {       // Для каждой точки на графике
      var x = width*i/data.length;               // Масштабировать i 
      var y = (ymax-data[i])*height/(ymax-ymin); // и data[i] 
      context.lineTo(x,y);       // Первый вызов lineTo() выполнит moveTo() 
    }
    context.strokeStyle = color; // Указать цвет кривой на диаграмме
    context.stroke();            // и нарисовать ее 
  }
});