В этой главе я буду показывать как можно рисовать некоторые сложные объекты с помощью процедур и циклов. Я буду использовать все стандартные процедуры (команды), которые были рассмотрены ранее в этой книге. Многие из них просто сокращают текст, не более того. Примеры рисуются в области 200*200. Содержание этой главы будет расти постепенно, так я показываю тут только те объекты, которые мне понадобились в работе. Придумывать объекты специально нет времени. А уже придуманные разумно запомнить на будущее, чтобы не пропали.
Такой объект мне часто приходится рисовать по работе, так как именно вогнутая параболическая линза фокусирует рентгеновское излучение. Но фокус в том, что таких линз надо много. Люди, которые рисуют в готовых программах графики, обычно параболу заменяют на часть круга. Так проще. Но при программировании можно нарисовать точную параболу, в этом нет никаких проблем. Фокус лишь в том, что если мне надо рисовать много таких линз, то удобно часть параметров определять языком постскрипта один раз, а аргументами процедуры сделать только координаты ее размещения на рисунке. Также есть специфика в том, что объект хочется закрасить серым и обвести черным. Значит сложный контур надо определять 2 раза. По этой причине разумно сделать дополнительную вспомогательную процедуру, которую главная процедура будет использовать два раза.
Чтобы переменные не пересекались с переменными других процедур, они все имеют впереди букву О. Итак главная процедура называется PRL и она два раза вызывает вспомогательную процедуру PRLC, последняя буква означает контур. Она рисует контур. У линзы есть параметры в переменных, которые надо определить перед тем как использовать процедуру. А аргументами являются два числа -- кооринаты центра объекта на графике. Ниже показан код обоих процедур
/PRLC{Ox Oy Ov sub m 0 Ov rl
On { Oy 2 mul Ody add Ody mul neg 2 div OR div Ody rl /Oy Oy Ody add def } repeat
0 Ov rl Ox1 2 mul 0 rl 0 Ov neg rl /Ody Ody neg def
On { Oy 2 mul Ody add Ody mul 2 div OR div Ody rl /Oy Oy Ody add def } repeat
0 Ov neg rl Ox1 2 mul neg 0 rl /Ody Ody neg def }bd
/PRL{/Oy ed /Ox ed gs Ox Oy tr /Ov OA 4 div def
/Ody OA On div def /Oy OA 2 div neg def /Ox1 Oy Oy mul 2 div OR div Od 2 div add def /Ox Ox1 neg def
PRLC 0.8 fr PRLC Ot slw 0 0 0 srgb sl gr }bd
А рисунок, который показан, на картинке, сделан с помошью такого кода
/OA 30 def /OR 3.75 def /Od 3 def /On 99 def /Ot 1 def 65 50 PRL 65 150 PRL 135 50 PRL 135 150 PRL
Здесь переменная ОА задает апертуру линзы, OR -- радиус кривизны параболы у вершины, Оd -- толщину перемычки между двумя поверхностями, On -- число точек для рисования параболы, обычно 99 достаточно, Ot -- толщину линии для контура. Эти переменные полностью определяют линзу, но не указывают где ее надо размеcтить. И вот координаты размещения задаются аргументами.
Иногда приходится рисовать круги. Это довольно затратная команда даже в исходном коде. И при этом часто забываешь как это кодируется. Удобно иметь готовый инструмент в самом общем виде. То есть двумерную решетку кругов. Код такой процедуры может выглядеть, например, так
/gridCirc{gs /Xb ed /Xg ed /XR ed /Xn ed /Xm ed /Xd ed /Xr ed /Y ed /X ed /Yc Y def
Xn { /Xc X def
Xm { Xc Xr add Yc m Xc Yc Xr 0 360 arc XR Xg Xb srgb fir /Xc Xc Xd add def } repeat
/Yc Yc Xd add def
} repeat gr}bd
А рисунок, который показан на картинке, сделан с помощью такого кода
50 50 15 50 3 3 1 0 0 gridCirc
Эта процедура имеет 9 числовых аргументов, структура которых такая: x y координаты центра первого круга, радиус кругов, период рещетки (одинаковый по обеим осям), число кругов по горизонтали и вертикали и три параметра R G B для задания цвета заливки. Числовые параметры задаются очень просто. Процедура может рисовать даже один круг, или два. Иногда это все равно удобнее, чем исходные команды.
Иногда приходится рисовать двумерную сетку линий. Тут снова циклы и не всегда помнится как это делать. Снова удобно иметь готовый инструмент в самом общем виде. Код такой процедуры может выглядеть, например, так
/gridLine{/Xt ed /Xb ed /Xg ed /Xr ed /Xn ed /Xm ed /Xd ed /Y ed /X ed gr Xt slw
Xr Xg Xb srgb /Xh{Xn 0 gt {Xd Xn 1 sub mul }{Xd Xn neg 1 sub mul} ifelse}bd
/XM{Xm 0 gt {Xm}{0} ifelse}bd
/Xw{Xm 0 gt {Xd Xm 1 sub mul}{Xd Xm neg 1 sub mul} ifelse}bd
/XN{Xn 0 gt {Xn}{0} ifelse}bd
/Xc X def XM { Xc Y Xc Y Xh add ls /Xc Xc Xd add def } repeat
/Yc Y def XN { X Yc X Xw add Yc ls /Yc Yc Xd add def } repeat gr}bd
А рисунок, который показан на картинке, сделан с помощью такого кода
25 25 50 4 4 1 0 0 1 gridLine
Эта процедура снова имеет 9 числовых аргументов, структура которых такая: x y координаты начала первой линии, d -- период, m -- число линий по горизонтали (вертикальных) и n -- вертикали (горизонтальных), три параметра R G B для задания цвета линий и t -- толщина линий. Но среди параметров нет таких, которые задают длину линий. Длина вертикальных линий задается как d*(n-1), а горизонтальных как d*(m-1). В этом случае все линии образуют законченный прямоугольник. А как быть, если нужно рисовать только вертикальные или только горизонтальные линии. Ведь число задает длину линий и ноль задавать нельзя. Да, вместо нуля надо задавать отрицательное значение. Тогда длину линий будет задавать модуль числа, а число линий будет равно нулю автоматически.
В разделе 6.3 я показал как можно нарисовать объемный шар используя круговой градиент цвета. В этом разделе я покажу как делать градиент цвета в любой сложной форме. Например, нам нужно нарисовать цилиндр, освещаемый из-за спины и наклоненый так что верх к нам ближе. В этом случае верхняя часть цилиндра вся светлая, нижняя часть не видна совсем, а боковая часть освещается тем больше, чем меньше угол между нормалью к поверхности и направлением до источника света. Для цилиндра мы используем более простой алгоритм линейного градиента уровня серого цвета.
При наклоне цилиндра его верхняя грань превращается в эллипс. В постскрипте нет готового контура для эллипса, но нам его и не нужно. Мы будем его вычислять по простой формуле, в которой координата x пробегает весь диаметр, а координата y = (b/a)*sqrt(a*a - x*x). Для верхней части эллипса y положительна, а для нижней -- отрицательна. Координата x проходит в цикле весь диаметр слева направо. При этом для рисования боковой грани нам надо задавать координаты двух точек на отрезке эллипса x1,y1 и x2,y2, выделять прямоугольник с высотой h и рисовать его текущим цветом, который все время меняется. А для рисования верхней грани надо просто нарисовать контур в виде эллипса и заштриховать его белым цветом.
Процедура cy3d имеет два повторяющихся элемента кода, который удобно выделить в вспомогательные процедуры po и ini. Весь код программы выглядит так
/po{/x1 x def /y1{a a mul x x mul sub sqrt b mul a div neg}def}bd
/ini{/x a neg def /dx 2 a mul n div def}bd
/cy3d{/C ed /Y ed /X ed gs X Y tr 50 30 100 100 /n ed /h ed /b ed /a ed
ini /d 1 n div def n{po /x2 x dx add def /y2{a a mul x2 x2 mul sub sqrt b mul a div neg}def
x1 y1 m x2 y2 l 0 h neg rl x1 x2 sub y1 y2 sub rl 0 h rl 1 1 C sub abs sub fr /x x dx add def /C C d add def}repeat
ini po x1 y1 m n{/x x dx add def po x1 y1 l}repeat n{/x x dx sub def po x1 y1 neg l}repeat 1 fr gr}bd
0.9 sg 0 0 200 200 rectfill 100 150 0.7 cy3d
Теперь я кратко объясню сам код. Процедура имеет три параметра: координаты центра верхней грани цилиндра и начальный уровень серого цвета. В самом начале эти параметры определяются в переменные X, Y и C. У процедуры есть также скрытые параметры a, b, h и n. Параметры a,b,h уже встречались в формуле для контура эллипса. Это горизонтальный и вертикальный радиусы цилиндра и высота проекция высоты боковой грани, а параметр n задает число повторений в цикле. Эти параметры определены внутри процедуры, но так, что их легко поменять, если необходимо. Процедура ini задает начальное значение координаты x и шаг ее изменения. Затем в переменную d вычисляется шаг измерения уровня серого цвета и открывается цикл. В цикле процедура po вычисляет координаты первой точки x1,y1. Затем явно вычисляются координаты второй точки x2,y2. По указанным точкам, используя команды m и l рисуется контур тонкого прямоугольника, затем вычисляется уровень серого цвета по формуле 1 - abs(1 - C) и прямоугольник заполняется цветом.
Формула для цвета устроена таким образом, что если С меньше единицы, то имеем С, а если больше 1, то имеем 2-С. При этом цвет сначала растет от темного до белого, а потом снова убывает до темного, хотя сам параметр С все время растет. Соответственно, цилиндр бликует в определенном месте в зависимости от начального значения параметра С. После того, как нарисован один прямоугольник, надо увеличить на шаг координату x и параметр C. И боковая грань нарисована. Осталось нарисовать контур верхней грани и закрасить его белым цветом. Это делается проще. Снова задается начальная точка, ставится команда m, затем в цикле по точкам ставится команда l. Как только прошли весь цикл, надо возвращаться назад в том же цикле, только уменьшая координату x и беря другой знак координаты y. У меня исходно координата y1 считается отрицательной, поэтому второй раз надо изменить знак командой neg.
Чтобы выделить белый цвет, перед использованием процедуры я залил прямоугольник светлосерым цветом. Это делается стандартными командами. Как видим, после того, как придумали алгоритм, написать код совсем не трудно. Для удобства его можно писать по частям, вводя дополнительные процедуры, а потом те процедуры, которые не повторяются, можно убрать. Именно так я и делал.
Использование процедур с параметрами особенно удобно, когда надо нарисовать периодические последовательности объектов, например, решетку из цилиндров. В этом случае просто используются циклы по параметрам процедуры, все это выглядит намного нагляднее, чем код без процедур. Рисунок, показанный справа, использует указанную выше процедуру и дополнительный код, который можно записать, например, так.
0.333 Sc 0 0 600 600 rectclip
0.9 sg 0 0 600 600 rectfill
/xa -200 def /ya 660 def /Ca 0.7 def
7{4{/xa xa 200 add def xa ya Ca cy3d}repeat /xa xa 100 add def /ya ya 60 sub def
3{/xa xa 200 sub def xa ya Ca cy3d}repeat /xa xa 300 sub def /ya ya 60 sub def}repeat
Здесь все достаточно просто, я объясню только некоторые моменты. Я хочу сделать рисунок, как и предыдущий, в области 200*200, но если не менять размеры цилиндров, то удобно делать область размером 600*600. По этой причине первой командой я устанавливаю масштабирование с уменьшением в три раза по обоим координатам. Затем я устанавливаю ограничение рисунка прямоугольником 600*600. Это не обязательно, если кроме данного рисунка больше ничего нет. А если данный рисунок является частью другого рисунка, но это необходимо. Поэтому на всякий случай лучше это сделать. Далее я заливаю эту область светлосерым цветом. Это снова не обязательно, так как цилиндры заполняют всю область, но и хуже не будет.
И теперь просто рисуем двумерную решетку в двойном цикле. Но параметры цикла надо взять с запасом, чтобы цилиндры заполнили всю область, если мы хотим показать, что это фрагмент бесконечной решетки. А если нет, то можно ставить любые числа повторений. Итак, сначала задаем стартовые значения параметров, затем меняем их в цикле и вызываем процедуру. Важно задать направление рисования сверху вниз, так как последующие цилиндры закрывают предыдущие. И у нас горизонтальные ряды повторяются через раз, поэтому надо два разных цикла для горизонтальных рядов. Вот и все.
В данном примере я покажу как можно очень просто рисовать абстрактные картины, которые можно распечатать и повесить на стену. Картины можно рисовать самые разные, но наиболее простой способ -- это использовать генератор случайных чисел. Например, картина, показанная справа, была создана при помощи вот такого совсем простого кода.
/rnd{rand 2147483647 div}bd
/trian{rnd rnd rnd srgb rnd rnd m rnd rnd l rnd rnd l fir}bd
1000001 srand 200 200 sca 199{trian}repeat
Здесь, как обычно, используются мои стандартные команды, описанные ранее в этой книге. Новой является процедура rnd. Она необходима по той причине, что в языке постскрипт генератор случайный чисел, то есть команда rand выдает целое число в диапазоне от 0 до 2147483647=231--1. А в других языках числа выдаются в диапазоне от 0 до 1, что более привычно. И данная процедура исправляет это различие.
Далее описана процедура, которая рисует треугольник случайным цветом и по случайным координатам вершин. Затем генератор случайных чисел инициируется некоторым числом (его можно менять), задается масштабирование на область 200*200 и процедура 199 раз вызывается в цикле. Число повторений тоже можно менять. Как видим, все очень просто. Естественно, можно придумать и более сложные программы, особенно с использованием рекурсии. Можно рисовать фракталы, решения нелинейных уравнений и многое другое.