Хотя в прошлом разделе и показаны несколько примеров постскрипт кода, а также дано описание как он работает, но научиться методам программирования по этим примерам все же непросто. В этой главе я попробую дать какие-то рекомендации общего характера с иллюстрациями. Проверять иллюстрации можно таким же способом, как и раньше, то есть используем пустой шаблон eps-файла с названием exam.eps. В нем заданы все команды языка vkPS в преамбуле и указана только одна команда run, аргументом которой является имя файла. Соответственно все примеры программ записаны в отдельные файлы с названиями exam-01.psf, exam-02.psf и так далее. Чтобы просмотреть все примеры нужно просто последовательно менять имена файлов в этой команде или написать несколько команд run с названиями разных файлов в качестве аргумента. При этом необходимо расширить размер картинки в 4-й строке с 200 200 до 400 400 (для 4 картинок) и делать трансляцию каждой картинки в новое место. Вот пример
gs (exam-04.psf) run gr gs 200 0 tr (exam-05.psf) run gr gs 0 200 tr (exam-06.psf) run gr gs 200 200 tr (exam-07.psf) run gr
Мы же будем анализировать именно тексты этих файлов, потому что команды шаблона являются стандартными и неизменными. Вам понадобится некоторое время чтобы их запомнить. Команды имеют очень краткие имена, что иногда приводит к уменьшению размера файлов.
В языке Постскрипт есть возможность организовывать процедуры с параметрами. Внутри одной процедуры могут быть другие процедуры. Вообще писать голый текст программы невыгодно, так как для него сложно составить описание (документацию). Выгодно весь текст писать в виде процедур с параметрами, тогда его легче читать, а процедуры можно использовать многократно. В этом разделе мы рассмотрим три способа начертить круг. Есть еще одна тонкость. Процедуры разумно создавать только для контуров объектов. Если описан контур, то его можно вычертить командой sl, предварительно задав толщину линии и цвет, а можно залить область внутри контура цветом. Есть и третья возможность, а именно, можно использовать контур для задания границ области, вне которой последующие объекты не рисуются. Можно делать полностью готовый контур, а можно делать контур без начала, такой контур затем можно комбинировать с другими контурами.
Итак, рассмотрим первый способ начертить круг. Круг - замкнутый объект, поэтому мы зададим контур полностью, то есть с началом. Параметрами будут x,y координаты центра круга и R - его радиус в указанном порядке. Создадим первую процедуру для считывания параметров в переменные и задания начала контура. Она выглядит так
/par1{/R ed /y ed /x ed x R add y m}def
Эта процедура определяет переменные R,y,x (в обратном порядке, так как числовые значения вынимаются из стека, первым идет последний элемент в списке) перед именем процедуры. После этого она устанавливает начальной точкой контура точку с координатами x+R,y, то есть это правая точка на окружности. Переменные определяются с помощью команды ed (exch def), эта команда берет из стека число, например 25 и формирует текст вида /R 25 def, а это уже чистое определение переменной. Именно такой способ позволяет задавать сразу много ззначений переменных и потом определять их в процедуре. Теперь напишем саму процедуру задания контура круга
/cir1{par1 x y R 0 360 arc}bd
Если в первом случае определить процедуру можно было командой def, то во втором случае надо уже использовать bd, потому что в теле процедуры есть другая процедура. Итак, что мы сделали. Мы фактически очень сложным образом применили единственную команду arc, загнав три ее аргумента сначала в переменные, а затем используя эти переменные. Можно было бы и по другому, более просто, но такой способ наиболее универсален. Теперь надо написать описание. Оно может выглядеть примерно так.
Процедура cir1 задает контур круга по трем параметрам, вызов процедуры: x y R cir1
Теперь попробуем использовать процедуру в цикле, постепенно изменяя толщину линий. Код такой программы выглядит так
/par1{/R ed /y ed /x ed x R add y m}def
/cir1{par1 x y R 0 360 arc}bd
0 0 1 srgb /X 54 def /t 1 def 10{t slw X X 45 cir1 sl /t t 1 add def /X X 9 add def}repeat
Здесь сначала задается синий цвет, затем определяются переменные X и t, затем в цикле 10 раз вычерчивается контур круга командой sl и изменяется диагональная координата центра круга и толщины линии. Важное замечание. Как и во всех интерпретируемых языках, переменные, используемые в процедуре нельзя использовать в цикле, иначе их нельзя будет корректно переопределить, процедура будет портить их значения.
Второй способ начертить круг состоит в том, что круг аппроксимируется многоугольником, контур многоугольника задается по абсолютным координатам точек. Этот способ более трудоемкий, но зато мы получаем и круг и многоугольник сразу, что добавляет универсальности. Естественно многоугольник имеет дополнительный параметр - число сторон. Итак, нам снова понадобится процедура par1 и дополнительно к ней мы напишем другую процедуру
/par2{/N ed /a 360 N div def}def
Она считывает переменную N - число сторон и сразу определяет угол, приходящийся на одну сторону многоугольника. Теперь напишем саму процедуру
/cir2{par1 par2 /A a def N{A cos R mul x add A sin R mul y add l /A a A add def}repeat}bd
Здесь сначала вызываются сразу две процедуры, формирующие переменные. Затем задается начальное значение угла A, затем вычисляются координаты точки на окружности по формулам X = x + R*cos(A), Y = y + R*sin(A), но эти точки не засылаются в переменные, а непосредственно формируются в стеке и затем сразу используются командой l. Команда выглядит как переменная, но в постскрипте нет разделения на команды, процедуры и переменные. Это все процедуры или объекты. Естественно, что названия всех команд, процедур и переменных лучше не повторять, иначе они переопределяются. А изменение значений переменной как раз и выглядит как переопределение объекта. После того, как точка на контуре задана, необходимо изменить значение угла. Описание такой процедуры может выглядеть примерно так.
Процедура cir2 задает контур многоугольника по четырем параметрам, вызов процедуры: N x y R cir2
Теперь напишем программу применения этой процедуры
/par1{/R ed /y ed /x ed x R add y m}def
/par2{/N ed /a 360 N div def}def
/cir2{par1 par2 /A a def N{A cos R mul x add A sin R mul y add l /A a A add def}repeat}bd
0 0 1 srgb 1 slw /X 54 def /N 4 def 10{N X X 45 cir2 sl /N N 2 add def /X X 9 add def}repeat
Здесь новой является только последняя строчка, вместо толщины линии в цикле меняется число сторон многоугольника от 4 (квадрат) до 22 (почти круг). Таким образом, хотя эта процедура и более сложная для вычерчивания круга, она может вычерчивать многоугольники с любым числом сторон.
Третий способ начертить круг полностью совпадает со вторым по результатам, то есть снова чертится многоуголник. Но если во втором способе для задания контура используются абсолютные координаты точек, то в третьем способе используются относительные координаты, то есть задаются координаты приращения к старой точке для получения новой точки, а контур формируется командой rl. Чисто геометрически легко вычислить, что длина стороны многоугольника равна 2*R*sin(a/2), где параметры R и a определены выше. Мы стартуем от правой точки и начальное значение угла надо задать как A = 90 - a/2, а потом в цикле сразу прибавлять к текущему значению угла A значение угла a. Соответственно, в этом методе к уже определенным процедурам par1 и par2 нужно добавить процедуру
/par3{/R a 2 div sin R mul 2 mul def /A 90 a 2 div sub def}def
которая переопределяет переменную R присваивая ей значение длины стороны многоугольника и задает стартовое значение угла. А сама процедура вычерчивания контура многоугольника выглядит так
/cir3{par1 par2 par3 N{/A A a add def A cos R mul A sin R mul rl}repeat}bd
то есть в данной процедуре цикл содержит даже меньше команд, чем во втором методе из-за отсутствия необходимости каждый раз прибавлять координаты центра круга. Описание этой процедуры полностью совпадает с описанием процедуры cir2. Различия только в самом коде, то есть в методе программирования. Эти различия могут быть существенными при модификации многоугольника в незамкнутую фигуру. Третий способ интересен еще и тем, что при использовании команды rl и самих отрезков ломаной линии контур как бы чертится в результате движения виртуального пера по этому контуру от первой до последней точки. Напишем программу применения этой процедуры в таком виде
/par1{/R ed /y ed /x ed x R add y m}def
/par2{/N ed /a 360 N div def}def
/par3{/R a 2 div sin R mul 2 mul def /A 90 a 2 div sub def}def
/cir3{par1 par2 par3 N{/A A a add def A cos R mul A sin R mul rl}repeat}bd
/rc 1 def /bc 0 def 1 slw /X 54 def /N 12 def 10{rc 0 bc srgb N X X 45 cir3 sl /N N 2 add def
/X X 9 add def /rc rc 0.1 sub def /bc bc 0.1 add def}repeat
Программа сначала задает начальные значения красного и синего цветов как 1 и 0, и затем в цикле меняет число сторон многоугольника и компоненты красного и синего цветов так, что в конце они меняются местами.
Иногда возникает необходимость начертить контур сложного объекта. Естественно самый прямой способ - использовать массив координат точек, но не всегда это оптимально. Некоторые объекты можно нарисовать математическими кривыми. Так контур листка можно создать пересечением двух парабол. Такой контур можно задать процедурой без начала, чтобы его можно было комбинировать с другими процедурами рисования контура. Листок имеет начальную точку 0,0, которая в данной процедуре не фиксируется по предполагается, он рисуется по заданному числу точек N, имеет длину L и степень выпуклости k. То есть три параметра. Естественно процедура должна первоначально считать параметры N L k (в обратном порядке). Затем она вычисляет шаг сетки точек аргумента a = L/N, и задает начальный аргумент x = a. Далее вычисляется реальный параметр выпуклости как b = k/L. Далее в цикле по N точкам вычерчивается парабола по формуле y = b*x*(L-x), каждый раз прибавляя к x значение a. После этого из x вычитается 2a и кривая идет в обратную сторону по той же формуле, но со знаком минус. Постскрипт код такой операции выглядит следующим образом
/leaf{/k ed /L ed /N ed /a L N div def /x a def /b 1 L div k mul def
N{x x L x sub mul b mul l /x x a add def}repeat /x x a 2 mul sub def
N{x x L x sub mul b neg mul l /x x a sub def}repeat}bd
Описание такой процедуры может выглядеть следующим образом.
Процедура leaf чертит контур листка без начала по N точкам слева направо длиной L и степенью выпуклости k. Нормальное значение k = 1. Возможный вызов процедуры: 0 0 m 25 50 1 leaf sl
Написать программу применения этой процедуры можно в таком виде
/leaf{/k ed /L ed /N ed /a L N div def /x a def /b 1 L div k mul def
N{x x L x sub mul b mul l /x x a add def}repeat /x x a 2 mul sub def
N{x x L x sub mul b neg mul l /x x a sub def}repeat}bd
/pro{0 0 m 25 40 1 leaf fir}bd
0.5 0.5 0 srgb 2 slw 20 20 140 140 ls 0 1 0 srgb
gs 50 50 tr pro gr gs 80 80 tr 90 r pro gr gs 110 110 tr pro gr gs 140 140 tr 90 r pro gr
Здесь дополнительно введена вспомогательная процедура pro, которая полностью чертит листок заданным цветом. Затем рисуется коричневая палочка и 4 листка на ней. Так как сама процедура рисует объект с началом в точке 0,0, то для перемещения и вращения объекта необходимо задавать временные изменения трансляции и вращения. При этом команды gr gs всякий раз восстанавливают исходное графическое состояние и затем снова его запоминают.
Описанная выше программа листка, так же как и круговой градиент цвета, уже использовались в примере 5.1. Сейчас мы подробно изучаем как это работает. В более современных языках программирования, включающих графический пакет, градиент цвета делается одной командой, но только линейный градиент цвета. В языке постскрипт нет никакой команды градиента цвета, ее надо делать в виде процедуры. Зато мы можем сделать процедуру кругового градиента цвета, такой команды нет ни в одном языке. Итак, какие параметры могут быть у такой процедуры. Очевидно r g b компоненты первого цвета, то есть три числа между нулем и единицей, задающие степени красного, зеленого и синего, далее r g b компоненты второго цвета, а также число переходов и радиус круга. Сделаем так, что первый цвет будет соотвествовать внешней окружности круга, а второй - его центру. Центр будет фиксирован в точке 0,0, так что перемещение придется делать трансляцией координат. Удобно сразу после определения цветов вычислить шаг изменения цветов и потом в цикле каждый раз прибавлять шаг в текущему цвету. Код cgc (circular gradient of color) процедуры выглядит так
/cgc{/R ed /N ed /b2 ed /g2 ed /r2 ed /b1 ed /g1 ed /r1 ed
/r2 r2 r1 sub N div def /g2 g2 g1 sub N div def
/b2 b2 b1 sub N div def /dR R N div def
N{r1 g1 b1 srgb R 0 m 0 0 R 0 360 arc fir /r1 r1 r2 add def
/g1 g1 g2 add def /b1 b1 b2 add def /R R dR sub def}repeat}bd
Хотя здесь достаточно много команд, но они все простые и однотипные. Сначала считываются параметры процедуры, их много, 8 чисел. Затем параметры второго цвета заменяются на шаги по формуле c2 = (c2 - c1)/N, затем вычисляется шаг по радиусу по формуле dR = R/N. И затем в цикле N раз задается первый цвет как текущий, задается контур круга с центром в точке 0,0. Круг заливается цветом. После этого цвет изменяется на приращение, радиус уменьшается на приращение и все повторяется.
Удобно использовать эту процедуру не саму по себе, а в сочетании с заданием контура окна, так что рисование проводится только внутри окна и не проводится вне окна. Такую операцию можно заказать командой clip. В частности вот такая простая программа, использующая процедуры cgc и cir1 (см. выше) позволяет нарисовать объемный шар
/par1{/R ed /y ed /x ed x R add y m}def
/cir1{par1 x y R 0 360 arc}bd
/cgc{/R ed /N ed /b2 ed /g2 ed /r2 ed /b1 ed /g1 ed /r1 ed
/r2 r2 r1 sub N div def /g2 g2 g1 sub N div def
/b2 b2 b1 sub N div def /dR R N div def
N{r1 g1 b1 srgb R 0 m 0 0 R 0 360 arc fir /r1 r1 r2 add def
/g1 g1 g2 add def /b1 b1 b2 add def /R R dR sub def}repeat}bd
%
gs 120 120 tr -20 -20 80 cir1 clip 0 0 0 1 1 1 40 160 cgc gr
Здесь программой является только последняя строка. В ней задается смещение точки 0,0 на 120 по диагонали. Это будет центр кругового градиента. Затем задается контур круга с центром в точке 100 по диагонали, то есть в центре области, и с радиусом 80. Но контур никак не рисуется а используется для задания окна рисования. Затем рисуется круговой градиент за 40 переходов и с радиусом 160. Но мы видим только часть этого круга, которая находится внутри окна рисования. Остальная часть не рисуется. В результате мы получили шар с неравномерной освещенностью, подчеркивающей его объем.
На постскрипте относительно удобно чертить кривые на плоскости, заданные в параметрической форме. Рассмотрим, например кривую, которую называют Декартов лист. Она задается уравнением x3 + y3 = 3axy c единственным параметром a. В параметрическом виде ее можно задать так x = 3at/(1 + t3), y = xt. Из формул сразу видно, что t = -1 является особой точкой, в которой кривая уходит на бесконечность. Поэтому ее разумно разбить на два куска от минус бесконечности до -1.5 и от -0.7 до бесконечности. Кривая два раза проходит точку 0,0. Первый раз при t = 0, второй раз при t равном плюс и минус бесконечности. Соответственно программа ее вычерчивания может иметь такой вид
/px{/x 3 a mul t mul t t mul t mul 1 add div def}def
/py{/y x t mul def} def
/pro{px py x y m N {/t t dt add def px py x y l} repeat sl}bd
100 100 tr
1 slw 1 0 0 srgb 0 -80 0 80 ls -80 0 80 0 ls
-10 25 0 25 ls -20 21 m (a)I
/a 25 def 1.5 slw 0 0 1 srgb
/t -0.7 def /dt 0.1 def /N 300 def pro
/t -1.5 def /dt -0.1 def /N 300 def pro
Здесь процедура px задает переменную x через t, а процедура py - переменную y через x и t. Процедура pro вычерчивает часть кривой и имеет параметры t - начальное значение t, dt - шаг изменения t и N - число точек. Далее задаются конкретные значения параметров и два раза вызывается процедура. Как видим все очень просто. Надо сказать, что в процедуре используется цикл с постоянным шагом, что очень неоптимально для данной зависимости. Можно было бы организовать более сложную процедуру с переменным шагом, тогда понадобилось бы меньше точек.
В научной литературе очень часто приходится чертить графики для представления каких-то зависимостей. Зависимости бывают разные, некоторые из них можно представить функциями одной переменной f(x). Такая функция изображается кривой линией в области, размеченной осями координат. Кривую линию, как правило, аппроксимируют ломаной линией, состоящей из прямолинейных отрезков, а оси координат полностью состоят из прямолинейных отрезков. Таким образом для рисования вполне хватает единственной процедуры ls, которая рисует прямолинейный отрезок между двумя точками по координатам этих точек. Все, что необходимо -- это провести вычисления координат большого числа точек. Для проведения таких вычислений можно написать программу на любом языке программирования, которая просто запишет готовый постскрипт код, состоящий из координат точек и команды ls. Такие программы называются постскрипт генераторами.
Но постскрипт сам по себе достаточно мощный язык программирования, и в нем тоже можно выполнить все вычисления. В данном примере мы напишем процедуры вычерчивания осей координат. Прежде всего надо договориться о структуре входных данных. Мы будем предполагать что точка пересечения осей координат имеет координаты 0,0. Перемещение графика всегда можно сделать с помощью трансляции. Тогда нам необходимо задать следующие общие параметры: lex,lef - длины осей x,f в единицах графика (pt), bo,to - длины коротких и длинных рисок в pt, ts - размер текста для рисования чисел на осях в pt, x1,x2 - минимальное и максимальное значения аргумента в математических единицах, f1,f2 - минимальное и максимальное значения функции в математических единицах. Удобно задание этих параметров организовать в отдельную процедуру, в которой также можно сразу вычислить некоторые нужные константы. Эта процедура имеет следующий вид
/par{ /f2 ed /f1 ed /x2 ed /x1 ed /ts ed /to ed /bo ed /lef ed /lex ed
/str 20 string def /cx lex x2 x1 sub div def /cf lef f2 f1 sub div def
/tcx 0 ts sub to 0 gt {to sub} if bo abs sub def /tcf 0 to 0 gt {to sub} if bo abs 2 mul sub def }def
Здесь первая строка засылает числовые значения в переменные в обратном порядке. Числовые значения будут записаны перед вызовом процедуры. Вторая строка определяет строковую переменную str которая нам понадобится при конвертировании числовых значений в текст командой постскрипта cvs. Затем вычисляются масштабирующие множители cx и cf, которые преобразуют математические единицы в единицы графика. И, наконец, вычисляются координаты смещений текстов с числовыми значениями на осях относительно координат длинных рисок. Итак, первая процедура достаточно простая, но есть одна сложность с положением числовых текстов.
Дело в том, что риски обычно смотрят в сторону от графика (так и задумано), но есть иногда такая необходимость рисовать риски внутрь графика. Некоторые российские научные журналы это требуют. Казалось бы нет проблем, параметры рассчитаны на то, что риски смотрят наружу, но если записать их длину со знаком минус, то они будут смотреть внутрь. Так и есть. Но вот положение числовых текстов тоже зависит от того куда смотрят риски. И тут одной сменой знака не отделаться. Поэтому при вычислении координат текстов используется условный оператор if. Такой код ( to 0 gt {to sub} if ) означает, что если to > 0, то вычитаем to, а если нет, то нет. То есть, если длинные риски смотрят наружу, то тексты надо отодвинуть, а если нет, то не надо.
Следующая процедура размечает конкретно ось x. Нам достаточно задать три параметра: rx - математическое значение у первой длинной риски, drx - шаг до значения следующей длинной риски, nx - число коротких рисок между длинными. Следующая процедура будет считывать эти три значения, определять параметры и проводить все необходимые вспомогательные вычисления. Итак
/parx{ /nx ed /drx ed /rx ed /dr1 drx nx 1 add div def
/r1 rx rx x1 sub dr1 div cvi dr1 mul sub def /n1 x2 r1 sub dr1 div cvi 1 add def
/n2 x2 rx sub drx div cvi 1 add def /r1 r1 x1 sub cx mul def /dr1 dr1 cx mul def
/r2 rx x1 sub cx mul def /dr2 drx cx mul def }def
Рассмотрим, что делает эта процедура. После определения указанных параметров rx,drx,nx она вычисляет шаг между короткими рисками dr1 по формуле dr1 = drx/(n + 1). Теперь надо вычислить начальную координату первой короткой риски. Она равна r1 = rx - int((rx - x1)/dr1)*dr1. Преобразование вещественного числа в целое реализует команда постскрипта cvi. Далее надо вычислить полное число коротких рисок n1 по формуле n1 = 1 + int((x2 - r1)/dr1). Точно так же надо вычислить полное число длинных рисок по формуле n2 = 1 + int((x2 - rx)/drx). Теперь надо начала и шаги коротких и длинных рисок перевести из математических единиц в единицы графика. При этом нам нельзя портить входные данные, поэтому новые начало и шаг для длинных рисок записываются в переменные r2 и dr2. Формулы простые: шаги просто умножаются на коэффициент cx, а из начала надо предварительно вычесть x1.
Итак мы выполнили все необходимые вычисления, пора переходить к рисованию оси x. Это делает третья процедура
/axx{0 0 lex 0 ls
/x r1 def n1{x 0 x bo neg ls /x x dr1 add def}repeat
/x r2 def n2{x 0 x to neg ls /x x dr2 add def}repeat
/x r2 def /v rx def n2{x tcx m ts He v str cvs sc /x x dr2 add def /v v drx add def}repeat }def
Здесь тоже все просто. Сначала в цикле рисуются все короткие риски командой ls. Затем в цикле рисуются все длинные риски той же командой. Наконец, в цикле у длинных рисок проставляются числовые значения. Здесь есть две тонкости. Первая. Числовые значения содержатся в переменной v и определяются через исходные значения в математических единицах. Преобразование числовых значений в текстовую строку реализует команда v str cvs. При этом используется вспомогательная строковая переменная str, которую мы определили в самом начале. Просто так работает постскрипт. А рисуется текстовая строка таким образом, что на координату ставится ее середина, это делает команда sc. Эта команда определена в преамбуле. Размер и тип фонта задает команда He, которая тоже определена в преамбуле.
Итак, мы нарисовали ось x. А ось f рисуется полностью аналогично с заменой в именах переменных буквы x на f. Но так как эти процедуры используют те же вспомогательные переменные, то необходимо сначала полностью нарисовать ось x, а только потом - ось f. Я покажу текст этих процедур без комментариев
/parf{ /nf ed /drf ed /rf ed /dr1 drf nf 1 add div def
/r1 rf rf f1 sub dr1 div cvi dr1 mul sub def /n1 f2 r1 sub dr1 div cvi 1 add def
/n2 f2 rf sub drf div cvi 1 add def /r1 r1 f1 sub cf mul def /dr1 dr1 cf mul def
/r2 rf f1 sub cf mul def /dr2 drf cf mul def }def
/axf{0 0 0 lef ls
/x r1 def n1{0 x bo neg x ls /x x dr1 add def}repeat
/x r2 def n2{0 x to neg x ls /x x dr2 add def}repeat
/x r2 def /v rf def
n2{tcf x ts 3 div sub m ts He v str cvs se /x x dr2 add def /v v drf add def}repeat }def
Важно, что даже если вы не поняли как работают эти процедуры, вы можете смело копировать их текст себе в файл и просто вызывать в своих программах. В этом преимущество процедур. Ниже показано краткое описание вызова процедур. Вместо чисел аргументов указываются их названия. В самой программе должны быть числа.
Процедура par считывает параметры для рисования осей и производит вспомогательные расчеты.
Вызов процедуры: lex lef bo to ts x1 x2 f1 f2 par
Процедура parx считывает параметры для разметки оси x и производит вспомогательные расчеты.
Вызов процедуры: rx drx nx parx
Процедура axx рисует ось x, аргументов не имеет.
Процедура parf считывает параметры для разметки оси f и производит вспомогательные расчеты.
Вызов процедуры: rf drf nf parf
Процедура axf рисует ось f, аргументов не имеет.
Осталось показать текст программы, которая выполняет рисунок, помещенный справа в этом разделе. Я не буду снова повторять все процедуры, они должны предшествовать программе. Как я уже говорил, все процедуры, можно поместить в отдельный файл с названием figpar.psf и просто вызывать этот файл перед текстом программы.
(figpar.psf) run
1 slw 0 0 0 srgb 30 30 tr
160 160 2 4 10 -1 11 -1 11 par 0 5 9 parx axx 0 5 9 parf axf
Рисование осей координат бывает полезным во многих случаях. Оси просто размечают некоторую область плоскости. В этой области можно рисовать карты почернения или цветные карты, разные чертежи и схемы. Но наиболее часто на осях координат изображают график функции в виде кривой линии, точнее ломаной линии, составленной из прямолинейных отрезков. Наиболее распространенный метод - задавать функцию только значениями самой функции на сетке точек аргумента с постоянным шагом. В этом случае все аргументы определяются всего четырьмя числами: первым и последним значениями аргумента a1, a2, числом точек аргумента na и масштабирующим множителем С. Для функции надо задать na значений. Если функция задана аналитической формулой, то эти na значений можно просто вычислить в самой программе. Но это не общий случай и мы его рассматривать не будем. Мы будем предполагать, что na значений функции записаны в массив чисел в текстовом формате. В принципе, числа можно записать в отдельный файл и считывать из файла. Это вариант можно рассматривать лишь как промежуточный. Принято считать хорошим тоном, когда весь рисунок описывается единым файлом. Мы рассмотрим именно такой вариант. Числа в текстовом формате всегда можно скопировать в любом текстовом редакторе. Для сокращения размера файла числа можно задавать как целые, а сама процедура будет умножать их на множитель для получения правильных значений.
Итак, рассмотрим процедуру рисования кривой f(x) ломаной линией. Эта процедура будет иметь своим аргументом массив na чисел значений функции и после него четыре числа a1, a2, na, С. Процедура достаточно простая, поэтому я сразу напишу ее текст, а потом объясню что он означает
/figl{/c ed /na ed /a2 ed /a1 ed /na na 1 sub def /da a1 a2 sub na div def
/a a2 def /v ed a x1 sub cx mul v c mul f1 sub cf mul m
na{/a a da add def /v ed a x1 sub cx mul v c mul f1 sub cf mul l}repeat sl }def
Итак, что мы делаем. Сначала мы считываем множитель и параметры аргумента a1, a2, na в обратном порядке. Затем из na вычитаем единицу и вычисляем шаг изменения аргумента da. Так как значения функции у нас записаны в правильном порядке, а считываться они будут в обратном порядке, то мы должны начинать аргумент с конца, именно так и определен шаг. После этого мы задаем аргументу последнее значение, считываем одно значение функции, умножаем на множитель, делаем преобразование значений из математических единиц в единицы графика и начинаем контур командой m. Затем в цикле увеличиваем (реально уменьшаем) a на da, считываем значение функции, снова умножаем на множитель, делаем преобразование единиц измерения и рисуем фрагмент ломаной командой l. После цикла закрываем контур и рисуем кривую. Все очень просто. Толщину линии и ее цвет можно задать заранее.
Мы можем рассмотреть также альтернативный вариант, когда значения функции показываются не ломаной линией, а маркерами. Для этого подхода нам нужна отдельная процедура, которая рисует маркер. Пусть это будут полые кружки. Тогда процедура пишется так
/mark{/y ed /x ed /R 3 def x R add y m x y R 0 360 arc sl}def
Она зависит от двух параметров, координат центра маркера в графических единицах. Все остальное определено и является свойством маркера. Теперь процедура рисования функции несколько видоизменяется
/figm{/c ed /na ed /a2 ed /a1 ed /da a1 a2 sub na 1 sub div def
/a a2 da sub def
na{/a a da add def /v ed a x1 sub cx mul v c mul f1 sub cf mul mark}repeat }def
Здесь сразу рисуются na точек и в каждой точке вызывается процедура маркера. Очевидно можно сделать столько маркеров, сколько нужно, нет никаких ограничений. Описанные выше процедуры тоже можно поместить в файл figpar.psf. Я хочу обратить внимание, что эти процедуры используют значения переменных cx, x1, cf, f1, которые определялись в процедурах рисования осей. Это и естественно, ведь функция рисуется именно относительно осей координат, а задается в математических единицах.
Иногда бывает так, что функция задана на неравномерной системе точек. И каждая точка описывается двумя парамерами, то есть аргумент и функция. Процедуры для рисования таких массивов точек даже проще. Я просто покажу сразу аналоги для рисования линий и маркеров.
/figla{/c ed /na ed /na na 1 sub def /v ed /a ed a x1 sub cx mul v c mul f1 sub cf mul m
na{/v ed /a ed a x1 sub cx mul v c mul f1 sub cf mul l}repeat sl }def
/figma{/c ed /na ed na{/v ed /a ed a x1 sub cx mul v c mul f1 sub cf mul mark}repeat }def
Эти процедуры имеют только два последних аргумента из четырех для их аналогов, использующих массивы с постоянным шагом. Осталось написать целиком программу показанного справа рисунка
(figpar.psf) run
1 slw 0 0 0 srgb 30 30 tr
160 160 2 4 10 -1 11 -1 11 par 0 5 9 parx axx 0 5 9 parf axf
0.821 1.320 2.019 2.938 4.066 5.353 6.703
7.985 9.048 9.753 10.000 9.753 9.048 7.985
6.703 5.353 4.066 2.938 2.001 1.020 0.221
0 10 21 1 figl
0.5 slw 1 0 0 srgb
0.821 1.320 2.019 2.938 4.066 5.353 6.703
7.985 9.048 9.753 10.000 9.753 9.048 7.985
6.703 5.353 4.066 2.938 2.001 1.020 0.221
0 10 21 1 figm
Последнее замечание: так как процедуры figl, figla и figm, figma полностью вынимают числа из стека, то их приходится ставить в файл дважды. В принципе в постскрипте есть возможность использовать массивы и библиотеки, но я привел минимально сложный для понимания код. Нет никаких проблем два раза скопировать числа. При разумном числе точек файл от этого не намного увеличивается. Но если точек очень много, то их можно записать в файл и использовать команду run.
В разделе 6.5 мы рассмотрели общий принцип вычерчивания осей координат. Там показаны две процедуры, которые чертят нижнюю и левую оси с числами при длинных метках. Однако в реальной жизни возникает необходимость рисовать оси координат самых разных типов. Иногда необходимо рисовать нижнюю и левую оси без чисел. Иногда необходимо рисовать правую и верхнюю оси как с числами, так и без чисел. Соответственно в дополнение к двум указанным процедурам можно написать еще 6 процедур, которые будут похожи на уже показанные с небольшими изменениями. Я придумал для них такие названия axx0 - нижняя ось аргумента без чисел, axx1 - верхняя ось аргумента без чисел, axx2 - верхняя ось аргумента с числами, axf0 - левая ось функции без чисел, axf1 - правая ось функции без чисел, axf2 - правая ось функции с числами. Очевидно, что процедуры для рисования таких осей похожи на уже описанные, только в них надо сделать небольшие изменения. Ниже я покажу текст этих процедур
/axx0{0 0 lex 0 ls
/x r1 def n1{x 0 x bo neg ls /x x dr1 add def}repeat
/x r2 def n2{x 0 x to neg ls /x x dr2 add def}repeat }def
/axx1{0 lef lex lef ls
/x r1 def n1{x lef x bo lef add ls /x x dr1 add def}repeat
/x r2 def n2{x lef x to lef add ls /x x dr2 add def}repeat }def
/axx2{0 lef lex lef ls
/x r1 def n1{x lef x bo lef add ls /x x dr1 add def}repeat
/x r2 def n2{x lef x to lef add ls /x x dr2 add def}repeat
/x r2 def /v rx def
n2{x lef to 2 mul add m ts He v str cvs sc /x x dr2 add def /v v drx add def}repeat }def
/axf0{0 0 0 lef ls
/x r1 def n1{0 x bo neg x ls /x x dr1 add def}repeat
/x r2 def n2{0 x to neg x ls /x x dr2 add def}repeat }def
/axf1{lex 0 lex lef ls
/x r1 def n1{lex x bo lex add x ls /x x dr1 add def}repeat
/x r2 def n2{lex x to lex add x ls /x x dr2 add def}repeat }def
/axf2{lex 0 lex lef ls
/x r1 def n1{lex x bo lex add x ls /x x dr1 add def}repeat
/x r2 def n2{lex x to lex add x ls /x x dr2 add def}repeat
/x r2 def /v rf def
n2{tcf neg lex add x ts 3 div sub m ts He v str cvs s /x x dr2 add def /v v drf add def}repeat }def
Все они тоже записаны в файл figpar.psf, а программа графика, показанного справа выглядит предельно просто
(figpar.psf) run
1 slw 0 0 0 srgb 10 10 tr
160 160 2 4 10 -1 11 -1 11 par
0 5 9 parx axx0 axx1
0 5 9 parf axf0 axf1
То есть просто записывая имена процедур рисования тех или других осей можно сформировать любую конфигурацию осей. При этом общие параметры задаются только один раз и дополнительно один раз - для горизонтальных и вертикальных осей.
Но и это еще не все. Во всех процедурах риски смотрят наружу. Просто я люблю так рисовать. Но можно написать еще 8 процедур, в которых риски будут смотреть внутрь. Тексты таких процедур я показывать не буду, вы сами можете их написать. Если на оси не надо ставить числа, то риски внутрь получаются при задании отрицательных значений для параметров bo,to. Однако в этом случае числовые значения оказываются не на своих местах и их позицию надо скорректировать. Конечно можно было бы написать одну большую процедуру с большим числом параметров, которая бы рисовала все указанные выше случаи. Так удобно делать в программах постскрипт генераторов, но не в самом постскрипте. В постскрипте удобнее сделать много процедур и просто использовать те из них, какие необходимы. При этом код процедур не слишком сложный и использовать их легче.
Иногда приходится изображать функцию, которая изменяется от очень малых до очень больших значений и интересно знать ее изменение как в области больших, так и в области малых значений. Такие функции удобно показывать не в исходном виде, а в виде десятичного логарифма. Но стандартом считается не показывать, что это логарифм функции, а рисовать логарифмическую ось координат. Хотя это совсем не обязательно, но так принято и нам надо иметь процедуру рисования логарифмической оси. На практике такой график можно сделать следующим образом. Сначала честно нарисовать десятичный логарифм функции на обычных осях, но вертикальные оси не показывать. Затем просто пририсовать к графику логарифмические вертикальные оси. Процедура рисования логарифмической оси не столь проста, как описанные выше и для ее реализации надо использовать логические операторы постскрипта. А раз так, то мы сразу сделаем программу, которая рисует две вертикальные оси (левую и правую) или одну ось в зависимости от знака параметра. Эту процедуру лучше всего сделать как независимую, потому что она в принципе должна позволять менять оси у уже готовых графиков, выполненных в другой технике.
Снова будем предполагать, что точка пересечения осей есть точка 0,0. Нам нужны следующие параметры: ширина и высота области графика lex и lef, то есть длины горизонтальной и вертикальной осей в pt, затем длины коротких и длинных рисок bo и to в pt, размер фонта для показа чисел ts в pt, и наконец минимальное и максимальное значения f1 и f2 десятичного логарифма функции, которые соответствуют концам логарифмической оси. Длинные риски ставятся у целых степеней 10, а короткие - у чисел 2,..., 9, так что это однозначно определено. Я сначала покажу всю процедуру, а потом объясню как она работает.
/axlf{
/pro{ x 0 x lef ls /ft f3 def
N{/Cu ft f1 sub cf mul def lef Cu sub Cu mul 0 gt{x Cu m tt 0 rl sl}if /n 2 def
8{/cu n log cf mul Cu add def lef cu sub cu mul 0 gt{x cu m bt 0 rl sl}if /n n 1 add def}repeat
/ft ft 1 add def} repeat }bd
/f2 ed /f1 ed /ts ed /to ed /bo ed /lef ed /lex ed /str 20 string def
/cf lef f2 f1 sub div def /f3 f1 cvi 1 sub def /N f2 f3 sub cvi 1 add def
/x 0 def /tt to neg def /bt bo neg def pro
lex 0 gt {/x lex def /tt to def /bt bo def pro}if
/ft f3 1 add def /N N 1 sub def /tss ts 0.7 mul floor def /s1 ts 24 div def
N{/x s1 36 mul neg def /y ft f1 sub cf mul s1 10 mul sub def x y m ts He (10)se
/x x s1 add def /y y s1 12 mul add def x y m tss He ft str cvs s
/ft ft 1 add def }repeat
}bd
Итак, код у нас перед глазами, начнем разбирать. Эта процедура рисует сразу две оси, обе оси рисуются практически одинаковым образом с небольшими изменениями, а именно горизонтальной координатой оси и длинами рисок, у левой оси они имеют отрицательное значение, у правой - положительное. Поэтому мы внутри своей процедуры организуем еще одну процедуру, которую потом два раза используем. Процедура имеет неуникальное название pro и три параметра x, bt, tt. Это и есть горизонтальная координата оси и длины рисок. Перед вызовом процедуры определяются два параметра: f3 - ближайшее целое число снизу от f1 и N - число длинных рисок + 1. Это число вычисляется как целая часть (f2 - f3) + 1. Сложность в том, что нам надо стартовать с длинной риски, которая находится ниже оси. Между длинными рисками у нас 8 коротких рисок с неравномерным шагом и нам надо сначала игнорировать все короткие риски, которым соотвествуют значения ниже оси. Итак процедура сначала чертит вертикальную черту, затем устанавливает переменной ft значение f3 и делает цикл по N длинным рискам. Сначала делается преобразование ft из математических единиц в единицы графика и результат записывается в переменную Cu. А теперь надо проверить, что координата Cu находится внутри интервала, соответсвующего оси, то есть Cu*(lef - Cu) > 0. Если да, то чертим длинную риску, а если нет, то не чертим. На постскрипте неравенства записываются как и все остальное через стек, то есть сначала надо сформировать в стеке два числа: левую и правую часть неравенства, а затем поставить знак > в виде команды gt. Что и было сделано. После этого в фигурных скобках надо написать процедуру, которая будет выполнена, если выполнится условие. В нашем случае процедура вычерчивает длинную риску. А после процедуры ставится команда if. Таким образом, команда if имеет своими аргументами условие и процедуру. Если условие выполняется, то и процедура выполняется, иначе нет. Вот такая логика.
На самом деле в постскрипте все как в других языках программирования, но из-за того, что нет нормальной памяти, а есть только стек, это накладывает свои ограничения на запись операторов. Итак, длинную риску нарисовали (или нет, как условие скажет), теперь надо рисовать короткие риски. Их 8 и они соответствуют числам от 2 до 9. Как обычно, засылаем в переменную n значение 2 и делаем цикл. В цикле вычисляем относительное положение короткой риски как переход lg(n) (десятичный логарифм пишется как log) в единицы графика и прибавляем к положению длинной риски. Снова проверяем условие и если выполняется, то чертим короткую риску, затем увеличиваем n на 1 и цикл закончен. Во внешнем цикле увеливаем ft на 1 и он тоже закончен. Итак ось нарисована.
После того, как процедура задана, начинается работа. Все очень похоже на то, что было раньше. Считываем аргументы в переменные, определяем строку str, вычисляем масштабирующий множитель, f3 и N. Пора чертить оси. Левую ось чертим сразу задавая x = 0, tt = -to, bt = -bo и вызывая процедуру. А вот правую ось чертим только по условию. Условие такое - если lex < 0, то ось не чертим. А если больше, то задавая x = lex, tt = to, bt = bo вызываем процедуру. Теперь осталось поставить числа в виде 10n у длинных рисок. При этом самую нижную риску (ниже графика) надо исключить. Здесь все просто, только показатель стпени рисуется меньшим размером фонта и немного выше 10. Все остальное точно так же как и при рисовании обычных осей.
Данная процедура тоже записана в файл figpar.psf, программа ее использования выглядит так
(figpar.psf) run
1 slw 0 0 0 srgb 30 30 tr
160 160 2 4 10 -1 11 -1 11 par 0 5 9 parx axx axx1 0 5 9 parf
0.821 1.320 2.019 2.938 4.066 5.353 6.703
7.985 9.048 9.753 10.000 9.753 9.048 7.985
6.703 5.353 4.066 2.938 2.001 1.020 0.221
0 10 21 figl
0.5 slw 1 0 0 srgb
0.821 1.320 2.019 2.938 4.066 5.353 6.703
7.985 9.048 9.753 10.000 9.753 9.048 7.985
6.703 5.353 4.066 2.938 2.001 1.020 0.221
0 10 21 figm
1 slw 0 0 0 srgb
160 160 2 4 10 -2.3 1.4 axlf
Я ее использовал с той же функцией как и выше, но теперь ее надо понимать как логарифм другой функции. Кроме того, есть несоответствие между разметкой осей и значениями функции, но такое соотвествие - на совести пользователя процедуры. По хорощему разметка вертикальной оси логарифма функции должна совпадать с разметкой логарифмической оси. Но, как видите, для графика это не обязательно. Логарифмическая ось рисуется в конце и она совершенно автономна, то есть не зависит от функции.
Но бывают и более сложные ситуации, например, оси надо выставить вокруг карты двумерного распределения функции, которую показывают картинкой черно-белого или цветного контраста. При этом к такой картинке надо добавить линейку равномерного контраста с указанием соответствия цветов значениям функции. Такая линейка может быть горизонтальной и тоже логарифмической. Чтобы ее показать прежде всего надо использовать процедуру горизонтальной логарифмической оси. Ниже такая процедура показана, причем только для вертикальной оси с числами.
/axlx{
/pro{ 0 y lex y ls /ft f3 def
N{/Cu ft f1 sub cf mul def lex Cu sub Cu mul 0 gt{Cu y m 0 tt rl sl}if /n 2 def
8{/cu n log cf mul Cu add def lex cu sub cu mul 0 gt{cu y m 0 bt rl sl}if /n n 1 add def}repeat
/ft ft 1 add def} repeat }bd
/f2 ed /f1 ed /ts ed /tt ed /bt ed /lex ed /str 20 string def
/cf lex f2 f1 sub div def /f3 f1 cvi 1 sub def /N f2 f3 sub cvi 1 add def /y 0 def pro
/ft f3 1 add def /N N 1 sub def /tss ts 0.7 mul floor def /s1 ts 24 div def
N{/y tt bt add def /x ft f1 sub cf mul s1 10 mul add def x y m ts He (10)se
/y y s1 18 mul add def /x x s1 add def x y m tss He ft str cvs s
/ft ft 1 add def }repeat
}bd
Эта процедура также записана в файл figpar.psf, а программа ее использования вместе с линейкой равномерного контраста выглядит так
(figpar.psf) run
0.6 slw 0 0 0 srgb 20 20 tr
170 3 5 10 -4.5 0.5 axlx
70 26 m 12 He (ln(I/I ))s 93 23 m 7 He (0)s gr
/x 190 def /a 0 def 2 slw /b 1 170 div def 170{a sg x 18 x 6 ls /a a b add def /x x 1 sub def}repeat gr
Справа вы видите пример рисунка, выполненного по этой программе.