Идеи реализации

Пример использования будущего класса Turtle завершился созданием графического файла в формате PNG. Значит ли это, что при программировании класса, способного создавать такие файлы, нам следует изучить этот формат? Конечно, нет. Такая задача была бы слишком сложной.

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

Одна из возможностей — обратиться к знакомому нам графическому формату SVG. Например, изображение

в формате SVG выглядит так:

<?xml version="1.0" encoding="UTF-8"?>
<svg
	xmlns="http://www.w3.org/2000/svg"
	version="1.1"
	viewBox="-1 -1 26 26"
	width="200"
	height="200"
	>
	<path
		d="M 0 24 L 24 0 L 0 0 L 24 24"
		fill="none"
		stroke="tomato"
		stroke-width="2"
		stroke-linejoin="round"
		stroke-linecap="round"
		/>
</svg>

В этом примере трёхзвенная ломаная нарисована одним росчерком при помощи единственного элемента path. Значение M 0 24 L 24 0 L 0 0 L 24 24 атрибута d в этом элементе следует понимать так: «встать в точку 0 24 ; прочертить отрезок в точку 24 0 ; прочертить отрезок в точку 24 24 ». Этот компактный способ представления линий (не только ломаных, но и гладких) будет нам не очень удобен: он годится для рисования только одним цветом и только одной толщины. При изменении этих величин потребуется закрыть текущий элемент path и начать следующий, с другими значениями атрибутов stroke (цвет рисования) или stroke-width (толщина линии). Атрибуты stroke-linejoin и stroke-linecap отвечают соответственно за форму соединений отрезков в точках излома и за форму концов линий. Атрибут fill — это цвет заливки замкнутого контура, а его значение none обозначает отсутствие заливки (этот контур не замкнут, и, если бы мы и пожелали его закрасить, столкнулись бы с определёнными сложностями).

Чтобы не слишком усложнять нашу задачу, будем рисовать отрезки по отдельности. Хотя это можно сделать при помощи всё тех же элементов path, в SVG имеется специальный элемент line. Его атрибуты x1, y1, x2, y2 задают координаты начала и конца отрезка. Можно снабжать каждый из элементов line атрибутами stroke, fill, stroke-width, stroke-linejoin, stroke-linecap. А можно собирать отрезки с одинаковыми значениями этих атрибутов в группу (то есть заключать их внутрь элементов g, и, чтобы не повторяться, добавлять общие атрибуты к элементу g). Приводим пример компромиссного решения, где все свойства отрезков, за исключением stroke, общие:

<?xml version="1.0" encoding="UTF-8"?>
<svg
	xmlns="http://www.w3.org/2000/svg"
	version="1.1"
	viewBox="-1 -1 26 26"
	width="200"
	height="200"
	>
	<g
		stroke-width="2"
		stroke-linejoin="round"
		stroke-linecap="round"
		>
		<line x1="0" y1="24" x2="24" y2="0" stroke="red"/>
		<line x1="24" y1="0" x2="0" y2="0" stroke="green"/>
		<line x1="0" y1="0" x2="24" y2="24" stroke="blue"/>
	</g>
</svg>

Без группировки SVG-файл со сложным рисунком становится весьма объёмным. Группировка способна значительно сократить размер документа, но при этом сильно усложнит алгоритм.

Часто изображения в формате SVG создаются в векторных графических редакторах. Наиболее популярные из них — CorelDraw и InkScape. Скриншот программы InkScape можно увидеть на рисунке 43.2. «Скриншот программы InkScape».


Программу InkScape можно использовать и для преобразования в другие форматы: векторные форматы PDF, PostScript или растровый PNG. После выполнения команд

картинка из файла Picture.svg будет преобразована в картинки Picture.pdf, Picture.ps, Picture.png в соответствующих форматах.

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

Мы откажемся от использования SVG. Во-первых, из-за его громоздкости, а во-вторых, для того, чтобы познакомиться с другими возможностями.

Одной из них является формат PostScript, который по совместительству является весьма мощным, красивым и интересным языком программирования. Исполнителями программ на языке PostScript являются так называемые графические устройства — принтеры или графические окна на экране. В качестве устройств также могут выступать графические файлы в разнообразных форматах, как растровых, так и векторных. Результатом исполнения программ на языке PostScript является изображение на устройстве (на бумаге, если используется принтер, в окне, в файле). Посредником между программой на PostScript и устройством является специальная программа — интерпретатор. Программа содержит набор модулей — драйверов, каждый из которых может обслуживать устройства определённого типа.

Самая известная из программ-интерпретаторов языка PostScript — GhostScript. Она входит в дистрибутивы большинства Linux-систем и запускается командой gs. Не вдаваясь пока в подробности, скажем лишь, что, запустив эту программу с нужными параметрами, можно преобразовать PostScript-документ во многие другие форматы. Список драйверов, поддерживаемых программой gs, можно увидеть, запустив её с ключом -h (после слов «Available devices»):

% gs -h

Среди них есть графическое окно (x11alpha), PNG-файл (pngalpha), PDF-файл (pdfwrite). Есть устройства для создания файлов с управляющими командами для принтеров различных моделей.

Разберём программу на языке PostScript, рисующую одноцветную четырёхзвенную ломаную:

%!PS-Adobe-3.0

2 setlinewidth
1 setlinecap
1 setlinejoin
1 0.388235294117647 0.27843137254902 setrgbcolor
newpath 0 0 moveto 24 24 lineto 0 24 lineto 24 0 lineto stroke
showpage

Наверное, читатель уже догадался, что знак процента обозначает комментарий, простирающийся до конца строки. Команда 2 setlinewidth устанавливает толщину линий в 2 единицы (одна единица — большой типографский пункт, 1 72 дюйма, примерно 0,35 мм). Команды 1 setlinecap и 1 setlinejoin устанавливают соответственно форму концов линий и углов — в обоих случаях закруглённую. Команда 1 0.388235294117647 0.27843137254902 setrgbcolor устанавливает цвет рисования; три числа, предшествующие setrgbcolor — компоненты цвета в цветовой схеме RGB. Команда newpath начинает построение новой кривой или ломаной (пути в терминологии PostScript). 0 0 moveto смещает воображаемый карандаш в точку с координатами 0 0 , 24 24 lineto чертит отрезок в точку 24 24 … Обращаем внимание, что в отличие от SVG координатные оси направлены привычным образом: ось ординат смотрит вверх. Команда stroke завершает построение пути и наносит на рисунок линию ранее заданного цвета, толщины, и с заданными формами концов и углов. Наконец, команда showpage переносит рисунок на заданный носитель (на лист бумаги в принтере, в графическое окно, в графический файл).

Для рисования разноцветной ломаной придётся указать три команды newpathstroke, предваряя каждую из них командой setrgbcolor:

%!PS-Adobe-3.0

2 setlinewidth
1 setlinecap
1 setlinejoin
1 0 0 setrgbcolor
newpath 0 0 moveto 24 24 lineto stroke
0 .5 0 setrgbcolor
newpath 24 24 moveto 0 24 lineto stroke
0 0 1 setrgbcolor
newpath 0 24 moveto 24 0 lineto stroke
showpage

В PostScript имеется несколько сотен встроенных команд. Кроме команд, посвящённых рисованию, есть и математические, и логические, и строковые операторы. Имеются массивы, ассоциативные массивы (словари), строки и операторы для манипуляций с ними. Есть также операторы, играющие роль условных конструкций и циклов. Имеется возможность определять свои операторы. Всё это делает PostScript полноценным и интересным алгоритмическим языком.

Особенность языка PostScript, которая прежде всего бросается в глаза, состоит в том, что параметры команд предшествуют самим командам. Это обстоятельство отражает тот способ, с помощью которого параметры передаются командам, а те, в свою очередь, возвращают результат своей работы. Все входные и выходные значения размещаются в так называемом стеке операндов. Например, оператор setrgbcolor, устанавливающий цвет рисования, рассчитывает, что наверху стека операндов находятся три числа. Этот оператор снимает числа со стека и устанавливает соответствующий цвет. Другой пример: оператор add снимает со стека два числа и вместо них кладёт на стек их сумму. Константы, встречающиеся в программе, просто добавляются в стек в порядке их появления, и ждут там своего часа, пока не будут как-то использованы операторами.

Чтобы пояснить эту мысль, рассмотрим программу, которая вычисляет пифагорову сумму двух чисел a = 3 и b = 4 , то есть a 2 + b 2 = 5 . В приведённой программе каждая команда занимает отдельную строку. Расположенные справа от команд зелёные аннотации показывают состояние стека операндов после выполнения очередной команды:

3		► 3
4		► 3 4
dup		► 3 4 4
mul		► 3 16
exch	► 16 3
dup		► 16 3 3
mul		► 16 9
add		► 25
sqrt	► 5

Встроенная команда dup добавляет в стек копию вершины стека, mul снимает со стека два верхних числа и кладёт их произведение, exch меняет местами два верхних элемента стека, add мы уже упоминали. И, наконец, sqrt снимает число и добавляет квадратный корень из него.

Конечно, такое программирование очень непривычно. Даже зная назначение упомянутых команд, довольно трудно сообразить, что фрагмент программного кода

dup mul exch dup mul add sqrt

снимает со стека два числа и кладёт обратно их пифагорову сумму. Если в программе требуется многократно вычислять пифагоровы суммы, есть резон определить оператор (назовём его hypot в честь гипотенузы), который решает эту задачу:

/hypot { dup mul exch dup mul add sqrt } def

Оператор def снимает со стека два объекта — имя будущего оператора /hypot и процедуру, которую следует связать с этим именем. Процедура — это произвольная последовательность команд, заключённая в фигурные скобки. В дальнейшем можно пользоваться оператором hypot, как если бы он был встроенным:

5 12 hypot	► 13

Полезным для нас применением этой возможности будет определение команды (назовём её seg), которая снимает со стека четыре числа — координаты начала и конца отрезка, и рисует отрезок. Вот её определение:

/seg { 4 2 roll newpath moveto lineto stroke } def

Выглядит таинственно. Что там за четвёрка, двойка, что означает roll? И почему командам moveto и lineto не предшествуют пары чисел?

Разберёмся по порядку. Команда roll снимает со стека два числа (в нашем примере 4 и 2). Затем берёт четыре верхних оставшихся на стеке элемента и сдвигает их по кругу вправо два раза. Таким образом код 4 2 roll меняет местами верхнюю пару элементов стека операндов, и расположенную под ней, сохраняя порядок элементов в каждой паре. Между прочим, если бы в нашем распоряжении не было встроенной команды exch, то её можно было бы определить через roll:

/exch { 2 1 roll } def

Предположим, нам нужно начертить линию из точки с координатами 111 222 в точку 333 444 . Проследим, как будет работать команда

111 222 333 444 seg

111 222 333 444	► 111 222 333 444
4 2 roll		► 333 444 111 222
newpath			► 333 444 111 222
moveto			► 333 444
lineto			
stroke			

Всё верно: перед вызовом команды moveto наверху в стеке находятся числа 111 и 222, а перед вызовом lineto — 333 и 444. Команды newpath и stroke стеком операндов не пользуются.

С учётом определения оператора seg наш пример с разноцветной табуреткой будет выглядеть так:

%!PS-Adobe-3.0

/seg { 4 2 roll newpath moveto lineto stroke } def
2 setlinewidth
1 setlinecap
1 setlinejoin
1 0 0 setrgbcolor
0 0 24 24 seg
0 .5 0 setrgbcolor
24 24 0 24 seg
0 0 1 setrgbcolor
0 24 24 0 seg
showpage

Экономия будет особенно заметной там, где черепахе придётся рисовать десятки тысяч отрезков. Эта экономия проявится при записи программы на диск и считывании её для интерпретации. Каждый раз, когда в процессе интерпретации встретится оператор seg, будет производиться выполнение его тела (процедуры, заданной в определении). При этом для каждого встроенного оператора newpath, roll, moveto, lineto, stroke, которые присутствуют в теле процедуры, будет осуществляться поиск встроенных процедур, реализующих эти операторы. Это явно не идёт на пользу быстродействию программы на PostScript. Избежать этих одинаковых многократно выполняемых действий поможет команда bind:

PostScript
/seg { newpath 4 2 roll moveto lineto stroke } bind def

Команда bind снимает со стека процедуру и заменяет её откомпилированной версией. Это значит, что для всех встречающихся в процедуре имён встроенных операторов происходит замена этих имён на сами операторы. Это делается однократно на момент вызова bind. И имя /seg связывается уже с откомпилированной версией, которая будет работать существенно быстрей.

Мы не ставили перед собой задачу изучить язык PostScript, однако обсудили все его возможности, которые нам потребуются. Интересующихся читателей мы отсылаем к полному описанию языка, любезно предлагаемому корпорацией Adobe — изобретателем языка PostScript. Приложения PostScript к созданию математических иллюстраций подробно обсуждаются в прекрасном руководстве Билла Кассельмана.

Объект класса Turtle обладает набором свойств. Некоторые из них могут запоминаться по команде SAVE и восстанавливаться по команде RESTORE. Все изменения таких свойств, проделанные после последнего SAVE, восстанавливаются при выполнении RESTORE. Будем называть свойства, на которые влияют эти две команды, сохраняемыми.

Для хранения набора свойств будет очень удобен ассоциативный массив. В нём ключами будут названия свойств, а храниться там будут соответствующие значения. В числе сохраняемых свойств будут x и y (координаты черепахи), direction (её направление), linewidth (толщина линии) и color (цвет). Набор сохраняемых свойств будем называть состоянием черепахи.

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

Объектная модель рисующей черепахи будет включать в себя следующие свойства:

savestack

Стек запомненных состояний — ссылка на массив, заполненный ссылками на ассоциативные массивы — фреймы запомненных свойств. В начале работы стек содержит единственный фрейм, в котором свойства x и y равны нулю (черепаха в начале координат), direction тоже ноль (черепаха повёрнута вправо), свойство color содержит чёрный цвет, scale равно единице (единичный масштаб).

bbox

Свойство, описывающее «габаритный» прямоугольник рисунка (Bounding BOX). Это будет ссылка на ассоциативный массив с ключами llx, lly, urx, ury. Соответствующие значения — координаты левого нижнего (Lower Left) и верхнего правого (Upper Right) углов прямоугольника. В начале работы прямоугольник вырожден в точку, так что все эти координаты нулевые, однако каждый нарисованный отрезок может расширить этот прямоугольник, если необходимо, отодвигая левый нижний угол влево и/или вниз, а верхний правый — вправо и/или вверх. При этом учитывается текущая толщина линии: достаточно расширить прямоугольник вместил два кружочка с центрами в концах рисуемого отрезка и с диаметрами, равными толщине линии.

tmpfile

Значение этого свойства — временный файл, открытый для записи. Файл будет автоматически удалён по завершению программы.

Теперь перечислим методы класса.

new()

Конструктор без параметров. Создаёт новый объект Turtle.

save()
restore()

Запомнить/восстановить состояние черепахи.

getProperty($name)
setProperty($name, $value)

Обобщённые геттер и сеттер для получения значения свойства с именем $name и для присваивания значения $value этому свойству. Будут использоваться специализированными геттерами и сеттерами, перечисленными ниже.

getXY()
setXY($x, $y)

Геттер возвращает в массиве пару координат черепахи, а сеттер устанавливает значения координат $x и $y.

getDirection()
setDirection($direction)

Возвращает и устанавливает курсовое направление черепахи — угол в градусах, отсчитываемый от правого направления против часовой стрелки.

getScale()
setScale($scale)

Возвращает и устанавливает коэффициент масштаба.

getLineWidth()
setLineWidth($lineWidth)

Возвращает и устанавливает толщину линии.

getColor()
setColor($color)

Возвращает и устанавливает цвет рисования $color — объект класса RGBColor (см. «Класс RGBColor»).

rotate($angle)

Поворачивает черепаху на угол $angle (в градусах против часовой стрелки).

jump($step)

Сдвигает черепаху вперёд на расстояние $step, умноженное на масштабный коэффициент, линия не рисуется.

forward($step)

Рисует линию, сдвигаясь вперёд на расстояние $step, умноженное на масштабный коэффициент.

modifyBBox()

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

writePostScript(@tokens)

Дописывает во временный файл токены языка PostScript из списка @tokens, разделяя их пробелами.

writePicture($outputFileName, $device, $resolution)

Создаёт графический файл $outputFileName, вызывая интерпретатор GhostScript, используя устройство $device и разрешение $resolution (в точках на дюйм). Все передаваемые параметры не являются обязательными; если какой-то не указан, используются значения по умолчанию — TurtleOut.eps для имени файла, epswrite для устройства, 72 для разрешения.

Пришло время объяснить, для чего нам понадобился временный файл. Интерпретатор PostScript способен получать и обрабатывать токены языка PostScript по одному, причём читать их он может не только из файла, но и из стандартного ввода. Почему бы не воспользоваться этой возможностью и не отправлять токены прямо в интерпретатор? Дело в том, что мы хотим получать на выходе графические файлы точно такого размера, что и изображение, которое рисует черепаха (мы имеем в виду не размер файла в байтах, а размеры картинки в пикселах — ширину и высоту). Узнать заранее, какого размера будет картинка, не представляется возможным — это выяснится только в самом конце работы черепахи. А интерпретатор PostScript должен знать размеры будущего изображения в самом начале работы — до того, как что-либо нарисовано. Поэтому придётся сохранять PostScript-команды по мере того, как черепаха рисует, во временном файле, одновременно изменяя габаритный прямоугольник. Когда же возникнет потребность сохранить изображение в графическом файле, размер картинки уже будет известен. Его следует сообщить PostScript-машине, после чего «скормить» ей содержимое временного файла. Ещё одна причина для использования временного файла — возможность сохранения картинок не только в самом конце работы черепахи, но и в процессе рисования.

Этот вспомогательный класс будет служить для представления цветов в программе. Мы используем популярную трёхкомпонентную цветовую модель RGB, используемую и в языке PostScript. Три числа в диапазоне от нуля до единицы задают доли соответственно красного, зелёного и синего цветов в цветовой смеси. В качестве объектной модели цвета подойдёт ссылка на массив, состоящий из этих трёх чисел.

Возможно, мы бы обошлись без специального класса RGBColor, если бы не захотели задавать цвета не только как трёхчисловые массивы, но и по именам. Список имён популярных цветов (а также трёхкомпонентные их значения) мы взяли прямо с сайта организации W3C (WWW Consortium), занимающейся разработкой веб-стандартов, правда там цветовые компоненты масштабированы до 256. Кроме того в классе будет определён метод, выполняющий некоторые вычисления с цветами — интерполяцию (нахождение цвета, промежуточного между двумя заданными цветами).

В качестве дополнительного удобства позволим задавать цвета в формате, широко используемом веб-разработчиками: #RGB или #RRGGBB, где R, G, B — шестнадцатеричная цифра. В первом случае каждая компонента цвета изменяется от нуля до пятнадцати, во втором — от нуля до 255. Если цвет задаётся в таких форматах, цветовые компоненты подлежат масштабированию — делению соответственно на 15 и на 255. Цвет «tomato» может быть задан как tomato, #FF6347, #F65 (приблизительно), или как ссылка на анонимный массив [1, .388235294117647, .27843137254902].

Для хранения таблицы соответствий имён цветов и их значений в классе RGBColor мы заведём локальную переменную %colorByName. Ключи в этом ассоциативном массиве — имена цветов, значения — строки в формате #RRGGBB.

В классе будут определены следующие методы:

new($color)

Конструктор без параметров. Создаёт новый объект RGBColor. Параметр $color — любой из описанных выше форматов.

interpolate($otherColor, $t)

Создаёт новый объект RGBColor, промежуточный между данным цветом и цветом $otherColor. Параметр $t отвечает за то, ближе к какому из цветов — исходному или $otherColor — будет результат. При нулевом $t возвращается цвет, равный исходному, при единичном — равный цвету $otherColor. В общем случае при t 0 1 возвращается цвет, каждая компонента которого вычисляется по формуле 1 t X 0 + t X 1 , где X 0  — соответствующая компонента исходного цвета, а X 1  — компонента цвета $otherColor.

Информатика-54© А. Н. Швец