Пример использования будущего класса 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
в этом элементе следует понимать так: «встать в точку
;
прочертить отрезок в точку
;
прочертить отрезок в точку
».
Этот компактный способ представления линий (не только ломаных, но и гладких)
будет нам не очень удобен: он годится для рисования только одним цветом
и только одной толщины. При изменении этих величин потребуется закрыть текущий
элемент 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. После выполнения команд
%
inkscape -A Picture.pdf Picture.svg
%
inkscape -P Picture.ps Picture.svg
%
inkscape -e Picture.png Picture.svg
картинка из файла Picture.svg
будет преобразована
в картинки Picture.pdf
, Picture.ps
,
Picture.png
в соответствующих форматах.
Преобразование в растровые форматы неизбежно приводит к потере информации, так как растровое изображение формируется из точек заданного размера. Если размер крупный (низкое разрешение), изображение получится грубым. Качество изображения возрастает с ростом разрешения, но вместе с этим растёт и размер файла. Процесс преобразования изображения из векторного формата в растровый называется растеризацией и часто сопровождается сглаживанием, которое призвано повысить качество растрового изображения. С некоторыми подробностями процесса растеризации можно ознакомиться в нашем руководстве METAPOST. Краткий курс в разделе «Компьютерная графика». Растровые изображения плохо переносят повороты и масштабирование. Получить обратно исходное векторное изображение из растрового практически невозможно.
Мы откажемся от использования SVG. Во-первых, из-за его громоздкости, а во-вторых, для того, чтобы познакомиться с другими возможностями.
Одной из них является формат PostScript, который по совместительству является весьма мощным, красивым и интересным языком программирования. Исполнителями программ на языке PostScript являются так называемые графические устройства — принтеры или графические окна на экране. В качестве устройств также могут выступать графические файлы в разнообразных форматах, как растровых, так и векторных. Результатом исполнения программ на языке PostScript является изображение на устройстве (на бумаге, если используется принтер, в окне, в файле). Посредником между программой на PostScript и устройством является специальная программа — интерпретатор. Программа содержит набор модулей — драйверов, каждый из которых может обслуживать устройства определённого типа.
Самая известная из программ-интерпретаторов языка PostScript — GhostScript. Она входит
в дистрибутивы большинства Linux
-систем
и запускается командой gs. Не вдаваясь пока в подробности,
скажем лишь, что, запустив эту программу с нужными параметрами, можно
преобразовать PostScript-документ во многие другие форматы. Список драйверов,
поддерживаемых программой gs, можно увидеть, запустив её
с ключом -h
(после слов «Available devices»):
%
gs -h
Среди них есть графическое окно (x11alpha), PNG-файл (pngalpha), PDF-файл (pdfwrite). Есть устройства для создания файлов с управляющими командами для принтеров различных моделей.
Разберём программу на языке PostScript, рисующую одноцветную четырёхзвенную ломаную:
examples/Chair.psPostScript%!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 setlinecap
и 1
setlinejoin
устанавливают соответственно форму концов линий и углов —
в обоих случаях закруглённую. Команда 1
0.388235294117647 0.27843137254902 setrgbcolor
устанавливает цвет
рисования; три числа, предшествующие setrgbcolor — компоненты
цвета в цветовой схеме RGB. Команда newpath
начинает построение новой кривой или
ломаной (пути в терминологии PostScript). 0 0 moveto
смещает воображаемый карандаш в точку
с координатами
,
24 24 lineto
чертит отрезок в точку
…
Обращаем внимание, что в отличие от SVG координатные оси
направлены привычным образом: ось ординат смотрит вверх. Команда stroke
завершает построение пути и наносит на
рисунок линию ранее заданного цвета, толщины, и с заданными формами концов
и углов. Наконец, команда showpage
переносит
рисунок на заданный носитель (на лист бумаги в принтере, в графическое окно,
в графический файл).
Для рисования разноцветной ломаной придётся указать три команды newpath … stroke
, предваряя каждую из них командой
setrgbcolor
:
examples/ChairSegments.psPostScript%!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 снимает со стека два числа и вместо них кладёт на стек их сумму. Константы, встречающиеся в программе, просто добавляются в стек в порядке их появления, и ждут там своего часа, пока не будут как-то использованы операторами.
Чтобы пояснить эту мысль, рассмотрим программу, которая вычисляет пифагорову сумму двух чисел и , то есть . В приведённой программе каждая команда занимает отдельную строку. Расположенные справа от команд зелёные аннотации показывают состояние стека операндов после выполнения очередной команды:
PostScript3 ► 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 снимает число и добавляет квадратный корень из него.
Конечно, такое программирование очень непривычно. Даже зная назначение упомянутых команд, довольно трудно сообразить, что фрагмент программного кода
PostScriptdup mul exch dup mul add sqrt
снимает со стека два числа и кладёт обратно их пифагорову сумму. Если в программе требуется многократно вычислять пифагоровы суммы, есть резон определить оператор (назовём его hypot в честь гипотенузы), который решает эту задачу:
PostScript/hypot { dup mul exch dup mul add sqrt } def
Оператор def снимает со стека два объекта — имя будущего оператора /hypot и процедуру, которую следует связать с этим именем. Процедура — это произвольная последовательность команд, заключённая в фигурные скобки. В дальнейшем можно пользоваться оператором hypot, как если бы он был встроенным:
PostScript5 12 hypot ► 13
Полезным для нас применением этой возможности будет определение команды (назовём её seg), которая снимает со стека четыре числа — координаты начала и конца отрезка, и рисует отрезок. Вот её определение:
PostScript/seg { 4 2 roll newpath moveto lineto stroke } def
Выглядит таинственно. Что там за четвёрка, двойка, что означает roll? И почему командам moveto и lineto не предшествуют пары чисел?
Разберёмся по порядку. Команда roll
снимает
со стека два числа (в нашем примере 4
и 2
). Затем берёт четыре верхних оставшихся на стеке
элемента и сдвигает их по кругу вправо два раза. Таким образом код 4 2 roll
меняет местами верхнюю пару элементов
стека операндов, и расположенную под ней, сохраняя порядок элементов в каждой
паре. Между прочим, если бы в нашем распоряжении не было встроенной команды
exch
, то её можно было бы определить через
roll
:
PostScript/exch { 2 1 roll } def
Предположим, нам нужно начертить линию из точки с координатами в точку . Проследим, как будет работать команда
PostScript111 222 333 444 seg
PostScript111 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 наш пример с разноцветной табуреткой будет выглядеть так:
examples/ChairSegmentsShort.psPostScript%!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 к созданию математических иллюстраций подробно обсуждаются в прекрасном руководстве Билла Кассельмана.
Существует мнение, что уважающий себя программист, на каком бы языке он ни программировал, должен быть знаком с PostScript. Мы соглашаемся с этим мнением и считаем, что знакомство с этим языком расширяет кругозор программиста.
Внимание | |
---|---|
Программирование на PostScript взрывает мозг! |
Обычно программы на языке PostScript не пишутся непосредственно программистом, а создаются другими программами. Наша программа тоже будет создавать программы на PostScript и запускать интерпретатор GhostScript для последующей трансляции изображения в другие форматы. Другой пример генератора PostScript-программ даёт METAPOST, которому мы посвятили отдельное руководство.
Объект класса Turtle
обладает набором свойств. Некоторые
из них могут запоминаться по команде SAVE и восстанавливаться по
команде RESTORE. Все изменения таких свойств, проделанные после
последнего SAVE, восстанавливаются при выполнении
RESTORE. Будем называть свойства, на которые влияют эти две
команды, сохраняемыми.
Для хранения набора свойств будет очень удобен ассоциативный массив. В нём ключами будут названия свойств, а храниться там будут соответствующие значения. В числе сохраняемых свойств будут x и y (координаты черепахи), direction (её направление), linewidth (толщина линии) и color (цвет). Набор сохраняемых свойств будем называть состоянием черепахи.
Поскольку команды SAVE и последующие изменения состояния могут следовать друг за другом без соответствующих команд RESTORE, сохранённых состояний (моментальных снимков) может быть много, и для хранения запомненных состояний подойдёт список. В начале работы черепахи такой список состоит из единственного состояния. Текущее, актуальное состояние всегда находится в конце списка, и именно в него и вносятся все возможные изменения. То то единственное состояние, которое имеется в начале работы, и является текущим (так как является последним в списке). Вызов SAVE приводит к добавлению копии текущего состояния в конец списка, а вызов RESTORE удаляет последний элемент списка, тем самым отменяя все сделанные после последней команды SAVE изменения. Для хранения списка состояний хорошо подходит стек.
Итак, решено использовать массив в качестве стека запомненных состояний черепахи, а сами состояния помещать в стек как ссылки на анонимные ассоциативные массивы сохраняемых свойств, добавляя копию вершины стека в этот же стек. Эта операция называется дупликацией вершины стека.
Будет ошибкой добавлять в массив копию его последнего элемента:
Perlpush @stack, $stack[-1];
Дело в том, что элементами стека будут не сами состояния (ассоциативные массивы), а ссылки на них. Тогда ассоциативный массив, хранящий состояние, будет существовать в единственном экземпляре, а стек будет заполняться копиями ссылок, указывающих на это состояние. Так что в стек следует добавлять не копию ссылки, а новую ссылку на копию ассоциативного массива. Правильная политика могла бы быть такой:
Perlpush @stack, {%{$stack[-1]}};
При таком подходе берётся вершина стека — ссылка на ассоциативный массив ($stack[-1]
), извлекается ассоциативный массив по этой
ссылке (%{$stack[-1]}
), и затем создаётся ссылка
на новый ассоциативный массив, в который копируются элементы старого: {%{$stack[-1]}}
. И, наконец, то, что получилось,
добавляется в стек.
Но мы поступим иначе. Заметим, что после команды SAVE совсем не обязательно состояние черепахи будет меняться. А если и будет, то не обязательно будут меняться все до единого её сохраняемые свойства. Чаще всего изменения будут касаться только части свойств. Остальные будут мёртвым грузом занимать память в ожидании конца программы или команды RESTORE, которая их удалит. Вместо дупликации стека мы по команде SAVE добавим в стек состояний пустое состояние — ссылку на пустой ассоциативный массив:
Perlpush @stack, {};
Изменение какого-либо свойства по-прежнему будет осуществляться как добавление/изменение значения в ассоциативном массиве, на который указывает ссылка в вершине стека:
Perl$stack[-1]->{linewidth}=6;
Если пара ключ-значение отсутствовала на вершине стека (а именно так и будет сразу после SAVE), эта пара будет туда добавлена.
Поиск значения при такой методике несколько усложнится. Придётся просматривать стек от вершины к основанию в поисках сохранённого состояния, в котором присутствует соответствующее свойство:
Perlfor(my $i=$#stack; $i>=0; $i--) { if(exists $stack[$i]->{direction}) { $direction=$stack[$i]->{direction}; last; здесь мы покидаем цикл, поскольку значение свойства direction найдено и сохранено в переменной$direction
} }
Объектная модель рисующей черепахи будет включать в себя следующие свойства:
Стек запомненных состояний — ссылка на массив, заполненный ссылками на ассоциативные массивы — фреймы запомненных свойств. В начале работы стек содержит единственный фрейм, в котором свойства x и y равны нулю (черепаха в начале координат), direction тоже ноль (черепаха повёрнута вправо), свойство color содержит чёрный цвет, scale равно единице (единичный масштаб).
Свойство, описывающее «габаритный» прямоугольник рисунка (Bounding BOX). Это будет ссылка на ассоциативный массив с ключами llx, lly, urx, ury. Соответствующие значения — координаты левого нижнего (Lower Left) и верхнего правого (Upper Right) углов прямоугольника. В начале работы прямоугольник вырожден в точку, так что все эти координаты нулевые, однако каждый нарисованный отрезок может расширить этот прямоугольник, если необходимо, отодвигая левый нижний угол влево и/или вниз, а верхний правый — вправо и/или вверх. При этом учитывается текущая толщина линии: достаточно расширить прямоугольник вместил два кружочка с центрами в концах рисуемого отрезка и с диаметрами, равными толщине линии.
Значение этого свойства — временный файл, открытый для записи. Файл будет автоматически удалён по завершению программы.
Теперь перечислим методы класса.
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
)
Создаёт графический файл
, вызывая
интерпретатор GhostScript, используя устройство
$outputFileName
и разрешение
$device
(в точках на дюйм). Все
передаваемые параметры не являются обязательными; если какой-то не указан,
используются значения по умолчанию — $resolution
TurtleOut.eps
для имени
файла, epswrite
для устройства, 72
для разрешения.
Пришло время объяснить, для чего нам понадобился временный файл. Интерпретатор PostScript способен получать и обрабатывать токены языка PostScript по одному, причём читать их он может не только из файла, но и из стандартного ввода. Почему бы не воспользоваться этой возможностью и не отправлять токены прямо в интерпретатор? Дело в том, что мы хотим получать на выходе графические файлы точно такого размера, что и изображение, которое рисует черепаха (мы имеем в виду не размер файла в байтах, а размеры картинки в пикселах — ширину и высоту). Узнать заранее, какого размера будет картинка, не представляется возможным — это выяснится только в самом конце работы черепахи. А интерпретатор PostScript должен знать размеры будущего изображения в самом начале работы — до того, как что-либо нарисовано. Поэтому придётся сохранять PostScript-команды по мере того, как черепаха рисует, во временном файле, одновременно изменяя габаритный прямоугольник. Когда же возникнет потребность сохранить изображение в графическом файле, размер картинки уже будет известен. Его следует сообщить PostScript-машине, после чего «скормить» ей содержимое временного файла. Ещё одна причина для использования временного файла — возможность сохранения картинок не только в самом конце работы черепахи, но и в процессе рисования.
Этот вспомогательный класс будет служить для представления цветов в программе. Мы используем популярную трёхкомпонентную цветовую модель RGB, используемую и в языке PostScript. Три числа в диапазоне от нуля до единицы задают доли соответственно красного, зелёного и синего цветов в цветовой смеси. В качестве объектной модели цвета подойдёт ссылка на массив, состоящий из этих трёх чисел.
Возможно, мы бы обошлись без специального класса
RGBColor
, если бы не захотели задавать цвета не только
как трёхчисловые массивы, но и по именам. Список имён популярных цветов (а также трёхкомпонентные
их значения) мы взяли прямо с сайта организации W3C (WWW
Consortium), занимающейся разработкой веб-стандартов, правда там цветовые
компоненты масштабированы до .
Кроме того в классе будет определён метод, выполняющий некоторые вычисления
с цветами — интерполяцию (нахождение цвета, промежуточного между двумя
заданными цветами).
В качестве дополнительного удобства позволим задавать цвета в формате, широко
используемом веб-разработчиками: #RGB
или
#RRGGBB
, где R
, G
,
B
— шестнадцатеричная цифра. В первом случае каждая
компонента цвета изменяется от нуля до пятнадцати, во втором — от нуля до . Если цвет задаётся в таких форматах,
цветовые компоненты подлежат масштабированию — делению соответственно на и на . Цвет «tomato» может быть задан как
tomato
, #FF6347
, #F65
(приблизительно), или как ссылка на анонимный массив [1,
.388235294117647, .27843137254902]
.
Для хранения таблицы соответствий имён цветов и их значений в классе
RGBColor
мы заведём локальную переменную
%colorByName
. Ключи в этом ассоциативном массиве — имена
цветов, значения — строки в формате #RRGGBB
.
В классе будут определены следующие методы:
new($color
)
Конструктор без параметров. Создаёт новый объект
RGBColor
. Параметр
— любой из описанных выше
форматов.
$color
interpolate($otherColor
, $t
)
Создаёт новый объект RGBColor
, промежуточный
между данным цветом и цветом
. Параметр
$otherColor
отвечает за то, ближе к какому из
цветов — исходному или $t
—
будет результат. При нулевом $otherColor
возвращается цвет, равный исходному, при единичном — равный цвету
$t
. В общем случае при
возвращается цвет, каждая компонента которого вычисляется по формуле
,
где
—
соответствующая компонента исходного цвета, а
—
компонента цвета $otherColor
.
$otherColor