В прошлой главе широко использовался метод создания процедур с параметрами, которые затем можно многократно и эффективно использовать, даже не вникая в их код. В этом разделе данный прием будет продолжен. Мы рассмотрим некоторые достаточно сложные методы программирования. При этом я буду не только сам придумывать код, но и использовать чужой код, хотя и в переработанном виде. В качестве введения я хочу обсудить одну проблему, которая может приводить к ошибкам и недоразумениям. Хотя, как я уже отмечал, в постскрипте можно работать с разными словарями определений, но если код не очень большой и сложный, то достаточно работать в одном словаре, а именно, в том, который запускается по умолчанию. Но тогда надо следить за тем, чтобы новые определения имели имена, не совпадающие с уже введенными определениями. Так в файле exam.eps уже введены определения, которыми я сам постоянно пользуюсь. Эти имена нельзя использовать в качестве имен новых определений, иначе команды будут переопределены и ими нельзя будет пользоваться. Для справки я перечислю здесь все эти имена в алфавитном порядке:
bd, c, ds1, ds2, ds3, ds4, ds5, ed, f, fir, fr, gr, gs, ib, ind, is1, is2, is3, is4, is5,
it, l, ls, m, np, pi, r, rl, rm, s, sc, se, sca, sg, sl, slw, sp, srgb, tr, B, Bc, G, H,
He, I, L, Lc, Le, PB, PI, PR, Sc, Sy
Ну и надо следить за тем, чтобы имена новых процедур не совпадали с именами самих операторов постскрипта. В постскрипте очень много операторов, но речь идет только о тех операторах, которые вы знаете и которыми пользуетесь. Что же касается временных переменных и процедур, то они могут переопределяться много раз без всякого ущерба для результата работы программы.
На бескрайних просторах интернета, я нашел ps файл, в котором был записан код рисунка в виде красивого текста. Код был записан в виде законченного рисунка и использовал словарь и длинные имена операторов постскрипта. Я переписал этот код в виде процедуры с параметрами, отказавшись от словаря и поменяв имена команд постскрипта на имена моих процедур, указанных выше. Эта процедура рисует красивый текст и называется btxt (beautiful text). Вид этого красивого текста можно изменять в зависимости от параметров. Параметрами процедуры в порядке записи являются логическая переменная rev, имеющая два значения true или false, затем rc, gc, bc - три компоненты цвета, далее DX и DY - координаты вектора смещения, далее ts - размер фонта в pt, tf - имя фонта (например /Courier) и наконец сам текст в круглых скобках, который будет показан. Все параметры более или менее понятны кроме логического параметра rev. Так вот, процедура рисует объемные буквы с неравномерным освещением боковых сторон. Если задано значение true, то свет падает сверху, а если false, то он падает снизу. Я сразу покажу текст процедуры, а результат работы программы, использующей эту процедуру показан на рисунке справа
/btxt{
/txt ed /tf ed /ts ed /DY ed /DX ed /bc ed /gc ed /rc ed /rev ed
/miang DY DX atan def /maang DY DX atan 180 add def
/step 1 64 div def
/mv{/y0 ed /x0 ed /x x0 def /y y0 def np}bd
/lnn{/yy ed /xx ed yy y sub xx x sub
1 index 0 eq 1 index 0 eq and not
{atan /a ed rev
{a miang le a maang ge or}
{a miang ge a maang le and} ifelse
{a 45 sub 2 mul cos 1 add 2.4 div sg x y m
xx yy l DX DY rl x DX add y DY add l closepath fill} if
} if /x xx def /y yy def }bd
/cr{/y4 ed /x4 ed /y3 ed /x3 ed /y2 ed /x2 ed /x1 x def /y1 y def
/cx x2 x1 sub 3 mul def /cy y2 y1 sub 3 mul def
/bx x3 x2 sub 3 mul cx sub def /by y3 y2 sub 3 mul cy sub def
/ax x4 x1 sub cx sub bx sub def /ay y4 y1 sub cy sub by sub def
step step 1
{/t ed t t mul t mul ax mul t t mul bx mul add
t cx mul add x1 add t t mul t mul ay mul
t t mul by mul add t cy mul add y1 add lnn } for }bd
/cl{x0 y0 lnn np }bd
ts tf f
0 0 m txt false charpath
rev {reversepath} if { mv } { lnn } { cr } { cl } pathforall
rc gc bc srgb
0 0 m txt true charpath fill
}bd
Указанная процедура наглядно показывает, что в постскрипте существует аппарат работы с фонтами, то есть буквы текста можно писать разным фасонным образом. Но для понимания работы данной процедуры, информации, описанной в данной книге недостаточно. Нужно обращаться к более солидным изданиям, в которых описаны все операторы языка Постскрипт. Я попробую здесь восполнить этот пробел. Итак ключевым оператором для работы с текстовыми фонтами является оператор charpath
string bool charpath
Оператор charpath имеет два аргумента, первый - это строка текста string, второй - логическая переменная bool. Он определяет контуры букв текста, записанных в первом аргументе так как будто они были бы нарисованы в текущей позиции, но не рисует буквы, а добавляет контуры к текущему контуру в словарь контуров. Логическая переменная регулирует процесс создания контура. Если она равна false, то контур добавляется незамкнуто и его можно вычерчивать. Если она true, то контур добавляется замкнуто и его можно заполнять цветом или использовать для задания окна видимости (clip). Еще один новый оператор
reversepath
без аргументов просто переворачивает направление контура. И наконец третий оператор pathforall,
move line cirve close pathforall
Как я понял из описания, этот оператор берет контуры из стека и последовательно выполняет 4 процедуры, являющиеся его аргументами, для того, чтобы получить модифицированный контур. Первая процедура задает начало модифицированного контура, вторая как бы чертит линию, третья чертит кривую и четвертая закрывает контур. Работает это так. Оператор берет последовательно элементы заданного контура и засылает их в стек, после чего выполняет одну из 4-х процедур по очереди. В нашей процедуре аргументами оператора являются процедуры mv, lnn, cr, cl, которые определены до вызова данного оператора. Видно, что mv действительно открывает новый контур, а вот lnn уже закрывает контур и заливает его серым цветом, яркость которого зависит от угла а. Процедура cr снова вызывает lnn после довольно сложного пересчета координат и наконец cl снова вызывает lnn, после чего открывает новый контур, тем самым закрывая старый.
Нет смысла описывать конкретные расчеты, которые приводятся в процедурах, любой желающий сам может это разобрать. Программу писал не я, так что для меня детали неизвестны. Но общий принцип ясен. Сначала берутся контуры букв, к ним пририсовываются боковые стенки, которые по разному заштиховываются в зависимости от угла наклона контура. А потом снова берутся контуры букв, но уже заливаются заданным цветом. Осталось показать пример программы, которая выполнила рисунок. Вот как это вызывалось
gs 15 40 tr true 1 0 0 -5 3 74 /Times-Bold (Kohn) btxt gr
gs 15 120 tr false 1 0 0 5 3 74 /Times-Bold (Kohn) btxt gr
Если нужно много текстов рисовать одним фасоном, то можно определить промежуточную процедуру, в которой задать все параметры, кроме самих тестов и координат установки текстов.
В заключение этого раздела хочу сказать, что указанная выше процедура делает очень красивый текст с переливами, но она ограничена только латинскими фонтами. Так как русских фонтов в стандартной поставке постскрипта нет, то для русских текстов это не работает. Если иметь в виду мой русский фонт /Cyr30vk, то программа работает с ним некорректно, поскольку у меня буквы рисуются несколькими линиями и нет четкого контра. Но можно придумать другую, более простую процедуру, в которой буквы текста рисуются несколько раз со смещением. Смещенные буквы рисуются серым, а основные - заданным цветом. Ниже показан ее код вместе с программой вызова
% (cyr30fnt.psf) run
/vtxt{/txt ed /N ed /DY ed /DX ed /bc ed /gc ed /rc ed
/dx DX N div def /dy DY N div def /x DX def /y DY def 0.6 sg
N{x y m txt s /x x dx sub def /y y dy sub def}repeat
x y m rc gc bc srgb txt s }bd
20 slw 74 /Cyr30vk f
gs 15 40 tr 1 0 0 -5 3 10 (Filq) vtxt gr
gs 15 120 tr 1 0 0 5 3 10 (Filq) vtxt gr
Я назвал эту процедуру vtxt (volume text), она имеет агрументами r,g,b компоненты цвета, DX,DY - координаты смещения, N - число копий и сам текст. Размер и тип фонта, а также координаты установки текста заказываются заранее. Эта процедура тоже делает объем, но без направленного освещения, то есть освещение равномерное как в тумане. Зато, она может работать с любыми фонтами, в том числе и с моим русским фонтом Cyr30vk. Но этот фонт надо заранее определить. В программе это сделано включением постскрипт кода из файла cyr30fnt.psf.
Здесь речь пойдет о процедуре распечатки календаря на любой месяц любого года. Опять же оригинальную программу я нашел среди тысяч ps файлов, скачанных из интернета. С какого сайта я ее скачал уже не восстановить. Файл называется dodecale.ps, а в тексте файла указано имя ее автора (Oskar Schirmer) и год создания (2000). Программа чертит выкройку 12-гранника. Если распечатать выкройку на бумаге и склеить как показано, получается 12-гранник, на каждой стороне которого есть календарь на один месяц заданного года. Это довольно интересный проект, но я уже давно не пользуюсь бумажными носителями, так что не интересно захламлять стол безделушками. Поэтому я выделил из программы код распечатки календаря на любой месяц любого года и сделал процедуру с двумя аргументами: число месяца и года. Хотя исходный код печатает календарь только латинскими буквами, я разделил код на две части. Первая часть может быть настроена на любой фонт и в том числе на мой фонт Cyr31vk, который отличается от предыдущей версии Cyr30vk тем, что к нему добавлены символы пробела и цифры от 0 до 9. Я начну с того, что приведу обе процедуры для английской версии календаря, а также вызывающую их программу, которая выполнила рисунок, показанный справа.
/caldata{
/FT{0 0 1 srgb 5 /Helvetica-Bold f}bd
/Name [(January) (February) (March) (April) (May) (June)
(July) (August) (September) (October) (November) (December)] def
/Day{(Sa) (Fr) (Th) (We) (Tu) (Mo) (Su)
7 {dup sp neg Y -.15 mul rm s} repeat }bd
}bd
/calendar{
96 96 tr 3 Sc
/Year ed /AM ed /AM AM 1 sub def
/HN{0 0 0 srgb 5 /Helvetica f}bd
/HB{0 0 0 srgb 5 /Helvetica-Bold f}bd
/X 50 36 sin mul def
/Y 50 36 cos mul def
/NY Year 4 string cvs def
/M1st Year 400 mod dup 4 idiv dup 25 idiv sub add 3 add 7 mod def
/F29 Year dup 100 mod 0 eq {100 idiv} if 4 mod 0 eq def
/Strt {dup 2 ge {2 sub 30.6 mul .5 add floor cvi}
{0 eq {-59} {-28} ifelse F29 {1 sub} if} ifelse} def
/ZS 2 string def
/In {false exch {2 index eq or} forall exch pop} def
/InM {false exch {aload pop 3 index eq exch 4 index eq and or} forall
exch pop exch pop} def
/Easter
/Eag Year 19 mod 1 add def
/Eac Year 100 idiv 1 add def
/Eax 3 Eac mul 4 idiv 12 sub def
/Eaz 8 Eac mul 5 add 25 idiv 5 sub def
/Ead 5 Year mul 4 idiv Eax sub 10 sub def
/Eae 11 Eag mul 20 add Eaz add Eax sub 480 add 30 mod def
Eae 25 eq Eag 11 gt and Eae 24 eq or {/Eae Eae 1 add def} if
/Ean 44 Eae sub def
Ean 21 lt {/Ean Ean 30 add def} if
Ean 7 add Ead Ean add 7 mod sub
def
FT Name AM get dup sp ( ) sp add NY sp add -2 div Y .55 mul m s ( ) s NY s
X -.7 mul Y .5 mul m Day
/Z 355 AM Strt M1st add sub 1 add 7 mod 6 sub def
/L AM 1 add Strt AM Strt sub def
6 {X .3 mul Y 1.05 mul rm 0 1 6 {/Z Z 1 add def
Z 0 gt Z L le and {0 eq
AM Strt Z add Easter sub [-2 0 1 49 50] In or
AM Z [[0 1] [4 1] [11 25] [11 26]] InM or
{HB} {HN} ifelse
Z ZS cvs dup sp neg Y -.15 mul rm s}
{pop 0 Y -.15 mul rm} ifelse
} for} repeat
}bd
caldata
1 2010 calendar
Итак, как видно из программы, сначала один раз вызывается процедура caldata, а затем можно много раз выхывать процедуру calendar с аргументами. Вторая процедура сделана таким образом, что календарь по умолчания вписывается в квадрат со стороной 200 pt. Если нужно по другому, то объект можно передвинуть, изменить размеры и повернуть на любой угол обычным образом. Прежде всего, обсудим общие принципы. Календарь рисуется тремя фонтами. Первые два изображают цифры обычных дней (HN) и воскресных дней (HB). Они стандартные и задаются в самой процедуре calendar. В принципе любой желающий может изменить определения этих фонтов в самой процедуре. Третий фонт (FT) используется для изображения месяца, года и дней недели. Он как раз задается в процедуре caldata и будет разным для английской и русской версий. При определении каждого из фонтов можно задать цвет букв. Я задал красный цвет для выходных дней, черный - для обычных дней и синий - для слов. В исходной программе все было черным, так как предполагалось для распечатки на черно-белом принтере. Но при показе на экране это уже не обязательно. Далее, в процедуре caldata определяется массив из 12 элементов с названиями месяцев и процедура рисования дней недели.
Перейдем к самой процедуре calendar. В ней сначала задаются преобразования координат, определения переменных через аргументы, и определения некоторых процедур. Расчеты делаются многоступенчато, при желании их можно расписать в обычных обозначениеях, но я не стал это делать. Сама процедура рисования начинается со строки "FT Name AM get ...". Здесь заказывается фонт заголовка, затем выбирается слово с индексом AM из массива слов Name, вычисляется длина текста заголовка и затем сам текст ставится серединой в точку x = 0 и с координатой y, которая вычисляется. После этого вычисляются координаты начала для распечатки слов дней недели и они распечатываются. Последующий текст является более сложным, он вычисляет в каком месте надо печатать числа лней месяца и каким фонтом. Этот текст я тоже не разбирал, это не так важно, поскольку он работает без ошибок. Вот и все. В отличие от предыдущего примера здесь нет неизвестных операторов, но есть очень сложные расчеты.
Сейчас посмотрим как можно сделать календарь с русскими текстами. В том русском фонте, какой у меня был раньше не было пробела и цифр. Но так как тут надо задавать заголовок сразу одним фонтом, то пришлось добавить пробел и цифры. Новый фонт я назвал Cyr31vk. При работе с ним необходимо его заказать вызвав текст из файла (cyr31fnt.psf) с помощью команды run. Если сами процедуры и программа записаны в файл, то надо заказывать оба файла один за другим. Поэтому в распечатке процедур я эту строку обычно комментирую. Процедура calendar никак не меняется, нужно только изменить процедуру
% (cyr31fnt.psf) run
/caldata{
/FT{0 0 1 srgb 20 slw 5 /Cyr31vk f}bd
/Name [(Qnwarx) (Fewralx) (Mart) (Aprelx) (Maj) (I@nx)
(I@lx) (Awgust) (Sentqbrx) (Oktqbrx) (Noqbrx) (Dekabrx)] def
/Days{
(Wo) dup sp neg Y -0.15 mul rm s
(Po) dup sp neg Y -0.15 mul rm s
(Wt) dup sp neg Y -0.15 mul rm s
(Sr) dup sp neg Y -0.15 mul rm s
(=e) dup sp neg Y -0.15 mul rm s
(Pq) dup sp neg Y -0.15 mul rm s
(Su) dup sp neg Y -0.15 mul rm s }bd
}bd
Здесь по другому заказывается фонт FT, при использовании русского фонта русские буквы кодируются латинскими через таблицу соответствия, которая обсуждалась в главе 5.3 данной книги. Принцип простой - русским буквам ставятся в соответствие латинские с таким же звучанием или с близким написанием (я через q), а дополнительные знаки кодируются как получилось. В процедуре Days отсутсвует цикл. Увы, почему то мой фонт неправильно работает в цикле, во всяком случае программа Gsview делает ошибку и не может выполнить рисунок. Я потратил огромное время, чтобы понять причину. Если циклы не использовать, то все работает нормально. Я так и не понял кто тут виноват. Либо я плохо написал фонт, либо используемая мной версия Gsview содержит ошибки. Но так как я сам с ней работаю и другим предлагаю ее скачать, то мой фонт лучше использовать без циклов.
В разделе 3.2 при обсуждении оператора if я показал процедуру fact вычисления факториала. Особенностью этой процедуры являлось то, что в определении процедуры был вызов самой процедуры. Такой прием называется рекурсией. В обычных компилируемых языках это, как правило, запрещают. В интерпретируемых языках это возможно, но все зависит от того, как интерпретатор на это реагирует и как он обрабатывает процедуры. Так в моем языке ACL (подробности на сайте vkacl.narod.ru) рекурсия запрещена. Интерпретатор отслеживает рекурсию, и если она происходит, то выдается сообщение об ошибке. Это сделано из соображений простоты понимания кода. В языке Постскрипт она не запрещена. Это значит, что процедура может вызывать сама себя, она может вызывать другую процедуру, которая, в свою очередь, вызывает ту, которая ее вызвала и так далее.
Рассмотрим, например, что происходит, если процедура вызывает саму себя. Это выглядит так, что процедура выполнила часть своего кода, а потом, не закончившись начинает выполнять свой код с самого начала. То есть это очень похоже на цикл loop по условию. Ясно, что рекурсивный вызов процедуры не может происходить просто так, иначе программа никогда не закончит. Это может делаться только по условию, которое несколько раз выполняется, а потом не выполняется. А это значит, что рекурсивный вызов процедуры можно оформить в виде двух процедур. Первая часть вызывается в цикле если условие выполнено, а вторая часть - если условие не выполнено. Так и надо понимать рекурсивный вызов процедуры. Но когда цепочка процедур достаточно длинная, то есть 1-я процедура вызывает 2-ю, та 3-ю, та 4-ю, та 5-ю, а 5-я снова 1-ю, то понимание такого кода очень затруднено. И тем не менее, существуют алгоритмы, где это оправдано и позволяет очень сильно сократить код всей программы в целом.
В этом разделе я покажу код программы с рекурсиями. Этот код очень короткий, но рисунок, который он выполняет очень сложный. Он выглядит как ветка с листочками и состоит из двух объектов: палочек и листочков. Однако в зависимости от единственного параметра и палочек и листочков может быть очень много, они в буквальном смысле лавинообразно размножаются, то есть также как факториал. Я сначала покажу весь код, а потом попробую объяснить кое какие его детали. Код не мой, я нашел его в интернете, но переписал на свои определения команд, после чего он стал намного короче. Итак, вот код
/cur{currentpoint}bd /rc{rcurveto}bd
/knopr{ray gs knop gr gs -22.5 r knop gr ray gs 22.5 r ray knop gr -22.5 r knop}bd
/rayr{ray ray}bd
/knop{dup 0 eq
{cur 3 3 3 3 6 0 rc -3 -3 -3 -3 -6 0 rc 0 0.7 0 srgb fill m}
{dup 1 sub knopr pop} ifelse }bd
/ray{dup 0 eq
{6 0 rl 0.2 0.2 0 srgb cur sl m}
{dup 1 sub rayr pop} ifelse}bd
/pro{Sc 1 0.8 sca 0.5 slw 10 5 m 45 r knop pop sl}bd
3 2 pro
% 4 1 pro
% 5 0.5 pro
Здесь в самом начале вводятся два дополнительных более коротких определения операторов постскрипта cur вместо currentpoint и rc вместо rcurveto. Оператор currentpoint в этой книге не описывался. Он делает следующее: берет координаты последней точки текущего контура и помещает их в стек. Если эти координаты задавались в явном виде, то без труда можно их задать еще раз. Например, 0 0 m 6 0 l 6 0 sl. После выполнения этой программы будет вычерчен отрезок, а координаты последней точки останутся в стеке. То же самое можно было бы написать по другому 0 0 m 6 0 l cur sl. Здесь я использовал уже новое определение оператора currentpoint и координаты последней точки контура поместил в стек неявным образом. Это помогает в тех случаях, когда координаты точек контура вычисляются очень сложным образом. Конечно это имеет смысл, если есть желание начать новый контур с конца старого контура. Казалось бы можно было бы просто не закрывать контур. Но иногда например надо часть контура покрасить синим, а другую часть красным. Тогда контур надо закрыть и потом снова открыть в той же точке. Оператор rcurveto описан ранее в разделе 2.3.
Далее описаны 5 процедур. Главной является процедура pro. Название не уникальное, если необходимо, то можно поменять. В наших рисунках кроме этой процедуры ничего больше не вызывается. Процедура pro имеет два параметра: первый характеризует число объектов, но не прямо, реально число объектов растет примерно как факториал этого параметра, второй задает масштабирующий множитель, чтобы размер рисунка в единицах pt не менялся. Так рисунок, показанный справа, выполнен вызовом 3 2 pro. Сама процедура pro вызывает процедуру knop. А процедура knop имеет оператор ifelse и две процедуры. Одна из них рисует листок (первый объект), вторая вызывает процедуру knopr. Последняя несколько раз вызывает процедуру ray и снова процедуру knop. Процедура ray тоже имеет оператор ifelse и две процедуры: первая рисует палочку (второй объект), а вторая вызывает процедуру rayr, которая просто два раза снова вызывает процедуру ray.
Таким образом все ясно. Весь процесс состоит в постоянной проверке условий и в зависимости от выполнения условия, либо рисуется один из двух объектов, либо процесс продолжается дальше. Весь фокус в том, чтобы записать условия таким образом, чтобы процесс рано или поздно прекратился. В этом и состоит искусство программирования и все определяется разработанным алгоритмом, который может быть достаточно сложным. Алгоритм данной программы я разбирать не буду, не я писал этот код, я нашел его в интернете. А результат работы программы легко проверить. Даже при числе 3 на рисунке очень много объектов. Если поставить число 4, то их становится существенно больше (рисунок получен вызовом 4 1 pro и показан под первым), а при числе 5 - еще больше (рисунок получен вызовом 5 0.5 pro и показан самым нижним). Интересно, что исходный размер рисунка все время увеличивается, поэтому его приходится масштабировать в сторону уменьшения. Если рисовать такой рисунок без рекурсии, то кода понадобится очень много. Правда можно было бы использовать циклы loop и все записать без рекурсии. Но с рекурсией полачается намного эффективнее.
В постскрипте нет никаких проблем нарисовать прямолинейный отрезок между двумя точками. Ломаную линию, проходящую между серией точек можно нарисовать последовательно, отрезок за отрезком. Но иногда это неудобно, особенно при автоматическом генерировании постскрипт кода. Хотелось бы такой вариант, чтобы все точки вперед, а потом постскрипт процедура. Это тоже легко сделать с использованием цикла, но тогда придется задавать обратный порядок точек, то есть первые в конце, ведь постскрипт работает со стеком - последний пришел, первый ушел. Но это тоже неудобно, хотелось бы чтобы точки задавались в прямом порядке. И это возможно, ведь есть команда index. Но при этом после задания всех точек надо еще задать дополнительно число отрезков, которые эти точки определяют. Ну и желательно задавать не сами точки, а разности от данной точки до предыдущей для всех точек кроме первой. В таком варианте легко масштабировать объект. Ниже показана процедура, выполняющая описанную выше работу. Процедура называется mrlc (multi relative line contour). Вот ее текст
/mrlc{/n1 ed /n n1 1 add 2 mul 1 sub def 2{n index}repeat
m n1{/n n 2 sub def 2{n index}repeat rl}repeat}bd
Рассмотрим что здесь делается. Итак, сначала считывается число из стека и приравнивается переменной n1. Затем вычисляется n=(n1+1)*2-1. Это есть номер первого числа записанного в стек глубоко и отсчитываемого от нуля. Команда 2{n index}repeat берет два первых числа из глубины стека и ставит их в конец стека. После этого команда m считывает эти два числа, открывает новый контур и переносит перо в начальную точку. Осталось n1 точек, они обрабатываются в цикле n1 раз. Каждый раз понижается n на 2 (два числа исчезают из стека при каждой команде рисования), потом два числа из глубины стека перемещаются в конец и считываются командой rl. Вот и все, контур задан. Теперь можно нарисовать кривую, а если контур замнутый, то закрасить область.
Рассмотрим теперь более сложную задачу. Пусть нам задан контур по тем же точкам и он замкнутый. Нам надо все острые углы заменить на сглаженные кривые. Для этого можно использовать команду постскрипта /rc{rcurveto}bd. Правда непосредственно в преамбуле данная команда не определена, так что ее надо определить прямо в процедуре. Итак, пусть мы знаем точки контура и число отрезков. У замкнутого контура столько углов, сколько и отрезков. И мы хотим сгладить каждый угол кривой, которая начинается от середины отрезка, предшествующего углу, имеет касательную, совпадающую с отрезком, она заканчивается на середине отрезка, который следует за углом и снова имеет касательную, совпадающую с отрезком. Таким образом нам надо из начальной точки переместиться в середину первого отрезка и запомнить начальную точку, затем надо обработать все углы, а для последнего угла начальная точка будет конечной. При этом контур не пройдет ни через одну задаваемую точку, но зато будет совпадать с ломаным контуром на середине отрезков. Что важно, мы хотим, чтобы структура задаваемых точек была точно такой же, как и для описанной выше процедуры.
Такая процедура имеет более сложный код, но в нем нет принципиальных сложностей. Назовем эту процедуру smrlc (smoothed multi relative line contour). Сначала я сразу покажу весь ее код
/co2{/o2 ed /o1 ed o1 o2 o1 o2}bd /stm{x3 y3 rm}bd /rc{rcurveto}bd
/spt{co2 /y0 ed /x0 ed}bd /sman{x3 y3 co2 x4 y4 rc}bd
/ipt{/y2 ed /x2 ed /y1 ed /x1 ed /x3 x1 2 div def /y3 y1 2 div def
/x4 x2 2 div x3 add def /y4 y2 2 div y3 add def}bd
/ptu{2{n index}repeat}bd /ptua{/n n 2 sub def ptu}bd
/smrlc{/n1 ed /n n1 1 add 2 mul 1 sub def ptu m ptua spt ptu ipt
stm sman n1 2 sub{x2 y2 ptua ipt sman}repeat x2 y2 x0 y0 ipt sman}bd
Теперь я поясню что здесь делается. Для более компактной записи основной процедуры введены вспомогательные процедуры. Это помогает лучше разобрать код. Итак первая процедура co2. Она берет из стека два числа и возвращает в стек два раза по два тех же самых числа. Процедура stm делает начальное смещение по координатам x3 y3. Процедура rc - это просто более короткая запись команды. Процедура spt запоминает стартовую точку. Для этого она удваивает два последние числа и потом считывает одну пару в переменные x0 и y0. Процедура sman чертит саму кривую по трем точкам, заданным относительными координатами (отрезками). При этом она удваивает среднюю точку и в качестве начальной точки использует текущую точку. Кривая вычерчивается командой rc, а все, что перед ней, определяет ее аргументы. Процедура ipt делает более сложную работу: она по координатам пары исходных отрезков, заданных 4-мя числами в стеке, вычисляет отрезки на середину исходных отрезков. При этом сначала 4 числа считываются в переменные x1, y1, x2, y2. А затем вычисляются переменные x3, y3, x4, y4. При этом x3 и y3 получаются просто делением x1 и y1 на 2, а x4 и y4 вычисляются точно так же, только к ним добавляются еще уже определенные отрезки x3 и y3. Так необходимо для правильного использования процедуры rc. Процедура ptu нами уже обсуждалась. Она просто перемещает два числа из глубины в конец стека используя переменную n. А процедура ptua делает то же самое, но еще перед этим уменьшает n на 2.
Осталось описать главную процедуру. Она поначалу работает так же, как и предыдущая процедура, то есть считывает n1 и вычисляет переменную n. Затем она перемещает первые два числа из глубины стека в конец и считывает их командой начального перемещения Далее она перемещает координаты длины первого отрезка, удваивает их и считывает одну пару в переменные x0 и y0. Их надо запомнить для обработки последнего угла. После этого она перемещает еще одну пару чисел и по двум отрезкам закругляет первый угол. Промежуточные углы числом n-2 закругляются в цикле. Задается вторая из исходных точек предыдущего шага, затем перемещается новая исходная точка, вычисляются середины отрезков и вычерчивается кривая. Последний угол закругляется непосредственно, для него все точки известны. Вот и все.
На картинке показаны две кривые, нарисованные описанными процедурами
1 slw 50 50 0 100 100 0 0 -100 -100 0 4 mrlc sl
2 slw 50 50 0 100 100 0 0 -100 -100 0 4 smrlc sl
Процедура smrlc пригодна для сглаживания только замкнутых контуров. Но иногда представляет интерес сглаживать и незамкнутые кривые линии. Для этого можно использовать другую процедуру, код которой показан ниже
/co2{/o2 ed /o1 ed o1 o2 o1 o2}bd /rc{rcurveto}bd
/sman{x3 y3 co2 x4 y4 rc}bd
/ipt{/y2 ed /x2 ed /y1 ed /x1 ed /x3 x1 2 div def /y3 y1 2 div def
/x4 x2 2 div x3 add def /y4 y2 2 div y3 add def}bd
/ptu{2{n index}repeat}bd /ptua{/n n 2 sub def ptu}bd
/smrsl{/n1 ed /n n1 1 add 2 mul 1 sub def ptu m ptua ptu ipt x3 y3 rl sman
n1 2 sub{x2 y2 ptua ipt sman}repeat x2 2 div y2 2 div rl}bd
Эта процедура называется smrsl (smoothed multi relative segment line). Она тоже использует вспомогательные процедуры, только здесь процедура начального смещения на середину первого отрезка заменена на процедуру вычерчивания кривой и вставлена прямо в текст основной процедуры. А запоминать первую точку не надо, поэтому процедуры spt нет совсем. Сразу рассмотрим основную процедуру, так как вспомогательные процедуры уже описаны выше. В начале процедуры снова запоминается n1 и вычисляется n, затем делается перемещение в начальную точку. Далее сразу считываются два отрезка, вычилсяются их середины и после этого чертится прямая линия в середину первого отрезка. Затем точно так же, как и выше обрабатываются все внутренние углы и в конце черится прямая из середины последнего отрезка в его конец. Таким образом отличие существует только в обработке первого и последнего отрезков.
На картинке показаны две кривые, нарисованные описанными процедурами
1 slw 50 50 0 100 50 0 0 -100 50 0 4 mrlc sl
2 slw 1 0 0 srgb 50 50 0 100 50 0 0 -100 50 0 4 smrsl sl