Вместо обложки
Титульный лист оригинального издания
Титульный лист
Аннотация и выходные данные
Содержание
Посвящение
Предисловие
1 О названии игры
2 Координаты
3 Кривые
4 Перья
5 METAFONT в действии
6 Как METAFONT считывает входные данные
7 Переменные
8 Алгебраические выражения
9 Уравнения
10 Присваивания
11 Увеличение и разрешение
12 Рамки
13 Рисуем, заливаем и стираем
14 Пути
15 Трансформации
16 Элементы каллиграфии
17 Группирование
18 Определения, или макросы
19 Условия и циклы
20 И снова о макросах
21 Случайные числа
22 Цепочки
23 Вывод на экран
24 Дискретность и дискретизация
25 Обзор выражений
26 Обзор языка системы
27 Как исправлять ошибки
Приложения
А Ответы ко всем упражнениям
В Базовые определения
С Коды символов
D Нестандартные приемы
Е Примеры
F Метрики шрифтов
G Файлы шрифтов \
Н Пробные оттиски
I Предметно-именной указатель
J Общество пользователей TeX
Выходные данные
Текст
                    Все про METAFONT


The METAFONTbook DONALD E. KNUTH Stanford University Illustrations by DUANE BIBBY ADDISON-WESLEY PUBLISHING COMPANY Reading, Massachusetts Menlo Park, California New York Don Mills, Ontario Wokingham, England Amsterdam • Bonn Sydney • Singapore • Tokyo Madrid • San Juan
Все про METAFONT ДОНАЛЬД Э. КНУТ Станфордский университет Москва • Санкт-Петербург • Киев 2003 |ГТ)П
ББК 32.973.26-018.2.75 К53 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией С. Н. Тригуб Перевод с английского М. Р. Саитп-Аметпова Под редакцией М. Р. Саитп-Аметпова Научный консультант докт. физ.-мат. наук, проф. Ю. В. Казаченко По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу: infoevilliamspublishing.com, http://www.williamspublishing.com Кнут, Дональд Э. К53 Все про METAFONT. : Пер. с англ. — Издательский дом "Вильяме", 2003. — 384 с. : ил. — Парал. тит. англ. ISBN 5-8459-0442-0 (рус.) В книге описывается METAFONT — мощная компьютерная система, предназначенная для разработки высококачественных шрифтов. Язык системы, который по сути является языком программирования высокого уровня, позволяет точно описать любые символы. Руководство представляет собой одновременно и учебник, и наиболее полный справочник по системе METAFONT, разработанной автором книги. Кроме того, книга может служить учебным и справочным пособием по системе METAPOST, язык которой аналогичен языку METAFONT. Книга предназначена для всех, кто интересуется разработкой шрифтов, вопросами допечатной подготовки документов и компьютерной графикой. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Addison-Wesley Publishing Company, Inc. Authorized translation from the English language edition published by Addison-Wesley Publishing Company, Inc, Copyright © 1986 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2003 TeX — зарегистрированная торговая марка American Mathematical Society. METAFONT — зарегистрированная торговая марка Addison-Wesley. Шутка на странице 19 принадлежит Ричарду С. Палайсу. Цитата из Уилкинса на странице 295 предложена Джорджией К. М. Тобин. ISBN 5-8459-0442-0 (рус.) ISBN 0-201-13445-4 (англ.) © Издательский дом "Вильяме", 2003 © American Mathematical Society, 1984, 1986
Содержание Предисловие 8 1 О названии игры 13 2 Координаты 17 3 Кривые 25 4 Перья 33 5 METAFONT в действии 43 6 Как METAFONT считывает входные данные 61 7 Переменные 65 8 Алгебраические выражения 71 9 Уравнения 87 10 Присваивания 99 11 Увеличение и разрешение 103 12 Рамки 113 13 Рисуем, заливаем и стираем 121 14 Пути 135 15 Трансформации 153 16 Элементы каллиграфии 159 17 Группирование 167 18 Определения, или макросы 171 19 Условия и циклы 181 20 И снова о макросах 187 21 Случайные числа 195 22 Цепочки 199 23 Вывод на экран 203
6 Содержание 24 Дискретность и дискретизация 207 25 Обзор выражений 221 26 Обзор языка системы 229 27 Как исправлять ошибки 235 Приложения А Ответы ко всем упражнениям 245 В Базовые определения 269 С Коды символов 293 D Нестандартные приемы 297 Е Примеры 313 F Метрики шрифтов 327 G Файлы шрифтов "общего" формата 335 Н Пробные оттиски 339 I Предметно-именной указатель 357 J Общество пользователей TeX 375
Посвящается Герману Запфу Создаваемые им линии бесподобны
Предисловие ПЕРВЫЕ ПОПЫТКИ воспроизводить начертания символов методом математических построений были предприняты в пятнадцатом столетии; в шестнадцатом и семнадцатом столетиях этот метод получил распространение, а на протяжении восемнадцатого столетия от него по ряду серьезных причин постепенно отказались. Возможно, именно теперь, когда математика продвинулась вперед, а вычисления выполняются с помощью компьютеров, пришло время для возрождения этой идеи. В современных растровых устройствах печати металлическая матрица заменена абстрактными математическими таблицами из нулей и единиц, которые определяют точки, куда дискретным образом должны наноситься чернила. Поэтому математика и информатика оказались теснейшим образом связаны с печатью. Теперь мы можем абсолютно точно определять начертания символов и на всех растровых устройствах вывода получать одинаковые результаты. Более того, начертания можно описывать в терминах переменных параметров. Компьютер за считанные секунды генерирует новые символы, поэтому разработчики шрифтов могут ставить такие эксперименты, которые в прошлом были просто немыслимы. METAFONT — это система, позволяющая разрабатывать шрифты, предназначенные для использования растровыми устройствами, печатающими или отображающими текст. Все символы, которые вы сейчас видите, чрезвычайно точно разработаны при помощи системы METAFONT. Разработка была довольно быстро выполнена самим автором, придумавшим эту систему, который в этом деле — простой любитель. Мы не сомневаемся, что с помощью METAFONT в будущем будут созданы поистине прекрасные шрифты. Руководство написано для тех, кто желает внести свой вклад в развитие искусства математического дизайна шрифтов. Первоклассный дизайнер должен иметь чрезвычайно острый глаз и повышенную чувствительность к малейшим изменениям формы. Первоклассный программист должен обладать недюжинным талантом абстрактного мышления и способностью выражать в формальных терминах интуитивные идеи. Очень редко человек сочетает в себе оба эти качества, поэтому наилучшие образцы, созданные с помощью системы METAFONT, вероятнее всего, станут плодом совместных усилий двух людей, способности которых дополняют друг друга. На самом деле эта ситуация не сильно отличается от той, с которой приходилось сталкиваться при разработке шрифтов многим поколениям. Разница лишь в том, что роль искусных резчиков по металлу в наше время играют искусные компьютерщики. Пользователь METAFONT пишет "программу" для каждой буквы или символа шрифта. Эти программы отличаются от обычных компьютерных программ, поскольку имеют скорее декларативный, нежели императивный характер. Вы описываете на языке системы METAFONT, где должны располагаться основные элементы того или иного символа и как они связаны между собой, но вам нет нужды углубляться в детали, например, указывать, где пересекаются линии и т.п. Компьютер берет на себя решение уравнений, которые он последовательно выводит из
Предисловие 9 заданных вами условий. Одно из преимуществ системы METAFONT состоит в том, что, действуя согласно определенным правилам, можно точно описать принципы построения начертаний. Идеи, лежащие в их основе, не остаются в воображении разработчика — они явно выражаются в программах. Поэтому в случае необходимости легко отследить закономерность и дополнить шрифт новыми символами, которые будут хорошо сочетаться с уже существующими. Но даже с помощью такой системы, как METAFONT, невозможно за пару часов создать новый красивый шрифт. Под кажущимися внешне простыми начертаниями букв скрывается бесчисленное множество тонкостей. Это значит, что разработчики высококачественных шрифтов выполнили свою работу настолько профессионально, что мы попросту не замечаем их внутренней сложности. Один из недостатков системы METAFONT заключается в том, что любой человек с ее помощью может создавать без особых материальных затрат большое количество некачественных шрифтов. Будем надеяться, что подобные эксперименты будут поучительны, демонстрируя, как важны в этом тонком деле маленькие хитрости, и мы не будем свидетелями распространения некачественных работ. Теперь кто угодно может напечатать книгу, в которой все шрифты будут самодельными. Но если ставится цель создать шрифт, который будет выглядеть должным образом, то человек или группа, которая берется за осуществление такого проекта, должна быть готова потратить на это не менее года. METAFONT не оставит без работы сегодняшних разработчиков шрифтов. Напротив, она превратит их в героев, так как множество людей сможет по достоинству оценить их умение. Хотя общей накатанной схемы разработки шрифта не существует, есть вещи, которые можно делать с помощью METAFONT, когда основная часть работы уже кем-то выполнена. Геометрические построения довольно просты, и модифицировать буквы и символы, которые уже описаны в терминах системы METAFONT, можно достаточно быстро. Кому-то из разработчиков все же придется взять на себя мужество создать шрифт "с нуля", зато потом остальные смогут без особого труда его усовершенствовать. Эта книга не о математике и не о компьютерах. Но если у вас имеются зачаточные сведения об этих предметах (вы знаете высшую математику и умеете использовать компьютер для ввода и редактирования текста), то по прочтении этой книги работа с METAFONT не вызовет у вас затруднений. Некоторые места книги могут показаться вам менее понятными, чем остальные, так как автор старался в одном руководстве удовлетворить запросы как опытных "METAFONT еров", так и новичков, а также тех, кто использует METAFONT от случая к случаю. О подстерегающей сложности вас предупредит специальный знак: увидев в начале абзаца берегитесь "опасного поворота" хода мысли — не читайте такой абзац, если в этом нет реальной необходимости. Даже не читая этих сложных разделов, набранных мелким шрифтом, вы сможете достаточно хорошо освоить METAFONT и даже самостоятельно разрабатывать символы наподобие знака опасного поворота. Некоторые абзацы так сильно оторваны от контекста, что отмечены знаком все, что было сказано об одинарном знаке опасного поворота, для этого справедливо вдвойне. Попрактиковавшись какое-то время с системой METAFONT, вы можете
10 Предисловие попробовать разобраться в таких вдвойне опасных глубинах системы. На самом деле большинству людей никогда не понадобится столь досконально знать систему METAFONT, даже если они используют ее ежедневно. В конце концов для того, чтобы приготовить яичницу, не обязательно знание биохимии. Но на тот случай, если вы любопытны, здесь рассказано все. (Про METAFONT, а не про яйца.) Основанием для такого разделения по уровням сложности является то, что человек меняется по мере освоения любого мощного инструмента. На первых порах работы с системой METAFONT некоторые вещи покажутся вам очень простыми, в то время как другие потребуют времени для освоения. Поначалу вы скорее всего будете слишком жестко контролировать генерируемые начертания, задавая много избыточных данных, которые система уже определила по имеющимся. Но со временем, когда вы начнете чувствовать, с чем машина и сама неплохо справляется, вы будете охотно позволять системе METAFONT помогать вам в процессе разработки. По мере приобретения опыта работы с таким необычным помощником изменяются ваши перспективы, и вы выходите на качественно новый уровень. Так происходит со всяким мощным инструментом: всегда есть чему поучиться, и всегда находится лучший способ сделать то, что вы уже умеете. На каждом этапе развития необходимо руководство иного типа. У вас даже может возникнуть желание самому написать такое руководство. Обращая внимание на знаки "опасного поворота" в этой книге, вы сможете найти именно то, что интересует вас в данный момент. Руководства к компьютерным системам, как правило, написаны скучным языком. Но мы вас обрадуем: в этом время от времени встречаются ШУТКИ, так что вы сможете по-настоящему насладиться чтением. (Однако, большинство шуток вы сможете оценить по достоинству только при условии, что понимаете техническую сторону дела, поэтому читайте внимательно.) Следует отметить еще одну особенность этой книги: в ней не всегда написана правда. Когда понятия системы METAFONT вводятся неформально, для них приводятся общие правила, которые, как выясняется впоследствии, не совсем верны. Вообще, в поздних главах книги содержится больше достоверной информации, чем в начальных. Автор считает, что такая методика умышленного обмана позволяет читателю лучше схватывать идеи. Поняв простое, но не совсем верное правило, вам не составит особого труда дополнить его исключениями. Для того чтобы помочь вам усвоить прочитанное, в руководство вкраплены УПРАЖНЕНИЯ. Предполагается, что читатель должен попытаться выполнить все упражнения, за исключением тех, которые попадают в зоны действия знаков "опасного поворота". Если вам не удается решить проблему, вы всегда можете заглянуть в ответ. Но вначале все же попытайтесь решить ее самостоятельно; это, во-первых, ускорит ваше обучение, а, во-вторых, так вы и узнаете больше. И даже если вы уверены в правильности своего решения, откройте приложение А, чтобы убедиться в этом. Предупреждение: Увлечение разработкой шрифтов может быть губительно для других ваших интересов. Заразившись им, вы разовьете в себе повышенную чувствительность к начертаниям символов; вы станете по-другому воспринимать то, что читаете. Вы вечно будете думать о том, как улучшить шрифты, где бы они ни попались вам на глаза, особенно те, которые вы сами разработали.
Предисловие 11 Язык системы METAFONT, описанный здесь, имеет очень мало общего с тем языком, который автор прежде пытался использовать для разработки шрифтов. Пятилетний опыт работы со старой системой показал, что в этом деле необходим совершенно иной подход. И тот, и другой языки носят название METAFONT, но старый язык следовало бы именовать METAFONT 79 и как можно скорее забыть о нем. Пусть языком системы METAFONT называется язык, описанный в этом руководстве, поскольку он, во-первых, намного лучше, а во-вторых, уже никогда не будет изменяться. Хочу поблагодарить сотни людей, помогавших мне создать окончательную версию системы METAFONT, в основе которой лежит их опыт работы с предыдущими версиями. В частности, Джон Хобби (John Hobby) отыскал алгоритмы, без которых новый язык был бы невозможен. Моя работа в Станфорде поддерживалась Национальным научным фондом (National Science Foundation), Военно-морским исследовательским бюро (Office of Naval Research), корпорацией IBM (IBM Corporation), a также Фондом развития систем (System Development Foundation). Я также благодарен Американскому математическому обществу (American Mathematical Society) за поддержку и публикацию журнала TUGboat — вестника Общества пользователей системы TeX (см, приложение J). Но в первую очередь я признателен моей жене Джилл за вдохновение, понимание, заботу и поддержку, которые она дарит мне вот уже более 25 лет, особенно последние восемь лет, когда я напряженно работал над проблемами печати математических текстов. Станфорд, Калифорния — Д. Э. К. сентябрь 1985 Надеюсь на то, что у Божественного Правосудия есть в запасе подходящая кара, и она будет послана на головы тех преступников, которые придумали изменять алфавит наших отцов. ... Гениальный механик, изобретший печать, оставил свой след: ужасные творенья, в которые он вдохнул жизнь, затмили индивидуальность, обезличив все, что публикуется. — АМБРОЗ БИРС, The Opinionator. Alpha betes (1911) Может ли новый процесс дать такой результат, который, скажем, члены Клуба библиофилов признали бы произведением искусства, сравнимым с книгами из их собраний? - СТЭНЛИ МОРИ СОН, Typographic Design in Relation to Photographic Composition (1958)
О названии игры
Глава 1: О названии игры Эта книга посвящена компьютерной системе METfiFONT, которая тесно связана с издательской системой TeX, подробно описанной в книге Все про TeX". Каждая из этих систем отвечает за одну из двух основных задач печати: TeX правильно размещает символы на странице, a METAFONT определяет форму самих символов. Если их пути когда-нибудь и разойдутся, то это произойдет очень нескоро. Почему же система называется 'METAFONT'? Часть '-FONT' легко объяснима, поскольку происходит от английского слова font — "шрифт", а шрифтами принято называть используемые в печати множества взаимосвязанных символов. Часть 'МЕТА-' более интересна. Она указывает на то, что нас интересует описание шрифтов на уровне, более высоком по сравнению с описанием любого конкретного шрифта. Вообще, новообразованные слова, имеющие приставку "мета-", отражают современную тенденцию к рассмотрению вещей как бы извне или сверху, на более абстрактном уровне, соответствующем, как нам кажется, более зрелому пониманию. Так, в наши дни существуют метапсихология (наука о том, как связаны сознание и тело, в котором оно существует), метаисюрия (наука о принципах, определяющих общий ход событий), метаматематика (наука о математическом обосновании), мета- литература (литература, в которой описаны ее же формы) и т.п. Метаматематик занимается доказательством метатеорем (теорем о теоремах); ученые-информатики часто работают с метаязыками (языками, служащими для описания языков). Точно так же, меташрифт представляет собой схематическое описание различных начертаний в пределах семейства родственных шрифтов; начертания шрифтов изменяются в соответствии с изменением определяющих параметров. Метадизайн значительно сложнее простого дизайна, ведь куда проще нарисовать что-либо, чем объяснить, как это сделать. Одно из затруднений состоит в том, что предусмотреть все возможные описания непросто. Другое — в том, что компьютеру нужно объяснять абсолютно все. Но если нам один раз удалось описать достаточно общим образом, как рисовать нечто, то это же описание подойдет и при других обстоятельствах для аналогичных объектов. Поэтому оказывается, что время, затраченное на точное описание, себя оправдывает. Шрифты, используемые для набора текста, как правило, достаточно мелки, и глазу легче воспринимать их, если буквы разработаны с учетом размера, в котором они будут реально использоваться. Получить шрифт размера 7 пунктов можно, просто уменьшив шрифт размера 10 пунктов до 70% натуральной величины. Но, хотя это и кажется на первый взгляд заманчивым, выбирая этот легкий путь, мы значительно проигрываем в качестве. Гораздо лучших результатов можно добиться, введя в правила метадизайна параметрическую изменчивость. Встроенная изменчивость имеет преимущества даже при разработке одного шрифта фиксированного размера, поскольку позволяет отложить принятие решений по многим вопросам на потом. Оставив на ранней стадии некоторые величины неопределенными, трактуя их как параметры, а не "замораживая", вы позволите компьютеру изобразить множество вариантов, соответствующих различным значениям параметров, и сможете затем сравнить результаты этих экспериментов. Такой подход значительно расширяет возможности изменения и настройки внешнего вида символов. Но если меташрифты имеют столь значительные преимущества перед старыми добрыми обычными шрифтами, то почему их не разработали раньше? Главная причина в том, что до недавнего времени компьютеров попросту не существовало. А для людей вычисления со множеством параметров были делом трудным и уто-
Глава 1: О названии игры мительным. Сегодня же эту работу с легкостью проделывают машины, и введение параметров — это естественный продукт автоматизации. Ну, хорошо, согласимся, что, по крайней мере теоретически, меташрифты — вещь хорошая. Однако пока непонятно, как они должны быть устроены практически. Как задать зависимость начертания от неопределенных параметров? Если переменным является лишь один параметр, то проблему достаточно легко решить визуально, накладывая друг на друга ряд изображений, в которых изменения начертания представлены графически. Например, если параметр меняется в пределах от 0 до 1, мы можем заготовить пять набросков, соответствующих значениям параметров 0, £, £, | и 1. Располагая эти наброски по порядку, мы можем без труда выполнить интерполяцию и найти начертание, отвечающее значению, которое находится в промежутке между двумя заданными, например, |. Мы даже можем попробовать выполнить экстраполяцию, например, для значения 1 £. Но если число независимых параметров два и более, то визуальное решение становится слишком громоздким. Мы вынуждены применить "вербальный" подход, используя для описания искомых изображений некий язык. Представим, например, что мы хотим объяснить другу, который находится в далекой стране, как выглядит буква 'а' некоторого шрифта, используя для общения только телефон. Желательно, чтобы наш друг в точности воспроизвел то начертание, которое мы ему описали. Если нам удастся сделать это для какого-то одного начертания, то не составит труда обобщить наше устное описание, введя вместо констант переменные параметры. Поясним сказанное, прибегнув к аналогии с кулинарией. Представьте, что вы испекли превкусный пирог с ягодами, и друзья попросили его рецепт, чтобы самим испечь такой же. Если вы привыкли готовить, целиком полагаясь на интуицию, то записать в точности всю последовательность своих действий может оказаться непросто. Однако существует традиционный язык описания рецептов, и если вы прибегнете к скрупулезным измерениям, то окажется, например, что вы использовали 1 \ стакана сахара. Если бы вы подобным образом инструктировали кухонный комбайн, управляемый при помощи компьютера, то могли бы на следующем этапе перейти к метарецепту. В нем вы указали бы, например, что на каждые х стаканов ягод следует использовать .25ж* стаканов сахара или .Ъх + .25у стаканов сахара — на х стаканов черники и у стаканов ежевики. Иными словами, переход от дизайна к метадизайну имеет много общего с переходом от арифметики к элементарной алгебре. Числа заменяются простыми формулами, содержащими неизвестные величины. В дальнейшем мы не раз столкнемся с примерами подобной замены. В системе METAFONT определение полноценного шрифта состоит из трех основных частей. Первую составляют довольно рутинные процедуры, выполняющие необходимые технические функции, такие, например, как присваивание кодовых чисел конкретным символам и позиционирование каждого символа внутри невидимой рамки. Последнее необходимо для того, чтобы программа верстки смогла правильно расположить символы друг относительно друга в наборе. Далее следует более интересный набор процедур, которые предназначены для рисования основных элементов, характерных для символов данного шрифта (засечек, скруглений, петель, арок и т.п.). В описания этих процедур, как правило, входят их собственные * Заметьте, что для отделения целой части десятичного числа от дробной используется не запятая, а точка, поскольку в системе METAFONT десятичные числа представляются именно так. — Прим. перев.
Глава 1: О названии игры 15 специальные параметры, так что каждая из них позволяет воспроизводить множество родственных элементов. Так, например, при помощи одной и той же процедуры можно рисовать засечки различной длины, хотя во всех них будет угадываться один почерк. И наконец, в определении шрифта содержатся программы для каждого из символов. Если процедуры первого и второго типа выбраны удачно, то программы для символов представляют собой достаточно простые описания высокого уровня, не обремененные излишними подробностями. Это позволяет получать шрифты различного вида, изменяя только процедуры (например, процедуру, рисующую засечки) и не меняя при этом ни одной из программ, в которых эти процедуры используются. [Особо впечатляющий пример использования такого подхода продемонстрировали Джон Д. Хобби и Гу Гуоянь в своей работе "A Chinese Meta-Font" (TUGboat 5 (1984), 119-136). Изменяя лишь 13 базовых подпроцедур и используя одни и те же программы для символов, им удалось воспроизвести 128 основных китайских иероглифов в трех различных начертаниях (Song, Long Song и Bold).] При помощи хорошей программы для METAFONT дизайнер может выразить свой замысел намного точнее, чем при помощи рисунка, поскольку в языке алгебры присутствуют простые идиомы, которые позволяют выразить многие визуальные связи между объектами. Таким образом, программы для METAFONT можно использовать для передачи информации о том, как должны выглядеть символы, точно так же, как опыт искусного повара можно передать посредством рецептов. Однако голые алгебраические формулы воспринимаются с трудом. Описания METAFONT нуждаются в сопутствующих иллюстрациях так же, как построения из учебников по геометрии — в сопутствующих чертежах. Глупо ожидать, что, прочитав текст программы для METAFONT, кто-либо воскликнет: "Какая красивая буква!". Но если имеется одно или несколько увеличенных изображений этой буквы, то глядя на них, человек, прочитавший соответствующую программу, может с полным правом заявить: "Я знаю, как нарисована эта красивая буква!". В дальнейшем мы увидим, что система METAFONT позволяет легко получать "пробные оттиски", которые здорово помогают в работе над программой. Несмотря на то что язык METAFONT предназначен для описания меташриф- тов, его, конечно, можно использовать и для описания фиксированных начертаний, для которых приставка "мета-" теряет смысл. Более того, METAFONT можно использовать не только для разработки шрифтов; система позволяет вполне успешно изображать геометрические объекты, никак не связанные с символами или иероглифами. Автор, например, иногда использует METAFONT как калькулятор, выполняя в интерактивном режиме простые вычисления. Компьютеру ведь безразлично, что программы используются для целей, которые не соответствуют их названиям. Комбинируя постоянные и переменные виды начертаний [Тингуэли], создал несколько крупных, красочных открытых рельефов. Впоследствии, для того чтобы подчеркнуть идеи и стили, положенные в основу их концепций, он назвал эти рельефы Meta-Kandinsky и Meta-Herbin. — К. Г. ПОНТУС ХАЛЬТЕН, Джин Тингуэли: Мета (1972) Сама идея меташрифта теперь понятна. Но в чем ее прелесть? Возможность манипулирования множеством параметров, очевидно, интересна и забавна, но кому нужен шрифт размера 6j пункта, представляющий собой нечто среднее между Baskerville и Helvetica? — ДОНАЛЬД Э. КНУТ, Понятие меташрифта (1982)
Координаты у
Глава 2: Координаты 17 Если мы хотим инструктировать компьютер, как он должен изображать ту или иную фигуру, необходимо объяснить ему, где должны быть расположены ее ключевые точки. Для этого в системе METAFONT используются обычные декартовы координаты. Положение точки определяется заданием координаты х — числа единичных отрезков вправо от точки отсчета {reference point), и координаты у — числа единичных отрезков вверх от точки отсчета. Вначале мы определяем горизонтальную (вправо/влево) составляющую положения точки, а затем — вертикальную (вверх/вниз). Мир METAFONT — двумерный, поэтому двух координат достаточно. Рассмотрим, например, следующие шесть точек: к , . , . 1-И-И4+ В системе METAFONT их положения задаются следующим образом: (*1>У1) = (0,100); (*2,у2) = (100,100); (*3>Уз) = (200,100); (*4,У4) = (0, 0); (*5,lto) = (100, 0); (o?e,ye) = (200, 0). Точка 4 совпадает с точкой отсчета, поскольку обе ее координаты — нули; для того чтобы попасть в точку 3 = (200,100), нужно сделать 200 шагов вправо от точки отсчета и 100 шагов вверх, и так далее.* ► Упражнение 2.1 Какая из приведенных выше шести точек ближе всего к точке (60,30)? ► Упражнение 2.2 Верно ли, что все точки, лежащие на одной горизонтальной прямой, имеют одинаковую координату ж? ► Упражнение 2.3 Где расположена точка (—5,15)? ► Упражнение 2.4 Каковы координаты точки, лежащей на 60 единичных отрезков ниже точки 6 на приведенной выше диаграмме? ("Ниже" означает не "под страницей", а "вниз по странице".) При работе с METAFONT, прежде чем перейти к определению фигуры, вы обычно составляете ее примерный набросок на миллиметровой бумаге и нумеруете важные точки этого наброска любым удобным для вас образом. Затем вы пишете программу для METAFONT, в которой задаете (а) координаты этих ключевых точек и (б) прямые или кривые линии, которые должны соединять эти точки. В METAFONT имеется своя встроенная "миллиметровка", которая образует так называемый растр (raster}u ил» сетку, состоящую из квадратиков-пикселей. Вывод METAFONT определяет цвет пикселей. Некоторым из них будет приписан черный цвет, а остальным — белый. Таким образом, действия компьютера сводятся в основном к преобразованию рисунков в таблицы из нулей и единиц, на основе которых разработчик может восстановить рисунок, закрашивая соответствующие пиксели. Этот процесс напоминает вышивку по канве нитками двух цветов. * Заметьте, что в системе METAFONT координаты точки отделяются друг от друга не точкой с занятой, а запятой. — Прим. иерее.
18 Глава 2: Координаты Координаты определяют длину, но мы пока не говорили, в каких единицах они выражаются. Важно выбрать удобные единицы измерения. В системе METAFONT координаты задаются в пикселях. Таким образом, квадратики в приведенном выше рисунке, соответствующие разнице в 10 единиц по горизонтали и вертикали, представляют массивы пикселей размера 10 х 10, а прямоугольник, ограниченный шестью отмеченными точками, содержит в общей сложности 20000 пикселей.* Координаты не обязательно должны быть целыми числами. Можно, например, сослаться на точку (31.5,42.5), лежащую в самом центре пикселя с координатами вершин (31,42), (31,43), (32,42) и (32,43). Компьютер может обрабатывать координаты, значения которых представляют собой целое число, умноженное на ^5536 w 0.00002 от ширины пикселя, что обеспечивает высокую точность построений. Однако METAFONT никогда не закрашивает часть пикселя, а только весь пиксель целиком; тут уж — все или ничего. Размеры пикселей определяются разрешением (resolution) растровой сетки. Разрешение измеряется, как правило, в пикселях на дюйм (в Америке), и в пикселях на миллиметр (везде, кроме Америки). К примеру, шрифт, которым набран этот текст, разработан при помощи METAFONT с разрешением, чуть большим 700 пикселей на дюйм, но чуть меньшим 30 пикселей на миллиметр. Пока мы будем считать, что размеры пикселей столь ничтожны, что округление до целого числа пикселей ни на что серьезно не влияет. Позже мы обсудим проблемы, связанные с округлением, которые возникают при работе с малым разрешением. Программы для METAFONT желательно составлять так, чтобы они позволяли генерировать шрифты для устройств с различной разрешающей способностью. Тогда пробные оттиски символов, разработанных для устройств с высоким разрешением, можно будет получать на печатающих устройствах с низким разрешением. Поэтому в программах для METAFONT ключевые точки редко определяются указанием точных числовых значений, например, '100'; как правило, координаты определяются по отношению к некоторой величине, зависящей от разрешения, что позволяет с легкостью вносить изменения. Например, координаты шести рассмотренных выше точек лучше определить следующим образом: (*i,2/i) = (0,6); (*2,У2) = (а,Ь); (*з,Уз) = (2а, 6); (*4,У4) = (0,0); (я?5,У5) = (а,0); (я?в,Ув) = (2а, 0). Тогда величины а и 6 можно будет выбрать с учетом нужного разрешения. В предыдущем примере мы имели: а = Ь = 100. Но такие постоянные значения уменьшают гибкость программы либо не оставляют ее вовсе. Обратите внимание на величину '2а' в определениях точек а?з и xq; METAFONT достаточно хорошо знает алгебру, для того чтобы сообразить, что это означает значение а, умноженное на два, каково бы ни было это значение. В главе 1 мы отмечали, что именно использование простых алгебраических операций и делает METAFONT метасистемой. Интересно, что декартовы координаты названы в честь Рене Декарта (Rene Descartes) не потому, что он их выдумал, а потому, что он показал, насколько эффективно можно их использовать, применяя алгебраические методы. Координаты использовались для таких целей, как определение широты и долготы задолго до Декарта, но именно он первым заметил, что, подставляя в коор- * Как правило, термин "пиксель" обозначает квадратный фрагмент рисунка, но иногда мы используем его для обозначения единицы длины. Пиксель-квадратик имеет один линейный пиксель в длину и один линейный пиксель в высоту.
Глава 2: Координаты 19 динаты переменные величины, мы получаем возможность описывать бесконечные множества взаимосвязанных точек и выводить свойства кривых, что было весьма затруднительно при использовании одних лишь геометрических методов. Мы уже определили несколько точек, но пока ничего с ними не делали. Давайте нарисуем прямую, соединяющую точки 1 и 6, следующего вида: Один из способов сделать это при помощи METAFONT — указать в программе draw (xbt/i) .. (х6,у6). Здесь посредством '..' мы просим компьютер соединить две точки. Такие формулы, как '(xi, t/i)', приходится писать довольно часто. Поэтому для них удобно ввести краткое обозначение. Впредь мы будем обозначать точку (яь2д) через z\, и вообще, через Zk с произвольным индексом — точку (хк,Ук)- Таким образом, приведенную выше команду 'draw' можно переписать как draw z\ .. zq. Добавив посредством команд 'draw z2 • • *ь и 'draw z$ .. z\ еще две прямые, мы получаем фигуру, напоминающую узор на флаге Великобритании (Union Jack): Мы назовем этот символ "гексом" (hex), так как он имеет шесть концов. Отметим, что линии имеют некоторую толщину и скруглены на концах так, будто нарисованы чувствительным к наклону пером с круглым кончиком. METAFONT обеспечивает множество средств для регулирования толщины линий и изменения формы их концов, однако эти возможности мы обсудим позже, поскольку сейчас нашей главной целью является изучение координат. Если символ гекс уменьшить так, чтобы параметр 6, определяющий его высоту, равнялся высоте букв в этом абзаце, он будет иметь вид 1Ж'. Просто ради интереса попробуем напечатать подряд десять таких символов: До чего же это просто!* * Теперь, когда мы получили возможность с легкостью вводить новые символы и использовать их затем в своих печатных трудах, следует задуматься, как далеко мы желаем зайти в своих экспериментах? Разумно ли тысячами вводить новые символы? Не приведет ли это к появлению шрифтов-уродцев? Такие вопросы выходят за рамки этой книги. Однако легко вообразить, какая эпидемия шрифтомании может вспыхнуть, если люди поймут, насколько увлекательно конструировать собственные символы. Тогда, пожалуй, придется лечить их при помощи специально разработанной операции "шрифтолоботомии".
20 Глава 2: Координаты Давайте-ка приглядимся к нашему новому символу. Символ Ж слегка высоковат, поскольку выходит за пределы точек 1, 2 и 3, если учитывать толщину линий. Кроме того, он опускается чуть ниже базовой линии (baseline), т.е. прямой с координатой у = 0, которой принадлежат точки 4, 5 и 6. Для того чтобы исправить эту неточность, нужно немного сдвинуть ключевые точки. К примеру, точка z\ не должна располагаться точно в (0,6); мы должны внести такую поправку, чтобы верхний край пера находился в точке (0,6), когда его центр находится в точке z\. Это условие для трех верхних точек можно выразить уравнением top zi = (0,6); top z2 = (a, 6); top z3 = (2a, 6); точно так же, сдвиг точек 4, 5 и 6 определяется из уравнений hot z4 = (0,0); bot z5 = (a, 0); bot z6 = (2a, 0). Полученный в результате сплющенный символ имеет вид Здесь он показан в двух версиях: исходной — 'Ж' и утолщенной — *Ж\ ► Упражнение 2.5 Десять утолщенных гексов образуют (>ЮЮЮЮЮЮЮЮЮК'. Отметим, что смежные символы частично накладываются друг на друга. Это объясняется тем, что каждый символ имеет ширину 2а, и поэтому точка 3 одного символа совпадает с точкой 1 следующего символа. Допустим, требуется, чтобы символы целиком содержались в прямоугольных рамках ширины 2а так, чтобы смежные символы лишь слегка соприкасались (ЖЖЖЖЖЖЖЖЖЖ). Объясните, как для этого нужно изменить приведенные выше уравнения, определяющие положения точек, имея в виду, что в METAFONT определены операции 7/fc' и *гЬ\ аналогичные Чор' и lboV (обозначающие, соответственно, левый и правый край пера). Пары координат можно представлять себе не только как точки, но и как векторы или "смещения". К примеру, (15,8) можно рассматривать как команду перейти на 15 пикселей вправо и на 8 пикселей вверх. Тогда точка (15,8) — это положение, в которое мы попадем, выполнив команду (15,8), если в начальный момент мы находились в точке отсчета. В такой интерпретации можно рассматривать операцию сложения векторов: смещение на вектор (15,8), а затем — на вектор (7, —3) даст тот же результат, что и смещение на (15,8) + (7, —3) = (15 + 7,8 — 3) = (22,5). Сумма двух векторов z\ = (xi,yi) и z2 = (#2,2/2) есть вект0Р z\ + z2 = (#1 + £2,2/1 +2/2), получаемый в результате сложения компонент а; и у по отдельности. Этот вектор представляет результат последовательного смещения на векторы z\ и z2. Иначе говоря, z\ + z2 представляет точку, в которую мы попадем, сместившись из точки z\ на вектор z2. ► Упражнение 2.6 Какой из четырех базисных векторов (0,1), (1,0), (0,-1) и (—1,0) соответствует смещению на один пиксель (а) вправо? (Ь) влево? (с) вниз? (d) вверх?
Глава 2: Координаты 21 Векторы можно не только складывать, но и вычитать. Разность z\ — z2 есть просто {х\ — х2,у\ — уг)- Более того, естественным образом вводится операция умножения вектора на произвольное число с. Произведение вектора (х, у) и числа с, которое записывается как с{х,у), есть вектор (сх, су). Таким образом, например, значение 2z = 2(х,у) = (2х,2у) оказывается равным z Н- z. В особом случае, когда с = —1, мы пишем —(х,у) = (—х, —у). Мы подошли к важному понятию, в основе которого лежит тот факт, что вычитание есть операция, обратная сложению. Если z\ и z2 — точки, то z2 — z\ есть вектор, соответствующий переходу из z\ в z2. Это объясняется просто: z<i — z\ — это как раз то, что следует прибавить к z\, чтобы получить 22, то есть z\ + (22 — z{) =22. Мы назовем это правило принципом вычитания векторов. Он часто применяется, когда разработчик хочет указать направление перехода из одной точки в другую и/или расстояние между точками. В системе METAFONT для отражения соотношений между точками часто используется еще одна идея. Предположим, что, стартовав из точки zi, мы начинаем движение по направлению к точке 22, но проходим лишь часть пути. Для такого смещения существует специальное обозначение, использующее квадратные скобки: \[z\, z2) — одна треть расстояния между z\ и^; Ifci»-^] — половина расстояния между z\ и z<i\ .8[zi, 2:2] — восемь десятых от расстояния между z\ и z<i- В общем случае £[21,2:2] обозначает точку, которая лежит на расстоянии, составляющем часть t от всего пути между z\ и 22. Формально эту операцию мы назовем помещением в промежуточное положение между точками, а неформально — функцией "часть от расстояния". Если значение t изменяется от 0 до 1, то t[z\, z2] пробегает точки на прямой между zi и z2. В соответствии с принципом вычитания векторов, для того чтобы проделать весь путь от z\ до z2l мы должны сместиться на Z2 — 21. Следовательно, часть t от расстояния между ними есть t[Zi,Z2] = 2i +£(22 -2i). Эта формула позволяет найти t[z\, z2] для любых заданных значений t, z\ и z2. Но в METAFONT эта формула встроена изначально, так что мы можем, не задумываясь, пользоваться нашим обозначением с квадратными скобками. Вернемся к шести точкам из первого примера и предположим, что нужно указать точку, лежащую на 2/5 части пути между 22 = (100,100) и zq = (200,0). В METAFONT это записывается как .4[22,2е]. Если же нам потребуется вычислить координаты в явном виде, то мы всегда можем вывести их из общей формулы: 22 + .4(26 - 22) = (100,100) + .4((200,0) - (100,100)) = (100,100) + .4(100, -100) = (100,100) + (40, -40) = (140,60). ► Упражнение 2.7 Верно ли, что (—3,5) — это вектор направления из (5, —2) в (2,3)? ► Упражнение 2.8 Если формула 'O^i,^]' имеет смысл, объясните его. Что означает 'l[2i,22]'? A'2[2i,22]'? А<(-.5)[2Ь22]'? ► Упражнение 2.9 Верно ли с точки зрения математики, что: (а) ^[21,22] = |(2i +22); (b) \[zuz2] = £zi + \г2\ (с) t[zuz2] = (1 - t)[z2,zi].
22 Глава 2: Координаты В заключение главы попробуем определить при помощи операции помещения в промежуточное положение пять ключевых точек буквы 'А', изображенной в увеличенном виде на рисунке справа. Точки 1 и 5 должны быть расположены на расстоянии а друг от друга, а точка 3 должна находиться на Ь пикселей выше базовой линии. Значения а и Ь установлены заранее в соответствии с методом, который мы опишем позже, и точно так же установлено значение параметра s, определяющего расстояние по горизонтали от точек 1 и 5 до края рамки. Мы будем предполагать, что точная высота, на которой должна , . , , находиться перекладина, нам неизвестна. Точка 2 должна находиться на прямой линии, соединяющей точки 1 и 3, а точка 4 — в соответствующем положении между точками 5 и 6. Но прежде чем определиться с выбором, мы должны проверить несколько возможных вариантов. Ширина символа составляет s + а + s, и положение точек z\ и z*> можно задать формулами hot zi = (s,0); z5 = z\ + (a,0). Это можно сделать и другими способами, но данные формулы ясно выражают наше желание, чтобы нижний край пера касался базовой линии в s пикселях справа от точки отсчета, когда само перо находится в точке z\, а точка z$ находилась на а пикселей правее точки z\. Мы можем выразить это формулой zs = (|[хьх5],6). То есть, координата х точки 3 должна быть средним между координатами х точек 1 и 5, а уз должно быть равно 6. Наконец, укажем z2 = alpha[zi,z3]] z* = alpha[z^,z3], где параметр alpha есть число между 0 и 1, которое определяет положение перемычки и которое задается позже. Когда значение alpha уже задано, мы можем написать draw z\ .. zz\ draw zz .. 25; draw z<i .. 24. Если a = 150, b = 250, s = 30, и alpha будет меняться от 0,2 до 0,5 с шагом 0,05, то METAFONT нарисует символы 'ААААААА'. В иллюстрации на предыдущей странице alpha — (3 — \/5)/2 ~ 0.38197; при таком значении отношение высот верхней и нижней частей фигуры составляет (\/5 — 1)/2 « 0.61803 — число, называемое в классической греческой математике золотым сечением. /$s (Вы уверены, что вам следует читать этот абзац? Знак "опасный поворот" преду- JL преждает, что здесь содержится материал, который должен быть пропущен при первом чтении, а может, и при втором. В таких абзацах, требующих от читателя особого внимания, иногда встречаются понятия, которые объясняются только в последующих главах.) /^\ ► Упражнение 2.10 JL Почему лучше определить гз как (|[xi,xs],b) и не выводить точные координаты точки zz — (5 + ^а, 6), которые можно определить из других уравнений.
Глава 2: Координаты 23 /^s/gK*Упражнение 2.11 JL JL ПуСТЬ ТОЧКИ Zl, 2з И 25 Тв Жв, ЧТО И ВЫШв. Как НуЖНО Определить 22 И 24, чтобы одновременно выполнялись следующие условия: ■ линия из 22 в 24 поднимается вверх под углом в 20°; ■ координата у середины этой линии составляет 2/3 от разности г/з и t/i; ■ 22 и 24 расположены, соответственно, на линиях z\ .. 23 и 2з .. 25. (Справившись с этим упражнением, вы получите букву 4А\) Достигая сферы математики, мы попадаем в окружение процессов, которые кое-кому кажутся самыми "бесчеловечными" и далекими от поэзии. Но именно здесь художник может дать самую полную свободу своему воображению. — ХАЙВЛОК ЭЛЛИС, Танец жизни (1923) Для тех, кто жил в одном из современных американских городов (за исключением Бостона), по крайней мере одна из идей, лежащих в основе декартовой аналитической геометрии, покажется до смешного простой. И все же математикам потребовалось целых две тысячи лет, чтобы додуматься до такой простой вещи. — ЭРИК ТЕМПЛ БЕЛЛ, Математика: королева и слуга науки (1951)
Кривые
Глава 3: Кривые 25 Еще в эпоху Возрождения Альбрехтом Дюрером (Albrecht Diirer) и другими деятелями предпринимались попытки установить математические принципы начертания символов. Однако буквы, которые у них получались, не отличались особой красотой. Их методы оказались неэффективными, поскольку они ограничивались построениями при помощи циркуля и линейки, не способными адекватно отразить все нюансы, присущие качественной каллиграфии. METAFONT успешно справляется с этой задачей, используя более сильные математические методы, которые, не будучи слишком сложными, обеспечивают необходимую гибкость. Цель данной главы — объяснить простые принципы, руководствуясь которыми компьютер может изображать "красивые" кривые. Основная идея состоит в следующем. Мы берем четыре исходные точки (2:1,2:2,2:3,2:4) и строим по ним три промежуточные — 2:12 = §[21,22], 223 = §[22,23] и 234 = §[23,24]: Затем по этим трем промежуточным точкам (212, 223, 2:34) строим две промежуточные точки второго порядка — 2123 = §[212,223] и 2234 = §[223,234]- Наконец, строим промежуточную точку третьего порядка 21234 = §[2123,2234]: Точка 21234 — это одна из точек кривой, определяемой точками (21,22,23,24). Для того чтобы получить остальные точки, нужно повторить построение на наборах (21,212,2123,21234), (21234,22з4,2з4,24) и так далее до бесконечности: Этот процесс быстро сходится, а вспомогательные линии (которые в нашем примере расположены выше предельной кривой) в итоге отбрасываются. Предельная кривая обладает следующими важными свойствами: ■ начинается в 2i и ведет в направлении к 22; ■ заканчивается в 24, попадая в 24 из 2з; ■ полностью содержится внутри так называемой выпуклой оболочки точек 2i, 22, 23 и 24, т.е. все точки кривой лежат "между" исходными точками.
26 Глава 3: Кривые f Рекурсивное правило нахождения промежуточных точек при рисовании кривых было открыто Полем де Кастелье (Paul de Casteljau), который показал, что кривую можно описать алгебраически посредством удивительно простой формулы z(t) = (l-t)3zi+3(l-t)2tz2 + 3(l-t)t2z3 + t3zA, где значение параметра t меняется от 0 до 1. Этот полином третьей степени по t называется полиномом Бернштейна, так как именно Сергей Николаевич Бернштейн в 1912 году впервые рассмотрел такие функции в своей пионерской работе, посвященной теории приближений. Кривые, которые очерчивают полиномы Бернштейна третьей степени, часто называют кубическими кривыми Безье, по имени Пьера Безье (Piere Bezier), который в 1960-е годы осознал их значимость для компьютерных приложений. /g\ Интересно отметить, что полином Бернштейна первой степени, то есть функция JL z{t) = (1 — t) zi +1Z2, есть ни что иное, как оператор помещения в промежуточное положение t[zi, 22], который мы обсуждали в предыдущей главе. Действительно, если в только что рассмотренной геометрической конструкции в качестве промежуточных точек выбирать точки, соответствующие части t от расстояния (т.е. если 212 = фь^г] и ггз = £[22»2з] и т.д.), то точка ^1234 совпадет с z(t) из приведенной выше формулы. Для любого набора (zi,22,2:3,24) описанное выше построение определяет кривую, которая соединяет точки z\ и z±. Эта кривая не всегда интересна или красива. К примеру, если все четыре исходные точки лежат на одной прямой, то и определяемая ими "кривая*' целиком принадлежит этой прямой. Нумеруя по- разному четыре исходные точки, мы получаем различные кривые: Очевидно, исходные точки желательно выбирать разумным образом. Но "четырехточечный метод" позволяет находить удовлетворительные приближения для произвольной кривой. Для этого кривую нужно разбить на достаточно короткие сегменты и на каждом из сегментов задать четыре подходящие точки. Оказывается, что обычно можно обойтись небольшим числом сегментов. К примеру, используя четырехточечный метод, мы можем построить приближенно четверть окружности с погрешностью, меньшей 0,06%; построить окружность точно он никогда не позволит, но разница между четырьмя такими четвертями и правильной окружностью практически неощутима. Как сказано выше, в основе всех кривых, которые рисует METAFONT, лежат четыре точки. Но пользователю не обязательно задавать все эти точки, так как компьютер обычно сам в состоянии додуматься, какими должны быть подходящие значения z^ и z$. В программах для METAFONT непосредственно указываются лишь крайние точки z\ и Z4, через которые кривая действительно будет проходить. Вернемся, например, к шести точкам, которые мы использовали в главе 1 при введении понятия координат. Там, чтобы нарисовать прямую из точки z\ в точку ze, мы использовали команду 'draw z\ .. zq\ Вообще же, если вместо двух точек перечисляются три или более, то METAFONT рисует гладкую кривую,
Глава 3: Кривые 27 проходящую через все эти точки. Так, например, команды 'draw z± .. z\ .. z^ .. z$ и 'draw z$ .. Z4 .. z\ .. z$ .. zq .. Z5' дадут, соответственно, такие результаты: (На этих рисунках точки, оставшиеся непомеченными, — это контрольные точки, которые система METAFONT ввела автоматически, чтобы применить метод четырех точек и нарисовать кривые, проходящие через все пары смежных точек на указанных путях.) Заметьте, что в примере справа кривая в точке zb не является гладкой, так как z*> присутствует на обоих концах заданного пути. Получить абсолютно гладкую кривую, которая возвращается в начальную точку, можно при помощи команды 'draw 25 .. 2:4 • • z\ .. zs . • zq .. cycle': Слово 'cycle' в конце определения пути означает возврат в начальную точку. По мнению METAFONT, эта фасолевидная фигура представляет собой наилучший способ соединения заданных точек в указанном циклическом порядке, хотя, конечно, существует множество других кривых, удовлетворяющих этим условиям, и, возможно, в вашем воображении она рисуется иначе. Вы сможете лучше контролировать ее построение, давая машине разного рода указания. Например, фасолевидную кривую можно "натянуть потуже" между точками z\ и z$, указав draw 25 .. z^ .. z\ .. tension 1.2 .. Z3 • • zq .. cycle; Натяжение (tension) между точками по умолчанию имеет значение 1; увеличив его до 1.2, мы получим кривую вида
28 Глава 3: Кривые Эффекта асимметрии можно добиться, увеличив натяжение только в точке 1, но не меняя его в точках 3 и 4. Фигура получена при помощи команды 'draw 25 ■. 24 .. tension 1 and 1.5 .. z\ .. tension 1.5 and 1 .. 23 .. Z6 • • cycle'. В данном примере эффект натяжения достигается посредством перемещения двух безымянных контрольных точек поближе к точке 1. Существует и другой способ управления кривой: можно задать направление движения в какой-либо одной или всех точках. Направления задаются в фигурных скобках. Например, draw 2:5 .. Z4{left} .. z\ .. 2:3 .. z^{left} .. cycle означает, что кривая в точках 4 и б должна быть направлена влево. В результате кривая от zq до z*> и далее от z$ до z± идет строго по прямой: Позже мы увидим, что 'left1 — это условное обозначение вектора (—1,0), который соответствует смещению влево на единичный отрезок. Мы можем задать любое направление, помещая в фигурные скобки {...} соответствующий вектор. Например, команда 'draw z\ .. 2:2(2:3 — 2:4} .. 2:3' нарисует такую кривую из z\ в zi и из z<i в ^з, что касательное направление в точке Z2 будет параллельно прямой 2:4 .. 2:3, поскольку вектор 2:3 — 2:4 соответствует смещению из z\ в z^: Тот же результат дает команда 'draw 2:4 .. 2:2(10(2:3 - 2:4)} .. z$\ поскольку направления векторов 10(2:3 — 2:4) и Z3 — ^4 совпадают. METAFONT игнорирует абсолютные величины векторов, когда они используются только для указания направлений. ► Упражнение 3.1 Каким будет результат команды 'draw 2:4 • • 2:2(2:4 ~~ 2з} • • *з\ если точки zi, z2, z$ и Z4 — те же, что в последних примерах?
Глава 3: Кривые 29 ► Упражнение 3.2 Как заставить METAFONT изобразить следующую изогнутую фигуру где кривая в точке б направлена в точку 2, а в точке 4 — от точки 2? [Указание: натяжение менять не нужно, достаточно указать направления в точках z± и zq] METAFONT позволяет менять форму кривой на ее концах указанием величины загиба, (curl). К примеру, следующие две команды draw z\{curlO} .. z2{z3 — z4} .. {curlO} z3; draw z4{curl2} .. z2{z3 - z4} .. {curl 2} z3 дают в результате, соответственно, кривые Сравните их с приведенной ранее кривой, в определении которой величина загиба не указывалась. (Если величина загиба или направление явно не указано, то по умолчанию оно принимается равным 1, точно так же, как значение натяжения между точками по умолчанию предполагается равным 1.) Более подробно об этом рассказывается в главе 14. Получить кривую вместо прямой можно даже тогда, когда заданы всего две точки. Для этого в одной из точек или в обеих нужно указать направление. Например, посредством команды draw z±{z2 — Z4} .. {down} z^ мы просим METAFONT нарисовать кривую, которая из начальной точки выходит по направлению к z2, а в конце идет строго вниз: Вот несколько кривых, которые METAFONT строит между двумя точками, получив указание выходить из левой точки под углом 60° и входить в правую точку под
30 Глава 3: Кривые различными углами: Этот рисунок получен при помощи следующей программы: for d = 0 step 10 until 120: draw (0,0){dir60} .. {dir-d} (6cm, 0); endfor; где функция 'dir' определяет направление, измеряемое в градусах, причем отсчет ведется от горизонтальной линии справа против часовой стрелки. Следовательно, 'dir — d1 задает направление на d° ниже горизонта. На рисунке нижние кривые соответствуют малым значениям d, а верхние — значениям, близким к 120°. Автомобиль, движущийся вдоль верхних путей на приведенном выше рисунке, постоянно поворачивает вправо, в то время как на нижних путях он достигает такой точки, где вынужден повернуть влево, для того чтобы войти в конечную точку под заданным углом. То место, где кривизна пути меняется с правой на левую или наоборот, называется точкой перегиба (inflection point). МЕТА FONT вводит точки перегиба, если считает, что лучше изменить кривизну линии, чем делать резкий поворот; при отрицательных d избежать появления точек перегиба невозможно, а кривые для малых положительных значений d должны не сильно отличаться от кривых, полученных при малых отрицательных значениях d. Программа for d = 0 step -10 until -90: draw (0,0){dir60} .. {dir-d}(6cm,0); endfor показывает, что делает METAFONT при отрицательных d: Иногда при положительных значениях d желательно избежать точек перегиба для того, чтобы кривая полностью содержалась внутри треугольника, определяемого направлениями в начальной и конечной точках. Этого можно добиться, используя в определении кривой три точки вместо двух: программа for d = 0 step 10 until 120: draw (0,0){dir60}...{dir-d}(6cm,0); endfor
дает кривые Глава 3: Кривые 31 Они ничем не отличаются от предыдущих, за исключением того, что при малых значениях d отсутствуют точки перегиба. Три точки '...' в определении кривой "ограничивают" ее, целиком помещая внутрь треугольника, определяемого крайними точками и направлениями; но если такого треугольника не существует, они не дают никакого эффекта. Точнее, если кривая ведет от точки zo к точке zi, и если существует такая точка z, направление которой в начальной точке есть направление из zo в z, а направление в конечной точке есть направление из z в zi, то кривая, определяемая при помощи '...', целиком содержится внутри треугольника с вершинами zo, zi и z. Если же такого треугольника не существует (например, если d < 0 или d > 120 в программе из предыдущего примера), то и '...', и '..' дадут в результате одну и ту же кривую. В этой главе мы познакомились со множеством способов рисования кривых в системе METAFONT. Но существует еще один способ, который включает в себя все остальные. Если для того, чтобы нарисовать кривую нужного вида, недостаточно изменения натяжений, загибов, направлений и/или ограниченности, то всегда есть возможность непосредственно указать все четыре исходные точки, которые фигурируют в четырехточечном методе. Например, команда draw Z4 • • controls z\ and z^ .. zq рисует следующую кривую из z± в zq: Итак, по-моему, я не упустил ничего, что необходимо для понимания кривых линий. —- РЕНЕ ДЕКАРТ, Геометрия (1637) Правила или то, что заменяет руки художника, никак не могут быть вполне достаточными, хотя, когда их устанавливают такие люди, как Дюрер, Тори, да Винчи и Серлио, они определяют каноны пропорций и построений, обеспечивающие прочный фундамент, на основе которого можно строить новые выражения. — ФРЕДЕРИК В. ГУДИ, Типология (1940)
Перья
Глава 4' Перья 33 До сих пор в наших примерах фигурировали только прямые и кривые линии, которые имели вид нарисованных при помощи чувствительного к наклону пера с идеально круглым кончиком. Математическая линия не имеет толщины и потому невидима; но если в каждой точке бесконечно тонкой кривой мы поставим круглую точку, то получим видимую линию постоянной толщины. Однако, кроме линий постоянной толщины, в арсенале системы METAFONT имеется еще несколько видов изобразительных средств, которые мы рассмотрим в этой главе. Мы увидим, что в процессе построения символов можно не просто менять размер и форму кончика пера, но строго и контролировать форму каждого штриха. Обсудим вначале простейшие модификации символов, рассмотренных нами ранее. Буква 'А' из главы 2 и фасолинка 'о' из главы 3 были нарисованы пером с круглым кончиком диаметра 0.4 pt, где 'pt' означает типографский пункт;* 0.4 pt — стандартная толщина линий ' ' в системе l^X. Такое перо можно "выбрать" посредством команды pickup pencircle scaled 0.4pt; Когда вы просите METAFONT нарисовать что-либо посредством команды draw, система использует то перо, которое было указано в последней по счету команде pickup. Перо pencircle имеет круглую форму и диаметр, равный ширине пикселя. В результате масштабирования пера на OApt оператором 'scaled' его диаметр увеличивается до 0.4 pt, поскольку pt — это количество пикселей в 1 пункте. Если ключевые точки {zi,Z2,Z3,zt,Zb,Zb) из глав 2 и 3 уже заданы, то команды pickup pencircle scaled O.Spt; draw 2:5 .. z\ .. z\ .. Z3 .. zq .. cycle дадут фасолинку, которая вдвое толще прежней: 'о\ вместо 'о'. Более интересные эффекты возникают при использовании перьев с некруглым кончиком. К примеру, команда pickup pencircle xscaled O.Spt yscaled 0.2pt означает выбор пера с кончиком в форме эллипса шириной 0.8 pt и высотой 0.2 pt; увеличенный в 10 раз, он имеет вид '«—'. (Оператор 'xscaled' умножает все координаты х на указанное число, но не меняет координаты у; оператор 'yscaled' действует с точностью до наоборот.) При использовании такого пера 'о' превращается в 'о', а 'А' — в 'А'. Более того, команда pickup pencircle xscaled O.Spt yscaled 0.2pt rotated 30 поворачивает эллипс на 30° против часовой стрелки, в результате чего кончик приобретает вид V; это меняет 'о' на 'о', и 'А' — на 'А'. Многократно увеличив * Как объяснялось в книге Все про TeX, 1 дюйм = 2.54 см = 72.27 pt.
34 Глава 4' Перья фасолевидную фигуру, мы сможем лучше рассмотреть, что при этом происходит: Пример справа получен исключением высказывания 'yscaled 0.2pt\ вследствие чего в момент перед вращением перо становится тоненьким, почти как бритва, и имеет высоту всего один пиксель. ► Упражнение 4.1 Опишите форму следующих перьев: (a) pencircle xscaled 0.2pt yscaled O.Spt; (b) pencircle scaled O.Spt rotated 30; (c) pencircle xscaled .25 scaled O.Spt. ► Упражнение 4.2 Мы рассмотрели немало примеров, в которых команда draw применялась к двум или более точкам. Как вы думаете, что будет, если попросить METAFONT выполнить следующие команды? draw z\\ draw Z2\ draw 2:3; draw Z4; draw z*>\ draw z%. Давайте теперь попробуем построить настоящую букву, которая не раз уже встречалась в этом руководстве, а именно — 'Т' из логотипа 'METAFONT'. По мере того как мы все глубже будем вникать в тонкости языка системы, все семь различных букв этого логотипа будут нами использоваться для иллюстрации различных идей. Первой мы рассмотрим букву 'Т', во-первых, (o,h) (w,h) потому, что она встречается в логотипе дважды, а во- вторых, (что более важно) потому, что она — самая простая. На рисунке справа показаны буква в увеличенном виде, ее ограничивающая рамка и четыре ключевые точки (zi,Z2,zz,zt). В таких издательских системах как TeX за основу берется предположение о том, что каждый символ ограничен прямоугольной рамкой. Подробно о рамках мы поговорим чуть позже, а пока нам достаточно просто знать о существовании этих границ.* Числа h и w подбираются так, чтобы углы рамки находились в позициях (0,0), ^ ' ' ^щ ' (0,/i), (ги,0) и (u?,/i), как показано на рисунке. Каждая из букв логотипа 'METAFONT' нарисована пером с кончиком в форме неповернутого эллипса, высота которого составляет 90% от его же ширины. Для шрифта размером 10 пунктов, которым набран основной текст книги, перо имеет * Строго говоря, вовсе не обязательно, чтобы все черные пиксели символа лежали в пределах ограничивающей рамки; к примеру, символ 'Т' в точке 4 вылезает немного вниз за базовую линию, а наклонные буквы часто выходят вправо за пределы своих рамок. Однако TeX размещает все символы, склеивая рамки в единое целое так, как если бы они были фрагментами металлической печатной формы, на которую наносятся чернила.
Глава 4- Перья 35 ширину 2/3 pt и поэтому задается командой pickup pencircle scaled \pt yscaled ^ или какой-нибудь эквивалентной. Мы предположим, что предварительно вычислена специальная величина 'о' такая, что нижний край вертикальной черты в ' Т' лежит ровно на о пикселей ниже базовой линии; эту величину называют перелетом (overshoot). При заданных /i, w и о определить четыре ключевые точки и нарисовать ' Т' просто: top Ift z\ — (0, h); top rt Z2 = (iu, h)', top zs = (.5iu, h); bot z± = (.5iu, —o); draw z\ .. Z2; draw zz .. 24. Иногда удобнее задавать координаты х и у по отдельности. Например, ключевые точки символа ' Т' могут быть указаны и таким образом: //£xi=0; w — X2=xi] хз = Х4(=.5гу; *ор yi = Л; bot 1/4 = -о; t/i = 2/2 — t/з- Уравнение ги — Х2 = xi показывает, что хг отстоит от правого края ограничивающей рамки ровно настолько, насколько xi отстоит от ее левого края. f Каков же точный смысл оператора Чор' в уравнениях METAFONT? Если выбранное на данный момент перо охватывает / пикселей слева от его центра, г пикселей — справа, t пикселей — вверх и b пикселей — вниз, то top * = * + ((),*), botz = z- (0,6), Iftz = z-(/,0), rt* = z + (r,0), где z обозначает пару координат. Но, как видно из предыдущего абзаца, справедливы и такие соотношения: top у = у -f t, bot у = у — 6, Ift х = х — I, rt х = х + г, где х и t/ — одиночные числовые переменные, а не пары координат. Нельзя применять Чор1 и LboV к координатам х, а '(/Г и 'rt1 — к координатам у. ► Упражнение 4.3 Верно ли, что: top bot z = z для любой пары координат z? ► Упражнение 4.4 На увеличенном рисунке, изображаю- (°»Л) (w^h) щем букву 'М' из логотипа METAFONT, мы видим пять ключевых точек. Предположим, что предварительно вычислены специальные величины ss и удар и заданы уравнения Xi = ss = w - х5; 2/з - 2/1 = ууар- Какие еще уравнения и команды 'draw' нужно задать для того, чтобы завершить определение этой буквы? (Значение w для ' М ' больше, чем для 'Т'; оно выражает ширину изображаемого символа в пикселях.) (о,о) (w,0) Команда 'draw' позволяет изображать символы вполне удовлетворительного вида. Но разнообразие их начертаний изначально ограничено тем, что вдоль всей проводимой линии форма кончика имитируемого пера остается неизменной.
36 Глава 4' Перья Люди, искусно владеющие пером, добиваются гораздо большего богатства эффектов, изменяя силу давления и/или вращая перо в процессе рисования. Мы можем гораздо строже контролировать форму создаваемых символов, задавая непосредственно их контуры, вместо того, чтобы работать только с ключевыми точками, лежащими где-то "внутри" фигуры. На самом деле внутренние операции METAFONT производит именно с контурами, и компьютеру гораздо легче произвести заливку целой области, чем закрасить соответствующие пиксели передвигая перо. Существует специальная команда заливки области — 'fill'. Например, залитая фасолевидная фигура была получена применением к небезызвестным шести точкам команды fill 2:5 .. 2:4 .. z\ .. zs .. zq .. cycle. Фактически, залитая область — это то, что будет вырезано бесконечно острым ножом, если мы проведем его лезвием вдоль всей кривой, разрезая тонкую пленку. В отличие от команды draw которая придает кривой некоторую толщину, так как в противном случае результат ее действия будет невидимым, команда fill заливает строго указанный контур и ничего лишнего не добавляет. Определение кривой в команде fill должно заканчиваться словом 'cycle', так как заливаться должна замкнутая область. Не имеет смысла указывать, например, 'fill z\ .. z^ - Заливаемый контур не должен сам себя пересекать, иначе у системы METAFONT возникнет множество проблем с выполнением команды 'fill', как, например, в случае 'fill z\ .. z% .. 2:3 .. 24 .. cycle'. f> Упражнение 4.5 В главе 3 рассматривалась кривая z$ .. z\ .. z\ .. zz .. 26 ■ 2s, которая в точке z$ не является гладкой. Команду fill к ней применить нельзя, поскольку ее определение не заканчивается словом 'cycle'. Однако она определяет замкнутую область Какие инструкции нужно дать системе METAFONT, чтобы она залила эту область? Черный треугольник '►', которым в этой книге отмечены упражнения, рисуется при помощи команды fill z\ - - Z2 - - zz - - cycle после того, как заданы угловые точки z\, Z2 и z$. В данном случае контур области, подлежащей заливке, определяется посредством символа '--', а не '..'; это обозначение мы до сих пор не обсуждали. Каждая связка '--' представляет прямолинейный отрезок, независимый от остальной части пути, которому он принадлежит. В этом его отличие от символа '..', который определяет отрезок линии, возможно, искривленный, соединеняющийся с соседними точками и линиями пути гладким образом. Поскольку в данном случае использовался символ '--', определяемая им область имеет прямые границы и острые углы. Говоря неформально, '..' означает
Глава 4' Перья 37 "соединить данные точки плавной кривой", в то время, как '--' означает "соединить данные точки прямой линией". . (0,/i) (w,h) /gN Точки zi, zi и zz специально были выбраны так, JL чтобы треугольник был равносторонним, т.е. чтобы все три его стороны имели одинаковую длину. Поскольку все углы равностороннего треугольника равны 60°, то нужные уравнения имеют вид: #1 = Х2 = w — хз = s; Уз = .5Л; z\ — 22 = (23 - 22) rotated 60, где w и h представляют, соответственно, ширину и высоту символа, as — величина отступа, т.е. расстояние на которое треугольник отстоит от левого и правого краев рамки. (о,о) (и;,о) fy команды fill есть "родственница" по имени unfill, которая меняет цвет пикселей внутри заданной области с черного на белый. Например, залитую фасолевидную фигуру с предыдущей страницы можно превратить в указав дополнительно 'unfill ^[24,2:2] • • f [^4,^2] • • cycle; unfill | [26,^2] • • f [^6,^2] cycle'. Кроме прочего, из данного примера видно, что, хотя две точки определяют лишь прямую линию, METAFONT преобразует двухточечное определение, такое как '^2 .. 22 • • cycle', в путь, более или менее похожий на окружность. f> Упражнение 4.6 Пусть 2о — точка (.8[xi, £2], -5[t/i , 2/4]) Введем шесть новых точек по формуле z'k = .2[2jb, 20], где к = 1, 2,3,4, 5,6. Объясните, как получить фигуру внутренняя область которой определяется точками z[... z'6, а не z\... zq. Возможность заливки контура дает основание предположить, что, рассматривая отдельно траектории, описываемые левым и правым краями пера, мы сможем имитировать перья с широким кончиком, которые, скользя по бумаге, меняют форму в зависимости от наклона и давления.* Например, можно представить, что * Такие перья обычно называют "плакатными". — Прим. перев.
38 Глава 4- Перья элегантная линия нарисована при помощи пера, которое в начальный момент находилось в крайнем левом положении и было наклонено на 30°; по мере движения перо постепенно поворачивалось и в момент достижения правого конца приняло вертикальное положение. В позициях 2 и 3 перо двигалось горизонтально. Эта линия была получена посредством команды fill zu .. Z21 {right} .. {right} z^x --z3r{left} .. {left}z2r .. zir --cycle; т.е. мы попросили METAFONT залить область, ограниченную "левым" путем, ведущим из zu в Z21 и далее — в гз/, который продолжается прямой линией до гзГ) далее следует "правым" путем, ведущим в противоположном направлении: из z3r в z2r и далее — в z\r, и, наконец, возвращается по прямой в исходную точку z\\. Ключевые положения пера в данном примере представлены множествами, состоящими из трех точек, такими как (zu,zi,zir), которые соответствуют левому краю пера, его середине и правому краю. В определении контура средняя точка не участвует, но, как мы увидим в дальнейшем, она может быть полезной. Связи между такими тройками точек устанавливаются при помощи команды 'penpos1, которая определяет ширину пера и угол его наклона в том или ином положении. К примеру, положения пера в точках 1, 2 и 3 на приведенной выше линии установлены посредством команд penpos1(1.2pi,30); penpos2(1.0p£,45); penpos3 (0.8pt, 90); которые означают, что в положении 1 перо имеет ширину 1.2 pt и наклон 30° к горизонтали, и т.д. В общем случае положение пера задается командой penposk(b, d), где А; — номер или имя положения, 6 — ширина пера (в пикселях), ас? — угол наклона (в градусах). Угол наклона пера отсчитывается против часовой стрелки от горизонтали. Поэтому, если указан угол 0°, то правый край пера находится на b пикселей правее левого края; если указан угол 90°, то правый край находится на b пикселей выше его левого края; если указан угол —90°, то правый край находится на b пикселей ниже левого края. Если указан угол 45°, то правый край лежит на Ь/у/2 выше и на Ь/у/2 правее левого края; если же указан угол —45°, то правый край лежит на 6/\/2 ниже и на Ь/\/2 правее. Если угол наклона пера заключен между 90° и 180°, то его "правый" край, на самом деле, лежит левее "левого" края. В терминах принятых в картографии обозначений сторон света, угол 0° указывает на восток, тогда как угол 90° указывает на север, а —90° — на юг. Юго-западному направлению соответствует угол —135°, или, что то же самое, 4-225°. ► Упражнение 4.7 Какой угол соответствует направлению на северо-северо-запад?
Глава 4' Перья 39 ► Упражнение 4.8 Какой угол наклона имеет перо в положениях 1, 2, 3 и 4 на приведенной здесь округлой фигуре? [Указание: Каждый из углов кратен 30°. Заметьте, что z$r лежит левее zzi] ► Упражнение 4.9 Каковы будут координаты zu и z\r после применения команды <penpos1(10, —90)', если z\ — (25,25)? f Команда 'penposfc (6, d)' представляет собой краткую форму записи двух уравнений: 'zk — \[zki,Zkr\ и lZkr = zu + (Ь,0) rotated d\ Если вам покажется более удобным определить другое соотношение между zu, Zk и Zkr, то можете задать его другими уравнениями, вместо того, чтобы использовать команду penpos. Установив командой 'penpos1 связи между тремя точками, мы до сих пор не указали их точное расположение; известны лишь их положения друг относительно друга. Для того чтобы зафиксировать положение каждой тройки по горизонтали и вертикали, необходимо еще одно или два уравнения. К примеру, дополнением к трем командам penpos, при помощи которых была получена линия, изображенная на предыдущей странице, служили уравнения 2i = (0,2pt); z2 = (4р£, 0); х3 = 9pt; y3l = y2r. Каждому положению должно соответствовать одно уравнение для х и одно — для у или одно уравнение для z, в котором одновременно определяются и х, и у. Имитируя таким способом перо с широким кончиком, приходится писать пространные команды fill, что довольно утомительно. Поэтому в METAFONT для этого предусмотрена удобная аббревиатура: вместо использовавшейся ранее команды 'fill z\\ .. Z2i{right} .. {right} zzi -- zzr{left} .. {left} z2r •• z\r -- cycle' достаточно написать penstroke z\e .. z2e{right} .. {right}Zse. Буква 'e' означает край (edge) пера. Команда penstroke производит заливку области 'р./ -- reversep.r -- cycle', где p.l и р.г обозначают левый и правый пути, которые получаются заменой каждой 'е' на 7' и V, соответственно. /$\ Команду penstroke можно применять как к обычным, так и циклическим путям. JL Например, фигуру из упражнения 4.8 мы получили, указав 'penstroke z\e .. Z2e • • «гзе • • *4e • • cycle'. Вообще, командой 'penstroke' можно заменить команды fill z\r .. Z2r • • z$r .. Z4r • • cycle; unfill zu .. Z21 .. zzi .. zai .. cycle; или те же команды, следующие в обратном порядке, если точки (zir, 22г, ^зг, zat) находятся внутри, а точки (zu, z2i, zu, zai) — снаружи. ft У пражнение 4.10 На самом деле фигура из упражнения 4.8 была нарисована при помощи команды penstroke несколько более сложного вида, по сравнению с приведенным выше. В положениях 1 и 3 края линии идут вертикально, а в положениях 2 и 4 — горизонтально. Как автору удалось этого добиться?
40 Глава 4' Перья При помощи команды penstroke можно, например, нарисовать рубленую* букву Ч\ Предположим, заданы две переменные h и ги, определяющие высоту и ширину символа в пикселях. Кроме того, предположим, что (0 h\ ^wh^ существует некий параметр stem, определяющий начальную * ,г ширину пера. Эта ширина убывает до .9stem в середине линии, а угол наклона пера меняется от 15° до 10°: penpos^stem, 15); penpos2(.9stem, 12); penpos3(stem, 10); х\ — х2 — #з = -5tu; 2/1 = Л; 2/2 = -55/i; у3 = 0; penstroke zle .. Z2e{down) .. 2зе. Устанавливая х\ — Х2 = #з = .5г^, мы центрируем линию; устанавливая yi = /г и уз = 0, мы помещаем ее внутрь ограничивающей рамки так, что она лишь немного выходит за ее 31 з верхнюю и нижнюю границы. В предпоследней строчке этой программы мы видим то, с чем ранее никогда не сталкивались: переменная x^i переопределяется как х/б от расстояния до центра пера, отчего левая сторона линии становится чуть тоньше. Операция *:=' называется присваиванием; отличия между ':=' и *=' мы рассмотрим в главе 10. f Важно отметить, что такие имитированные перья имеют одно серьезное ограничение по сравнению с реальными перьями, которые используют каллиграфы. Дело в том, что в процессе работы левый и правый края пера, генерируемого командой penpos, не должны пересекаться, следовательно, совершая обход замкнутой кривой, мы вынуждены поворачивать перо. Рассмотрим, например, две кривые Кривая слева нарисована пером с широким кончиком фиксированной ширины при фиксированном угле наклона; следовательно, левым краем пера выведена левая часть внешней границы и правая часть внутренней границы. (Она построена посредством команд 'pickup pencircle xscaled O.Spt rotated 25; draw z\ .. z<i .. cycle'.) Фигура справа получена посредством команд 'penpos1 (0.8pt, 25); penpos2(O.Spt, 25); penstroke z\e .. Z2e • • cycle'. Как видим, в точках поворота пера отсутствуют важные фрагменты фигуры, так как они не принадлежат ни пути z\\ .. z^i .. cycle, ни пути z\T .. z2r • • cycle. fB заключение этой главы мы усовершенствуем символ Ж из главы 2. Этот символ нарисован при помощи пера постоянной ширины, поэтому несколько темноват в середине. Основная проблема, с которой мы сталкиваемся, работая со "статическими" перьями, состоит в том, что на стыке линий они дают темные пятна даже тогда, когда перья относительно тонки, а линии практически перпендикулярны. Мы хотим сделать линии в центре тоньше ровно настолько, чтобы избавиться от темного пятна, но при этом сохранить видимость того, что линии имеют постоянную толщину. * Рублеными называю! шрифты без засечек. — Прим. иерее.
Глава 4' Перья 41 Используя "динамические" перья, 'ЖЖЖ>ЮЮЮЮК>ЮК' легко усовершенствовать, превратив в '^+^>^^Ж^^^^^>^^>^^>Ю+0^<,: pickup pencircle scaled 6; top z\ — (0, h), top Z2 = (.5u>, h); top 23 — (w, h); hot za = (0, 0), bot z5 = ( bw, 0); hot zq = (w, 0), draw z2 .. 25; zv = .2b[zuz&); z& = .75[2i,26]; zz» = .25[23,24]; zA> = .75[23,24]; thetai := angle(z6 — Z\) + 90, */ie*a3 •= angle(z4 - z3) + 90, penposlt(b, thetai); penpos6,(b, thetai); penpos3, (6, £/ie£a з); penpo54/ (6, thetaз); penpos7(.6b, thetai), penpos8(.6b, theta3); 27 = 28 = -5[21,2б], draw zi .. 2i*; draw 26' . draw 23 .. 23/; draw 24/ . penstroke ziie{z6> — 2^} penstroke z3>e{z4i — 23/} 24; 27e • Z8e • •W • {*4' ~ ZV}Zb> -z3,}z« (0,0) (tu,0) Здесь 6 — диаметр пера в крайних точках; оператор 'angle' вычисляет направляющий угол вектора. Прибавляя к направляющему углу 90°, мы получаем перпендикулярное направление (см определения thetai и theta3). Изменять вертикальную линию 22 .. 25 нет нужды, так как две диагональные линии в точке их пересечения занимают участок, больший по ширине, чем вертикальная линия. f> Упражнение 4.11 Модифицируйте символ "гекс" так, чтобы его концы были обрезаны под прямым углом и чтобы он полностью помещался в ограничивающей рамке, как показано на рисунке справа (0,0) (iu,0) Очень важно, чтобы кончик пера был остро отточен и затачивался всякий раз, когда притупляется. Затупленным пером невозможно выводить "чистые" линии. — ЭДВАРД ДЖОНСТОН, Writing ii Illuminating & Lettering (1906) Я бы сравнил мощную вычислительную машину с очень большим и неуклюжим карандашом, который нужно долго затачивать и который нельзя зажать пальцами так, чтобы казалось, что он откликается на мои мысли. Но в нем заложен более тонкий механизм, и он будет писать с сумашедшей скоростью, как только я буду готов диктовать. — Р. Г. БРАК, Вычислительные аспекты некоторых комбинаторных проблем (1956)
5 МЕТАFONT в действии
Глава 5: METAFONT в действии 43 Сейчас самое время приостановить чтение и поиграть с компьютером, поскольку METAFONT лучше изучать методом проб и ошибок. (Одно из самых приятных свойств компьютерной графики состоит в том, что здесь ошибки зачастую более интересны и забавны, чем правильные решения.) Вероятно, вам придется попросить кого-нибудь рассказать о характерных особенностях той версии системы, которая у вас установлена. METAFONT работает примерно одинаково на всех машинах, но для разных компьютеров и различных печатающих устройств требуются разные интерфейсы. Мы будем предполагать, что вы располагаете компьютером с монитором, имеющим достаточно высокое для работы с графикой разрешение, а также некоторым устройством вывода (возможно, с низким разрешением), и что вы можете достаточно легко приспособить это устройство для работы с вновь созданными шрифтами. Вы готовы? Отлично. Вначале вам, конечно, необходимо загрузить операционную систему, а затем запустить METAFONT. Для этого, как правило, достаточно набрать в командной строке mf. Как только вы с этим справитесь, вас поприветствуют сообщением примерно такого содержания: This is METAFONT, Version 2.0 (preloaded base=plain 89.11.8) ** Две звездочки '**' — это запрос METAFONT на имя входного файла. Теперь напечатайте '\relax' — то есть обратную косую черту, г, е, 1, а, х — и нажмите (return) (или что-либо другое, что на вашей клавиатуре обозначает конец строки). Система METAFONT активизирована и готова создать шрифт с большим числом символов; однако вы сообщаете ей, что беспокоиться не стоит, так как на сей раз вы не зададите ей много работы. Обратная косая черта означает, что нужно не считывать файл, а следовать инструкциям, получаемым с клавиатуры; 'relax' значит "не выполнять никаких действий". В ответ машина напечатает одну звездочку: '*'. Это значит, что она готова получать инструкции (а не имя файла). Просто ради интереса напечатайте drawdot (35,70); showit; и нажмите (return). Не забывайте печатать точки с запятой. На экране появится более или менее круглая точка! Кроме того, машина вновь напечатает звездочку, ожидая ваших дальнейших указаний. Напечатайте drawdot (65,70); showit; и нажмите (return), чтобы получить еще одну точку. (Больше мы не будем вам напоминать о необходимости нажимать (return) в конце каждой строки, вводимой с клавиатуры.) Наконец, напечатайте draw (20,40)..(50,25)..(80,40); showit; shipit; end. В ответ система METAFONT нарисует кривую, проходящую через три заданные точки, выведет результат на экран и в выходной файл и завершит работу. На экране должно появиться сообщение '[0]'. Это значит, что система отправила на вывод символ, который имеет номер ноль в только что созданном "шрифте". Кроме того, она должна подтвердить, что создан выходной файл под названием 'mfput .2602gf \ (Имя mfput используется, если вы не ввели имя получше в ответ на ** в самом начале сеанса работы. Расширение 2602gf расшифровывается как "шрифт общего формата {generic font) с разрешением 2602 пикселей на дюйм". Данные из файла
44 Глава 5: METAFONT в действии mf put. 2602gf можно преобразовать в шрифты, совместимые с широким спектром устройств печати; мы называем формат этого файла "общим", поскольку сам он не соответствует ни одному из стандартных шрифтовых форматов.) Шрифт, который можно получить на основе данных из этого файла, особого интереса представляет, поскольку он содержит всего один символ и его разрешение, вероятно, не соответствует вашему устройству вывода. Однако он вполне годится для получения пробных оттисков; поэтому на следующем шаге вы должны преобразовать данные из mfput. 2602gf в изображение, пригодное к распечатке. На вашем компьютере должна быть установлена программа GFtoDVI. Применив ее к mfput.2602gf, вы получите файл mfput.dvi, который уже можно распечатать. Друзья-компьютерщики подскажут вам, как запустить GFtoDVI и как распечатать mfput.dvi, после чего в вашем распоряжении окажется чудный сувенир в память о вашей первой встрече с METAFONT. Если вы провели пробный сеанс только что описанным образом, то знаете, как совершается весь цикл, и, значит, готовы взяться за более сложный проект. Поэтому в следующем эксперименте вместо того, чтобы вводить входные данные с клавиатуры, мы будем работать с файлом. Создайте при помощи вашего излюбленного текстового редактора файл с именем io.mf ,* содержащий следующие 23 строки текста (ни больше, ни меньше): 1 mode_setup; 2 em#:=10pt#; cap#:=7pt#; 3 thin#:=l/3pt#; thick*:=5/6pt#; 4 o#:=l/5pt#; 5 define_pixels(em,cap); 6 define.blacker„pixels(thin,thick); 7 define_corrected_pixels(o); 8 curve.sidebar=round l/18em; 9 beginchar(M0",0.8em#,cap#,0); "The letter 0"; Ю penposl(thick,10); penpos2(.1[thin,thick],90-10); и penpos3(thick,180+10); penpos4(thin,270-10); 12 xll=w-x31=curve_sidebar; x2=x4=.5w; 13 yl=.49h; y21=-o; y3=.51h; y41=h+o; 14 penstroke zle{down}..z2e{right} 15 ..z3e{up}..z4e{left}..cycle; 16 penlabels(l,2,3,4); endchar; 17 def test_I(expr code,trial_stem,trial_width) = 18 stem#:=trial_stem*pt#; define_blacker.pixels(stem); 19 beginchar(code,trial_width*em#,cap#,0); "The letter I"; 20 penposl(stem,15); penpos2(stem, 12); penpos3(stem,10); 21 xl=x2=x3=.5w; yl=h; y2=.55h; y3=0; x21:=l/6[x21,x2]; 22 penstroke zle..z2e{down}..z3e; 23 penlabels(1,2,3); endchar; enddef; (Номера строк печатать не следует — они нужны лишь для того, чтобы мы могли на них ссылаться.) * Файл должен быть записан в текстовом формате. — Прим. перев.
Глава 5: METAFONT в действии Этот файл-образец назван в честь Ио — греческой богини ввода (input) и вывода (output) * Он несколько длинноват, но, набирая его, вы приобретете необходимый опыт. Так что, вперед, займитесь этим сейчас же. Для вашей же пользы. И вдумывайтесь в то, что печатаете; в примере демонстрируются важные свойства METAFONT, которые вы сможете изучить, создавая этот файл. Вот краткое пояснение того, что вы только что напечатали. Строка 1 содержит команду, которая присутствует в начале любой программы для METAFONT; она сообщает компьютеру, что он должен приготовиться работать в определенном "режиме" (mode)^ который в данный момент предпочтителен. (Такие файлы, как io.mf, могут использоваться как для получения пробных оттисков, так и для генерирования шрифтов разного размера, совместимых со множеством различных устройств; команда 'mode_setup' позволяет METAFONT приспособиться к поставленной задаче.) Строки 2-8 определяют параметры, которые будут использоваться при рисовании букв шрифта. Строки 9-16 содержат полную программу для рисования буквы 'О'; а строки 17-23 содержат программу, которая рисует букву Т множеством взаимосвязанных способов. Все это на первый взгляд выглядит устрашающе, но, приглядевшись повнимательнее, мы увидим, что завеса таинственности, которой окутана Ио, не так уж плотна, стоит лишь ее приоткрыть. Давайте потратим еще несколько минут и рассмотрим наш файл более подробно. В строках 2-4 определяются единицы измерения, не зависящие от режима работы; знаки '#' указывают на то, что речь идет о "точных", или "настоящих" единицах, которые остаются неизменными при любом разрешении. Так, один 'pt#' — это настоящий типографский пункт, одна 72.27-ая дюйма. В этом его отличие от 'pt\ который мы обсуждали в предыдущей главе, так как lpV есть число пикселей, соответствующее при текущем разрешении одному типографскому пункту. Величина 'pt#' никогда не меняется, а величина lpV автоматически устанавливается при выполнении команды mode_setup. Присваивания 'em#:=10pt#' и 'cap#:=7pt#' в строке 2 означают, что шрифт Ио имеет два параметра, ет и cap, абсолютные (не зависящие от режима работы) значения которых равны, соответственно, 10 и 7 пунктам. Утверждение 'def ine_pixels(em,cap)' в строке 5 переводит значения этих величин в пиксели. Например, если мы работаем со сравнительно низким разрешением 3 пикселя на пункт, то по выполнении компьютером инструкций из строки 5 окажется, что ет = 30 и cap =21. (Позже мы увидим, что ширина символов данного шрифта выражается в единицах era, a cap равняется высоте заглавной буквы. Таким образом, изменения в строке 2 влияет на ширину и/или высоту всех букв.) Шрифт Ио зависит также от параметров thick и thin, которые определяются в строке 3, и переводятся в пиксели в строке 6. Они используются для того, чтобы управлять шириной имитируемого пера при рисовании буквы О. Как показывает опыт, при работе с некоторыми устройствами вывода METAFONT дает лучшие результаты, если ширина перьев в пикселях чуть больше той, что получается переводом из настоящих единиц измерения, поскольку в процессе рисования черные пиксели иногда самопроизвольно "сгорают". Команда 'def ine.blacker„pixels' в строке б добавляет поправку, учитывающую особенности устройства, для которого разрабатывается шрифт. Например, если разрешение равно 3 пикселям на пункт, то значение thin, получаемое переводом в пиксели настоящих единиц в результате * Иниересно, догадывались ли об этом древние греки? — Прим. перев.
Глава 5: METAFONT в действии применения команды define_pixels, будет равно 1, однако define»blacker_pixels может установить значение thin близким к 2. Параметр 'о' в строке 4 представляет величину "перелета" кривых за пределы ограничивающих рамок. Она переводится в пиксели в строке 7 уже другим способом, позволяющим избежать еще одной проблемы, возникающей при печати с низким разрешением. Автор приносит извинения за то, что в этом обучающем примере позволил себе отвлечься на рассмотрение реальных проблем. Пока мы остановимся и не будем дальше лезть в эти туманные дебри, так как более тонкие детали мы рассмотрим в главе 11, вначале изучив как следует основы. Пока что, для нас важно лишь то, что в построении шрифтов участвуют параметры, выражающие физические длины. Эти параметры нужно перевести "точных" в "неточные", выраженные в пикселях величины, причем, если вы хотите добиться наилучшего результата, то это следует делать аккуратно. Как только METAFONT выполнит строку 7 нашей программы, параметры em, cap, thin, thick и о со значениями, выраженными в пикселях, будут готовы к дальнейшему использованию при построении букв нашего шрифта. В строке 8 определяется величина curve-sidebar, которой будет измеряться расстояние от левого и правого краев буквы 'О' до ограничивающей рамки. Вычисляется она посредством округления ^ега до ближайшего целого числа пикселей. Например, если ет = 30, то, округляя || = |, получаем значение curve.sidebar — 2; так что при данном разрешении справа и слева от буквы Ю' будут располагаться два столбца белых пикселей. Прежде чем двигаться дальше, мы должны обсудить странный набор слов и псевдослов из файла io.mf. Какие из терминов *mode_setup, 'em', 'curve.sidebar' и им подобных представляют собой элементы языка METAFONT, а какие из них сконструированы специально для примера с Ио? Так вот, выясняется, что в этом примере нет почти ничего, что было бы написано на чистом языке METAFONT, понятном компьютеру! Язык системы METAFONT — это настоящий язык низкого уровня, построенный так, чтобы легко адаптироваться ко множеству различных стилей программирования, и io.mf иллюстрирует лишь один из бесчисленных способов его использования. Большинство терминов в io.mf представляют собой условные обозначения "базового формата" — plain METAFONT, который представляет собой набор подпрограмм, описанный в приложении В. Примитивы системы METAFONT не предполагают непосредственного их применения, поскольку это навязало бы всем пользователям единый стиль. В самом начале сеанса работы в память компьютера загружается так называемый форматный файл (base file), так что множество стандартных соглашений сразу становится применимым. В приглашении системы METAFONT, о котором упоминалось в начале этой главы, говорится 'preloaded base=plain'. Это означает, что примитивный язык системы METAFONT уже расширен так, что в него включены все возможности, предусмотренные базовым форматом. В этой книге рассказывается не только про саму систему METAFONT, в ней также объясняется, как использовать соглашения ее базового формата. Точно так же, в книге Все про TeX описывается стандартное расширение системы TeX, называемое "базовым форматом TeX" (plain TeX). Базовые форматы систем TeX и METAFONT аналогичны друг другу. Обозначения mode.setup, define.pixels, beginchar, penpos и многое другое из того, что содержится в io.mf, представляют собой элементы базового формата, но не являются "встроенными" в METAFONT. Эти команды, а также соотноше-
Глава 5: METAFONT в действии 47 ния между "точными" и "неточными" переменными, определяются в приложении В. В нем оговаривается даже то, что z\ обозначает [х\,у\) — в саму систему METAFONT это соглашение не встроено. Набравшись опыта, вы сможете сами определять формат системы. Однако новичкам лучше начинать с работы в базовом формате. fEciiH в каких-нибудь важных приложениях вы используете другой формат, то можете сами создать версию METAFONT с каким угодно предзагружаемым форматом. Такие программы, как правило, называют специальными именами, поскольку сокращение 'mf' зарезервировано под версию, включающ} ю лишь то, что принято в этой книге за стандартный базовый формат. Так, автор создал специальную версию под названием 'cmrnf' исключительно для того, чтобы при разработке шрифтов семейства Computer Modern не было необходимости всякий раз загружать соответствующий форматный файл. f Существует простой способ заменить предварительно загруженный форматный файл другим: если первым символом, который вы введете в ответ на приглашение '**' будет амперсанд ('ft'), то прежде чем продолжить работу, METAFONT загрузит в память тот форматный файл, который вы укажете, заменив им предварительно загруженный. Например, если имеется не специальная программа 'cmmf', а форматный файл 'cm.base', то вы можете заменить в программе mf базовый формат форматом Computer Modern, напечатав в начале сеанса '&ст\ Если вы работаете с версией системы, которая не загружает автоматически базовый форматный файл, то первый эксперимент из этой главы вам не удастся провести описанным образом. Провести его вы сможете, если в самом начале напечатаете не просто '\relax', a 'ftplain \relax'. Эти соглашения в точности совпадают с аналогичными соглашениями, принятыми в системе Tgfi. В примере с шрифтом Ио используются следующие слова, которых нет в базовом формате METAFONT: era, cap, thin, thick, о, curve-sidebar, test-I, code, triaLstem, triaLweight и stem. Если вы измените эти слова на какие-либо другие (например, замените 'thin' и 'thick' на 't' и 'Т', соответственно, в строках 3, 6, 10 и 11), результаты останутся неизменными, если только эти замены не будут несовместимы с другими обозначениями, которые уже присутствуют в базовом формате METAFONT. Вообще, лучше всего выбирать для обозначения величин, фигурирующих в ваших программах, описательные имена, поскольку при таком выборе маловероятно, чтобы они вступали в конфликт с зарезервированными псевдословами, вроде penpos или endchar. Мы уже отмечали, что в строках 9-16 содержится программа рисования буквы 'О'. В главной части этой программы, т.е. в строках 10-15, используются идеи главы 4, но термины из строк 9 и 16 нам ранее не встречались. В системе METAFONT базового формата удобно начинать программу для каждого символа с утверждения вида beginchaг((кoд), (ширина), (высота), (глубина)); где (код) — это либо сам определяемый одиночный символ, скажем м0", либо число, представляющее номер позиции символа в шрифте. Остальные три величины определяют, соответственно, ширину, высоту и глубину ограничивающей рамки символа. Это информация необходима для того, чтобы программа верстки, например система TeX, могла использовать этот символ. Эти три величины должны выражаться в "точных", аппаратно-независимых единицах. ► Упражнение 5.1 Найдите высоту и ширину ограничивающей рамки, описанной в команде beginchar из строки 9 файла io.mf при помощи параметров, определенных в строке 2. Ответ выразите в типографских пунктах.
48 Глава 5: METAFONT в действии Каждая операция beginchar присваивает значения специальным переменным гу, h и d, представляющим, соответственно, ширину, высоту и глубину ограничивающей рамки текущего символа, округленные до ближайшего целого числа пикселей. В нашем файле-образце при помощи w и h устанавливаются определенные положения пера (см. строки 12, 13 и 21 файла io.mf). ► Упражнение 5.2 В условиях предыдущего упражнения, какими будут значения w и Л, если в одном пункте содержится ровно З.б пикселя? В конце строки 9 приводится фраза "The letter 0". Это — заголовок, который будет печататься на пробных оттисках. Команда 'endchar' в строке 16 завершает определение символа, начатое в строке 9; символ записывается в выходной файл и может быть отображен на экране. При этом желательно видеть расположение контрольных точек ^i, ^2, 2:3 и 2:4, которые используются при его построении, а также положения вспомогательных точек (21/,22/>2зь24/) и (^irj^2г>^Зг?^4г)? которые неявно присутствуют в командах penpos; утверждение 'penlabels(l,2,3,4)' означает, что на пробных оттисках эти точки будут помечены. Вот вам и буква О! Строки 17-23 аналогичны предыдущим, за исключением одного нового фрагмента: в них содержится небольшая самостоятельная программа, ограниченная разделителями 'def... enddef, обозначающими определение подпрограммы (subroutine). Иными словами, в этих строках определяется последовательность команд METAFONT, которые мы предполагаем выполнить несколько раз с небольшими вариациями. Эта подпрограмма называется test J ] в ней имеется три параметра: code, triaLstem и triaLwidth (см. строку 17). Дело в том, что мы хотим нарисовать несколько различных вариантов буквы Т, в которых ширина ее ножки и ширина всего символа будет различной, но программу мы хотим набрать только один раз. В строке 18 для данного значения triaLstem определены величины stem# и stem. В строках 19-23 программа рисования буквы I завершается (дословно переносится из главы 4). Ой, что-то мы слишком увлеклись разговором об io.mf. Самое время прекратить болтовню и всерьез заняться экспериментом номер 2, ведь гораздо интереснее поглядеть, что же компьютер делает с файлом. Хватит ли у вас смелости провести эксперимент номер 2? Конечно. Снова запустите METAFONT, но на этот раз в ответ на запрос машины ***' вы должны ответить 'io', поскольку именно это имя носит файл, так старательно подготовленный вами. (Файл можно назвать и полным именем, 'io.mf, но если расширение не указано, то METAFONT автоматически добавляет '.mf' к имени файла.) Если все идет нормально, то компьютер немного помигает лампочками, потом — миг — и на вашем экране появится большая буква 'О'. Но если вам везет так же, как автору, то, наверное, в первый раз не все пойдет гладко, и, вероятнее всего, причиной этого будут ошибки, допущенные при наборе текста программы. В программах для METAFONT содержится множество данных, записанных относительно лаконично, поэтому одна единственная ошибка может круто изменить их смысл. Проверьте, все ли вы набрали абсолютно точно: убедитесь, что вы не спутали букву Т с цифрой Т (особенно это касается строки 12, в которой написано 'х1Г, а не 'xll' или 'xll'); убедитесь, что вы различаете букву '0' и цифру '0' (особенно это касается строки 9); убедитесь, что пропечатали символы подчеркивания в таких словах, как 'mode\_setup'. Позже мы увидим, что METAFONT умеет ловко обходить
Глава 5: METAFONT в действии 49 большинство ошибок, но пока ваша задача — убедиться, что файл io.mf набран правильно. Как только вы получите работающий файл, компьютер нарисует вам 'О', и выдаст что-то вроде: (io.mf The letter 0 [79]) * Что бы это значило? Ну, '(io.mf' значит, что система начала читать ваш файл, а фраза 'The letter 0' была напечатана после того, как в строке 9 был обнаружен заголовок. Затем, дойдя до команды endchar в строке 16, система METAFONT выдала '[79]', чтобы дать вам понять, что она отправила на вывод символ под номером 79. (Это — ASCII код буквы 0; если вам необходимо знать все эти коды, то они приведены в приложении С.) Закрывающая скобка ')', следующая за '[79]', означает, что система закончила считывать файл, а символ '*' значит, что она ждет дальнейших указаний. Хммм... Файл содержит программы для рисования, как О, так и I. Почему же мы получили только букву О? Да потому, что в строках 17-23 подпрограмма test J лишь определяется, но никакие действия с этой подпрограммой не выполняются. Если мы хотим увидеть, что делает test.1, то должны активизировать эту подпрограмму. Поэтому давайте напечатаем test.I ("1",5/6,1/3); т.е. вызовем подпрограмму при code = "I", triaLstem = | и triaLwidth = |. Теперь компьютер нарисует букву I, соответствующую этим значениям параметров,* и даст запрос на новую команду. Теперь пора напечатать 'end', после чего система METAFONT сообщит, что сеанс работы завершен и создан выходной файл с именем 'io. 2602gf'. Прогнав этот файл через GFtoDVI точно так же, как и в эксперименте номер 1, мы получим два пробных оттиска с изображениями созданных нами букв 'О' и 'Г. Мы намеренно не приводим здесь эти изображения; чтобы увидеть результаты, вы должны сами проделать этот эксперимент. Получив пробные оттиски, рассмотрите их. Они наглядно демонстрируют возможности конструкций, моделирующих перья с широким кончиком, которые мы рассматривали в главе 4. Сравним букву 'О' и программу, при помощи которой она нарисована. Обратите внимание на то, что: (a) penpos2 в строке 12 делает кривую чуть толще у основания и у вершины, (б) в силу цепочки равенств 1хц = w — х& = curve.sidebar1 из строки 12 правый край кривой отстоит от правого края ограничивающей рамки ровно настолько, насколько левый — от левого, (в) в строке 13 точка 1 помещается ниже точки 3. Буква 'Г на пробном оттиске должна быть очень похожей на соответствующую иллюстрацию из конца главы 4, но несколько больше. fHa полученном вами пробном оттиске буквы 'О' должны быть обозначены двенадцать ключевых точек, но только десять из них будут помечены, т.к. для меток в точках 2 и 4 недостаточно места. Информация о не поместившихся метках обычно приводится в верхнем правом углу оттиска. Например, с4 = 41 + (-1,-5.9)' означает, что точка z\ расположена на один пиксель левее и на 5.9 пикселей ниже помеченной точки z±\. (В некоторых версиях системы эта информация не выдается, поскольку для этого не всегда хватает места.) * Если, конечно, при наборе строк 17-23, в которых определяется подпрограмма lest_/, не была допущена ошибка.
50 Глава 5: METAFONT в действии На пробных оттисках, полученных в эксперименте номер 2, показаны и ключевые точки, и ограничивающие рамки. Но эта дополнительная информация может зрительно накладываться на само изображение символа. Существует простой способ получения пробных оттисков, позволяющих оценивать результаты не с логической, а с эстетической точки зрения. Получение таких оттисков — цель нашего следующего эксперимента. Для того чтобы проделать эксперимент номер 3, нужно сделать следующее. Запустив, как обычно, METAFONT, наберите \mode=smoke; input io в ответ на '**'. Этим вы вновь загрузите файл io.mf, установив предварительно "дымовой" режим. (Как и в эксперименте номер 1, командная строка начинается с *\\ поэтому компьютер знает, что вы не вводите в самом начале имя файла.) После этого проведите сеанс до конца точно так же, как в эксперименте номер 2, набрав 'test.lC'I" ,5/6,1/3) ; end', и примените GFtoDVI к полученному файлу io.2602gf. На пробных оттисках будут изображены те же символы, но на этот раз они будут темнее, и на них не будет помеченных точек. Ограничивающие рамки будут лишь обозначены по углам небольшими метками. Вы можете приложить рамки одну к другой и прикрепить получившееся к стене. Посмотрев с некоторого расстояния, вы увидите, как будут выглядеть эти символы, напечатанные типографским способом при высоком разрешении. (Этот режим работы называется "дымовым" (smoke), по аналогии с дымовыми оттисками, которые традиционно использовались резчиками печатей. Они держали только что вырезанную печать в дыме свечи, пока она не покрывалась копотью, а затем прижимали к бумаге. Таким образом они могли составить четкое представление о символе и решить, нужно ли его подправить.) /gK Кстати, во многих операционных системах вызвать METAFONT и указать имя JL входного файла можно в одной командной строке, не дожидаясь приглашения '**'. Так, в случае эксперимента номер 2 командная строка будет иметь вид 'mf io'. Точно так же, в начале экспериментов 1 и 3 в таких операционных системах можно использовать однострочные команды 'mf \relax' и 'mf \mode=smoke; input io'. Вы можете попробовать, пройдет ли это на вашем компьютере, или же спросить у кого-нибудь — возможно, для этого предусмотрено какое-нибудь похожее сокращение. В экспериментах 1, 2 и 3 мы научились получать пробные оттиски символов, но так и не получили ни одного шрифта, который можно было бы использовать для набора текста. Поэтому, приступая к эксперименту номер 4, мы зададимся целью разработать новый шрифт. Допустим, мы вполне довольны буквой О из io.mf, и хотим получить рубленую букву I, которая генерируется в общем виде подпрограммой testJ, но не уверены, какова должна быть толщина ножки I для того, чтобы она хорошо сочеталась с О. Более того, мы не знаем точно, сколько белого пространства нам нужно оставить по обе стороны от I. Поэтому желательно иметь несколько образцов набора с использованием ряда различных I. Идеальный способ для этого — создать пробный шрифт с высоким разрешением и просмотреть его в нормальном размере. Однако это может оказаться слишком дорогостоящей затеей, так как хорошее оборудование для печати обычно запускают только для выполнения большого объема работы. Наилучшая из альтернатив — использовать принтер с низким разрешением, но увеличить изображение
Глава 5: METAFONT в действии 51 так, чтобы зрительно повысить разрешение. Так мы и поступим, поскольку в этом случае научимся не только создавать шрифты, но и увеличивать символы. Для начала эксперимента номер 4 снова запустите METAFONT и в ответ на '**' наберите \mode=localfont; mag=4; input io. Система METAFONT базового формата узнает в слове localf ont название режима работы, в котором генерируются шрифты для вашего "стандартного" устройства вывода. Уравнение mag=4 означает, что будет генерироваться шрифт, увеличенный вчетверо; то есть результат будет в 4 раза больше обычного. Как и прежде, компьютер прочтет io.mf, но на этот раз он не высветит на экране 'О'. В режимах, предназначенных для создания шрифтов, символы, как правило, на экран не выдаются, так как при генерировании компьютером предварительно разработанного шрифта желательно, чтобы он работал как можно быстрее. Все, что вы увидите, — это '(io.mf [79])' и '*'. Вот теперь начинается кое-что поинтересней: вы должны набрать code=100; for s=7 upto 10: for w=5 upto 8: test_I(incr code,s/10,w/20); endfor endfor end. (Слово 'upto' должно быть напечатано слитно.) Повторения операций при помощи конструкции 'for.. .endfor' мы изучим в главе 19. Эта небольшая программа генерирует букву I в 16 вариантах: с шириной ножки, равной ^, ^, ^ и Щ pt, и шириной символа, равной ^, ^, ^ и ^ em. Эти шестнадцать пробных символов займут в шрифте позиции с 101 по 116, которые в кодировке ASCII содержат строчные буквы с е по t, включительно. (Если бы мы задали значение 'code' равным не 100, а каким-либо другим, то эти коды были бы другими. Конструкция 'incr code' увеличивает значение code на 1 и выдает это новое значение; таким образом, каждое обращение к testJ имеет свой номер кода.) В этом сеансе METAFONT генерирует не только файл шрифта "общего" формата с именем io.nnngf, но и так называемый метрический файл с именем io.tfm, который подсказывает издательской системе типа TeX, как использовать этот новый шрифт. Теперь, чтобы завершить эксперимент номер 4, нужно подключить к работе TeX. Мы сделаем несколько пробных образцов с использованием нового шрифта, чтобы определить наилучшую из букв Т. Здесь вам, возможно, понадобится помощь какого-нибудь местного компьютерного гения, поскольку может оказаться, что файл io.tmf нужно перенести в определенное место, где Т£Х и прочие издательские программы смогут его отыскать. Кроме того, вам понадобиться прогнать io.nnngf через программу, которая переведет его в шрифтовой формат, используемый вашим устройством вывода. Но будем надеяться, что обе эти операции оказались для вас достаточно простыми, и в вашу систему был успешно инсталлирован новый шрифт с именем 'io'. Этот шрифт будет содержать семнадцать букв, а именно: 0 и шестнадцать букв I, причем I окажутся на тех позициях, которые обычно занимают е, f, ..., t. И, кроме того, шрифт будет увеличен в 4 раза. f Увеличение шрифта отражается на имени его файла. Например, если режим localf ont соответствует устройству с разрешением 200 пикселей на дюйм, то шрифт io общего формата с увеличением в 4 раза будет называться *io.800gf \
52 Глава 5: METAFONT в действии Вы можете пользоваться этим шрифтом в системе TJtjX точно так же, как и любым другим, но для целей эксперимента 4 лучше использовать специальный пакет, разработанный для тестирования шрифтов. Для этого нужно запустить TeX, который вызывается точно так же, как и METAFONT, с той лишь разницей, что вместо 'mf нужно набрать 'tex', и в ответ на приглашение '**' напечатать 'testf ont'. (Программа 'testf ont' должна быть доступна в вашей системе; в противном случае вы сами или кто-нибудь другой может набрать ее, используя материал приложения Н.) Машина попросит вас ввести имя шрифта, который вы хотите протестировать. Наберите io scaled 4000 (что на жаргоне TJtjX означает: "шрифт io, увеличенный в 4 раза"), так как именно это было только что создано системой METAFONT. Теперь машина попросит вас ввести команду тестирования. Чтобы провести тест сочетаний, вы должны ответить \mixture (Не забудьте про обратную косую черту.) Вас попросят ввести три буквы: фоновую (background), начальную (starting) и конечную (ending). Введите, соответственно, '0', 'е' и 't'. В результате вы получите шестнадцать строк, первая из которых будет содержать сочетание букв 0 и е, вторая — сочетание 0 и f и так далее. Завершая эксперимент номер 4, дайте системе TeX команду '\eiid' и распечатайте файл testf ont. dvi, который она выдаст. Если все пройдет нормально, то у вас будет шестнадцать строк, скажем, вида 'ОЮОНОООПЮГ, с различными буквами I в разных строках. Чтобы при выборе строчки, которая выглядит наилучшим образом, соседние строки не влияли на ваше восприятие, удобно использовать два чистых листа бумаги, прикрывая ими все строки, кроме той, которую вы оцениваете. При этом следует помнить, что буквы имеют размер, в четыре раза превышающий тот, в котором нужно было бы рассматривать окончательный вариант шрифта, поэтому вы должны рассматривать образцы издали. При ксерографическом их уменьшении могут возникнуть искажения, в результате которых создастся ложное впечатление. Иногда при слишком уж пристальном рассмотрении подобных объектов они либо все кажутся одинаково плохими, либо все — одинаково хорошими; как правило, первое впечатление оказывается более верным, чем результаты логического анализа. Во всяком случае, вы должны прийти к определенному мнению, относительно того, какими должны быть значения ширины ножки и ширины символа у порядочной буквы 'Г; их можно включить в программу io.mf, а разделители 'def и 'enddef — удалить. После этого можно перейти к построению других символов, которые составят компанию вашим I и О. К тому же вы всегда можете вернуться и подкорректировать буквы после того, как испытаете их в более разнообразных сочетаниях. <&)<&)* Упражнение 5.3 JL JL Богиню Ио в Египте называли Исис (Isis). Разработайте в ее честь букву 'S'. В задачи этой книги не входит описание художественных аспектов разработки шрифтов. Пример с io.mf приведен лишь для того, чтобы вы составили представление о всем процессе создания шрифта — от разработки отдельных символов до их использования в печатных документах. Теперь мы должны вернуться в компьютерный мир для того, чтобы изучить еще несколько практических аспектов использования системы METAFONT.
Глава 5: METAFONT в действии 53 Эта глава довольно длинная. Но соберитесь с духом: осталось провести еще один эксперимент, и вы будете знать о системе METAFONT достаточно, чтобы в дальнейшем смело работать с ней. Единственное, чего вам для этого не хватает, — это информации о том, как поступать с сообщениями об ошибках. Время от времени METAFONT приостанавливает работу и спрашивает вас, что делать дальше. Возможно, это уже случилось, и вы впали в панику. Сообщения об ошибках могут напугать, если вы к ним не готовы, но могут быть и забавными, если к ним правильно относиться. Просто нужно помнить, что компьютер не умеет обижаться, и что никто не собирается использовать ваши ошибки вам во вред. Тогда вы обнаружите, что работа с METAFONT вовсе не страшна, а, напротив, обогащает вас опытом. Первый этап эксперимента номер 5 — намеренное внесение ошибок во входной файл. Создайте копию файла io.mf и назовите ее badio.mf. Затем измените строку 1 в badio.mf следующим образом: mode setup; '/, an intentional error! (т.е. удалите символ подчеркивания в mode_setup и допишите комментарий, который означает "намеренно допущенная ошибка"). Кроме того, в строке 2 замените первую точку с запятой (';') двоеточием (':'); в строке 10 измените 'thick, 10' на 'thick, 10' (т.е. замените цифру Т буквой Т); наконец, в строке 11 замените 'thin' на 'thinn'. В результате вы получите четыре типичные опечатки, допускаемые при наборе текста программы. Интересно, приведут ли они к каким-нибудь катастрофическим последствиям? Теперь снова запустите METAFONT, но, когда компьютер выдаст '**', не подстраивайтесь под него, а напечатайте 'mumble'. (Коль скоро вы собираетесь делать намеренные ошибки, то можете немного подурачиться.) Система METAFONT пожалуется, что она не может найти файл по имени mumble.mf, и попросит вас ввести другое имя. На этот раз просто нажмите (return), и вы увидите, что лучше задать имя существующего файла. Поэтому наберите 'badio' и ждите, когда METAFONT дойдет до одного из ложных шагов в этой пародии на программу. Ага! Машина скоро остановится, выдав примерно следующее: » mode.setup ! Isolated expression. <to be read again> * 1.1 mode setup; У, an intentional error! Сообщения об ошибках, которые выдает METAFONT, начинаются со знака '!'; иногда ему предшествует одно-два математических выражения, которые содержатся в строке, начинающейся с '»'. Кроме того, после каждого сообщения об ошибке идут строки контекста, которые показывают, что считывал компьютер в момент обнаружения ошибки. Такие строки контекста появляются парами; верхняя строка пары (например, 'mode setup;') показывает, что система METAFONT уже успела считать, и откуда это взялось ('1.1', т.е. строка номер 1); нижняя же строка (в данном случае 7, an intentional error!') показывает остаток строки, которую система METAFONT не дочитала. В данном случае, имеются две пары строк контекста; верхняя пара относится к точке с запятой, которая уже была считана, но будет считываться снова, поскольку не вяжется с предыдущим материалом.
Глава 5: METAFONT в действии Вам не нужно доставать бумагу и карандаш и записывать сообщения об ошибках, поскольку METAFONT всегда ведет "стенограмму", или log-файл, в который записывает все, что происходит во время сеанса работы. Так, у вас теперь должен иметься файл с именем io.log, содержащий стенограмму эксперимента номер 4, а также файл mf put. log, содержащий стенограмму эксперимента номер 1. (Старая стенограмма эксперимента номер 2 была, скорее всего, переписана заново, когда вы проводили эксперимент номер 3, а затем еще раз, когда вы проводили эксперимент номер 4, так как все три эти стенограммы записывались в файл io. log.) По окончании эксперимента номер 5 вы получите файл badio. log, который будет служить полезным напоминанием об ошибках, которые необходимо исправить. Знак вопроса — '?', который появляется после фрагментов контекста, означает, что METAFONT хочет посоветовать вам, что делать дальше. Если вы столкнулись с сообщением об ошибке впервые или вы просто забыли, какой ответ от вас ожидается, то можете набрать '?' (не робейте, попробуйте!); METAFONT выдаст в ответ следующее: Type <return> to proceed, S to scroll future error messages, R to run without stopping, Q to run quietly, I to insert something, E to edit your file, 1 or ... or 9 to ignore the next 1 to 9 tokens of input, H for help, X to quit. Это варианты выбора. Для того, чтобы выбрать, каким образом продолжать работу, вы можете: 1. Просто нажать (return). METAFONT попытается сделать все, что в ее силах, чтобы исправить ошибку, и продолжит работу. 2. Набрать 'S'. METAFONT будет работать, не останавливаясь, чтобы получить инструкции, даже если в дальнейшем встретятся ошибки. Последующие сообщения об ошибках промелькнут на вашем экране, возможно, быстрее, чем вы успеете их прочитать. Но они будут записаны в log-файл, и вы сможете внимательно изучить их на досуге. Таким образом, ввести 'S' — это все равно, что нажимать (return) в ответ на каждое сообщение. 3. Набрать *R\ Это почти то же самое, что и 'S', но сильнее, поскольку дает системе METAFONT указание не останавливаться ни при каких обстоятельствах, даже если она не может найти файл. 4. Набрать 'Q'. Это то же, что и 'R', но сильнее, поскольку не только дает METAFONT указание работать не останавливаясь, но и подавляет весь дальнейший вывод на экран. Это быстрый, но довольно бессмысленный режим работы (предназначенный для работы METAFONT без вмешательства человека). 5. Набрать 'I', а затем — текст, который вы хотите вставить. METAFONT считает этот текст до того, как продолжить чтение. 6. Набрать число, не превосходящее 100. Система METAFONT удалит из входного файла указанное количество лексем, которые должны быть считаны сразу после того, как она возобновит работу. Затем она снова остановится, чтобы вы могли просмотреть результат. (Лексема (token) — это имя, число или символ, которые METAFONT считывает, как самостоятельную единицу; например, 'mode', 'setup' и ';' — первые три лексемы в файле badio.mf, а 'mode_setup' — первая лексема в файле io .mf. Точный смысл этого понятия объясняется в главе 6.)
Глава 5: METAFONT в действии 55 7. Набрать 'Н'. Это то, что вы должны сделать сейчас и делать всякий раз, когда столкнетесь с сообщением об ошибке, которое вам ранее никогда не встречалось. На случай каждой ошибки, которую METAFONT может распознать, у нее имеется два встроенных сообщения: краткое и развернутое. Сначала выдается краткое сообщение (например,'! Isolated expression.'). Развернутое сообщение система выдает на экран, если вы просите помощи, набирая 'Н', и записывает в log-файл, когда вы прокручиваете сообщения об ошибках в безостановочном режиме. Развернутые сообщения служат дополнением к кратким. В них система METAFONT объясняет, в чем, по ее мнению, состоит проблема, и зачастую предлагает способ решения. 8. Набрать 'X'. Это означает "выход". Получив эту команду, METAFONT прекращает сеанс работы, внеся предварительно последние коррективы в log- файл и те символы, которые уже были выведены в файл с расширением gf и/или tfm. Текущий (незаконченный) символ выведен не будет. 9. Набрать 'Е'. Это действует так же, как и 'X', но, кроме того, подготавливает компьютер к редактированию файла, который METAFONT считывает на данный момент, с текущей позиции. Благодаря этому вы с легкостью сможете внести поправки, прежде чем предпринять следующую попытку. Набрав 'Н' (или 'h', что тоже работает), вы получите сообщение, в котором объясняется возникшая проблема: считав математическую величину (т.е. mode.setup), система METAFONT не обнаружила после нее '=' или ': =', и поэтому не может ничего сделать. В главе 6 объясняется, что пробел между лексемами эквивалентен точке (например, 'mode setup'эквивалентно'mode.setup'). Если бы KOMaHfla'mode.setup' была написана правильно, то система METAFONT узнала бы в ней имя подпрограммы, предусмотренной базовым форматом, но встроенной команды 'mode.setup' базовый формат METAFONT не содержит. Поэтому, 'mode.setup' выглядит несуразно, и METAFONT догадывается, что здесь что-то не так. В нашей ситуации можно просто нажать (return) и продолжить работу, поскольку, если не выбран никакой специальный режим работы, то нет реальной необходимости выполнять команду mode-setup. METAFONT просто забудет об этом нестандартном выражении и продолжит работу. При этом она проигнорирует оставшуюся часть строки 1, так как все, что следует за знаком 'У,', игнорируется. (Об этом говорится в главе 6; таким образом удобно вставлять комментарии в программы для METAFONT.) Итак, изменения, которые мы внесли в строку 1 файла badio.mf, оказались относительно безобидными. Но практически сразу METAFONT натолкнется на изувеченную точку с запятой в строке 2: ! Extra tokens will be flushed. <to be read again> 1.2 em#:=10pt#: cap#:=7; ? Что бы это значило? Чтобы узнать ответ, наберите 'Н'. Система METAFONT не знает, что ей делать с ':' в этом месте файла, и поэтому планирует выйти из затруднения, избавившись от всего, что в обозреваемой строке предшествовало точке с запятой. Вот теперь (return) лучше не нажимать, поскольку, сделав это, мы потеряем важное присваивание 'cap#:=7pt#', а это повлечет за собой более серьезные ошибки.
56 Глава 5: METAFONT в действии В этом месте вы могли бы набрать 'X' или 'Е', чтобы выйти из METAFONT и исправить ошибки в строках 1 и 2 перед повторным сеансом. Однако, как правило, лучше не прерываться. Стараясь обнаружить как можно больше ошибок за один сеанс, вы повышаете производительность, и одновременно снижаете затраты машинного времени. Опытный пользователь прерывает сеанс лишь в том случае, если встреченная ошибка не может быть исправлена или он почти наверняка знает, что больше ошибок не встретится. В данном случае проблему нужно решать в два этапа: сначала дать METAFONT указание стереть следующую лексему (ненужный символ ':'), набрав 'Г, а затем вставить точку с запятой, набрав 'I;'. Эта точка с запятой предотвратит сброс оставшейся части строки 2, так что теперь все пойдет хорошо до тех пор, пока METAFONT не дойдет до следующей исковерканной строки. Следующее сообщение об ошибке более пространно, чем предыдущие, так как эту ошибку METAFONT обнаружила при попытке выполнить команду penpos. Поскольку операция penpos не является примитивной (она определена в базовом формате), то приводится гораздо больше контекста: » 1о ! Improper transformation argument. <to be read again> penpos->...(EXPR3),0)rotated(EXPR4); x(SUFFIX2)=0.5(x(SUFF... 1.10 penpos1(thick,10) ; penpos2(.1[thin,thick],90-10) ; На первых порах такие сообщения об ошибках будут казаться вам абсолютно бессмысленными, так как большинство из того, что вы видите, — это примитивные коды системы METAFONT, которых вы вовсе не писали. Но это впечатление пройдет, когда вы приобретете необходимый опыт и прочувствуете, как выполняются внутренние операции. Нижняя строка показывает, до какого места в файле дошла система METAFONT: она уже считала 'penposl(thick, 10)', но еще не успела считать точку с запятой в строке 10. Команда penpos раскладывается в длинную последовательность лексем; этот список настолько длинный, что не может уместиться в двух строках, и многоточия '...', которые в них встречаются, указывают на то, что в этом месте определение программы penpos было усечено. В разложениях сложно устроенных программ часто присутствуют значения параметров; например, в данном случае '(EXPR3)' и '(EXPR4)' соответствуют параметрам 'thick' и '10', a '(SUFFIX2)' соответствует 'Г. Система METAFONT обнаружила ошибку, натолкнувшись на фразу 'rotated(EXPR4)'; значение (EXPR4) — неопределенная величина (а именно: '10', что METAFONT трактует, как '/о', т.е. переменную с нижним индексом), а аргумент операции вращения, задаваемой при помощи оператора rotated, должен иметь известное числовое значение. Вращение — это частный случай того, что на языке METAFONT называется трансформацией; поэтому, описывая эту ошибку, METAFONT сообщает вам о "неправильном аргументе трансформации". Получив такое многострочное сообщение об ошибке, причину проблемы, как правило, следует искать в нижней строке (поскольку в ней содержится то, что вы напечатали) и в верхней строке (в ней содержится то, что вызвало взрыв негодования системы). Обычно в этих местах удается "вычислить" ошибку.
Глава 5: ME/ RFONT в действии 57 Если вы теперь напечатаете 'Н', то узнаете, что METAFONT решила просто продолжить работу, и не выполнять требуемое вращение. Таким образом, если вы в ответ нажмете (return), то METAFONT продолжит работу, начиная с того места в программе, где заканчивается 'penposl(thick,0)'. Вред нанесен относительно небольшой, но, прежде чем продолжить, можно полностью исправить ошибку. Задайте вращение правильно, набрав I rotated 10 METAFONT выполнит вращение на 10° так, как если бы вместо '10' стояло '10'. Что же происходит далее в эксперименте номер 5? METAFONT "икнет", наткнувшись на последнюю из ошибок, которые мы намеренно внесли в наш файл. Однако на этот раз сообщений не появится, потому что со строкой 11 все в порядке. (Переменная thinn не определена, но неопределенные величины — не проблема, если только вы не совершаете с ними сложных действий, вроде вращения. Программы для METAFONT, как правило, состоят из уравнений, в которых присутствует много неопределенных величин; лишь со временем переменные приобретают точные значения. Поэтому орфографические ошибки невозможно распознать до последней минуты.) И вот, наступает момент истины, когда badio пытается нарисовать путь, проходящий через неизвестную точку, и вы получаете сообщение об ошибке, которое выглядит еще более устрашающе, чем предыдущее: » 0.08682thinn+144 ! Undefined х coordinate has been replaced by 0. <to be read again> { <for(l)> . . .FFIXOHup}. .z4(SUFFIX0){ left}..cycle; ENDF0R penstroke->...ath_.e:=(TEXT0);endfor .if.cycle.path_.1:eye... <to be read again> 1.15 ... ..z3e{up}..z4e{left}..cycle; Ух ты! Что это такое? В разложении команды penstroke присутствует "цикл for", и ошибка обнаружена где-то внутри него. Из выражения '0.08682thinn+144' в первой строке сообщения можно сделать вывод, что виновато во всем неправильно написанное 'thin'. Если этой информации недостаточно, то еще одной подсказкой вам послужит то, что последним было считано 'z4(SUFFIX0)'; (SUFFIX0) — это текущее значение переменной цикла, a '<for(l)>' указывает, что значение, о котором идет речь, — '1', и потому zw выглядит подозрительно. (Из сообщения следует, что неопределенную координату х, ставшую виновницей этой ошибки, можно записать как хм = Q.0S6S2thinn + 144.) Последствия ошибки в строке 11 столь пагубны, что в этом месте имеет смысл набрать 'X' или 'Е'. Однако вместо этого, ради интереса наберите 'S'. Этим вы даете системе METAFONT указание ломиться вперед, делая все, что возможно, для исправления оставшихся ошибок. (Несколько проблем еще возникнет, так как некоторые из оставшихся переменных зависят от параметра 'thinn'.) Прежде чем достигнуть конца файла, METAFONT нарисует очень странную букву О. Чтобы завершить эксперимент, вы должны дать команду 'end'.
58 Глава 5: METAFONT в действии Если теперь вы снова захотите отредактировать файл badio.mf, то наверняка заметите, что в строке 2 по-прежнему вместо точки с запятой стоит двоеточие. Просьба стереть это двоеточие и вставить что-то другое, не означает, что изменениям подвергнется сам файл-программа. Однако файл-стенограмма badio.log содержит записи обо всех ошибках, и туда всегда можно заглянуть, если вы захотите их исправить. (Отчего бы вам не заглянуть в badio.log и io.log и не познакомиться с log-файлами?) f> Упражнение 5.4 Допустим, вы проделываете эксперимент номер 3 не с io, ас badio, и начали его, указав '\mode=smoke; input badio'. Затем вы захотели исправить ошибку в строке 1, вставив правильную команду mode.setup, вместо того, чтобы нажать (return), так как "дымовой" режим работы устанавливает программа mode_setup. К сожалению, если вы попытаетесь набрать 'I mode_setup' в ответ на сообщение о "нестандартном выражении", то из этого ничего не выйдет. Что нужно набрать вместо этого? Проделав пять экспериментов из этой главы, вы научились своими руками (1) создавать пробные оттиски различных видов, в том числе "дымовые оттиски''; (2) создавать и тестировать новые шрифты; а также (3) сохранять спокойствие, когда METAFONT делает вам строгие предупреждения. Поздравляем вас! Вы стоите на пороге, за которым открывается много других возможностей. Читая последующие главы, желательно проводить пробные сеансы работы, ставя эксперименты собственного изобретения. ► Упражнение 5.5 Поскольку эта глава была ну очень длинной, вы должны теперь выйти на свежий воздух и выполнить несколько физических упражнений.
Глава 5: METAFONT в действии 59 Узнаем же теперь, как исступленье вдруг нашло — Она о бедствиях ей повествует многих. — ЭСХИЛ, Прикованный Прометей (470 г. до н.э.) Студентов, желающих использовать графические методы как инструмент, не следует слишком уж настойчиво убеждать в том, что практический навык и теоретические знания одинаково важны. Здесь применимо часто цитируемое педагогами изречение: "Мы учимся, делая". — ТЕОДОР РАННИНГ, Графическая математика (1927)
6 Как METAFONT считывает входные
Глава 6: Как METAFONT считывает входные данные 61 До сих пор мы говорили лишь о том, что система METAFONT умеет, но умалчивали о том, чего она не умеет. Во множестве примеров мы встречались с командами, которые METAFONT понимает, но нигде не упоминали о том, что многие фразы компьютер сочтет лишенными всякого смысла. Настало время подойти к делу более системно и изучить точные правила языка системы METAFONT. Мы выясним, что для машины имеет смысл, и научимся избегать грамматически неверных высказываний. Программа для METAFONT состоит из одной или более строк текста, где каждая строка строится из букв, чисел, знаков препинания и прочих символов, которые имеются на стандартной клавиатуре. Всего может быть задействовано 95 символов, а именно: пробел плюс 94 видимых символа, предусмотренных стандартом ASCII. (Кодировка ASCII, в которой кодовые числа от 33 до 126 приписаны 94 специальным символам, описана в приложении С. При написании программ для METAFONT схема кодировки не имеет особого значения; важно лишь то, что могут использоваться 94 различных непустых символа.) Система METAFONT преобразует каждую строку текста в последовательность лексем (tokens), и программист должен иметь отчетливое представление о том, как происходит это преобразование. Лексемы являются самостоятельными лексическими единицами, которые управляют действиями компьютера. Они — элементарные кирпичики, из которых могут быть выстроены осмысленные последовательности инструкций. В конце предыдущей главы мы уже вкратце упоминали о лексемах; теперь остановимся на них подробно. Строка 9 файла io.mf из предыдущей главы — это типичный пример того, с чем приходится сталкиваться машине: beginchar ("О",0.8em#,cap#,0); "The letter 0й; Считывая эти ASCII-символы, METAFONT обнаруживает шестнадцать лексем: [begincharl Щ [ЧГ] [7] [О] [ш] [#] Q Э В □ 0 И Ш l"The letterT17] Q] Две из них — "0м и "The letter 0" — называются лексемами-цепочками (string tokens), поскольку представляют собой цепочки символов. Две другие — '0.8' и '0' — называются числовыми лексемами (numeric tokens), поскольку представляют числа. Остальные двенадцать — 'beginchar', Ч' и т.д. — называются символьными лексемами (symbolic tokens); смысл таких лексем может меняться по ходу программы, тогда как лексемы-цепочки и числовые лексемы всегда имеют одно, строго определенное значение. Заметьте, что такие группы символов, как 'beginchar', рассматриваются как единое целое; это относится и к таким наборам, в которых буквы перемежаются символами подчеркивания, как, например, в 'mode.setup'. Правила, которые мы вскоре изучим, объясняют, что эти и некоторые другие группы символов, такие, как '0.8', и ':=', являются неразложимыми лексемами. Система METAFONT имеет собственный метод определения того, где заканчивается одна лексема и начинается другая. Правила грамматики удобно формулировать, используя специальные обозначения, которые были введены примерно в I960 году Джоном Бэкасом (John Backus) и Питером Нором (Peter Naur). "Части речи" представляются величинами с определенными именами, заключенными в угловые скобки, а способы, с помощью которых эти величины могут формироваться из более простых единиц, выражаются
62 Глава 6: Как METAFONT считывает входные данные посредством правил синтаксиса,. Существует, например, три правила синтаксиса, которые описывают все возможные виды числовых лексем: (десятичная цифра) —►0|1|2|3|4|5|6|7|8|9 (цепочка цифр) —> (десятичная цифра) | (цепочка цифр) (десятичная цифра) (числовая лексема) —> (цепочка цифр) | . (цепочка цифр) | (цепочка цифр). (цепочка цифр) Первое правило говорит о том, что величина (десятичная цифра) — это либо 'О', либо '1', ..., либо '9'; таким образом, она должна быть одной из этих десяти цифр. Следующее правило говорит о том, что величина (цепочка цифр) — это либо (десятичная цифра), либо (цепочка цифр) и следующая за ней (десятичная цифра); таким образом, она должна представлять собой последовательность из одной или более цифр. И наконец, (числовая лексема) может принимать одну из трех форм, примерами которых являются, соответственно, '15', '.05' и '3.14159'. Правила синтаксиса объясняют только поверхностную структуру языка, но не затрагивают глубинного смысла вещей. Например, из приведенных выше правил следует, что '15' — (числовая лексема), но вовсе не следует, что '15' имеет какую- либо связь с числом пятнадцать. Поэтому правила синтаксиса обычно дополняются правилами семантики, которые определяют смысл синтаксически правильных цепочек символов. В случае числовых лексем семантику определяют принципы обычных обозначений в десятичной системе счисления, с поправкой на то, что METAFONT оперирует лишь числами, заключенными в ограниченном диапазоне. А именно: абсолютное значение числовой лексемы должно быть меньше 4096, и оно всегда округляется до ближайшего числа, кратного 65\36. Таким образом, например, 'Л' означает не ^, а ^$4- (что несколько превышает ^). Лексемы '.099999' и '0.10001' имеют то же значение, что и '.1', поскольку все они представляются числом -^4_ 65536 " f^ Упражнение 6.1 Эквивалентны ли в программах для METAFONT следующие пары числовых лексем? (а) 0 и 0.00001; (Ь) 0.00001 и 0.00002; (с) 0.00002 и 0.00003; (d) 04095.999999 и 10000. METAFONT преобразует каждую строку текста в последовательность лексем, применяя следующие правила до тех пор, пока в строке не останется символов: 1) Если следующий символ — пробел или точка ('.'), и за ним не следует десятичная цифра или точка, его следует проигнорировать и двигаться дальше. 2) Если следующий символ — знак процента ("/,'), то следует проигнорировать и его, и всю оставшуюся часть текущей строки. (Таким образом, знак процента позволяет писать комментарии, невидимые для METAFONT.) 3) Если следующий символ — десятичная цифра или точка, за которой следует десятичная цифра, то следующая лексема — это числовая лексема, которая состоит из самой длинной последовательности смежных символов с началом в текущей позиции, удовлетворяющей приведенным выше правилам синтаксиса для числовых лексем. 4) Если следующий символ — двойная кавычка (""), то следующая лексема — это лексема-цепочка, содержащая все символы, начиная с текущей позиции и до следующей двойной кавычки включительно. (Остаток строки должен содержать еще хотя бы одни двойные кавычки, иначе METAFONT сообщит,
Глава 6: Как METAFONT считывает входные данные 63 что "цепочка неполна".) Лексема-цепочка представляет последовательность символов, заключенную между двойными кавычками. 5) Если следующий символ — скобка ('(' или ')'), запятая (V) или точка с запятой (V), то следующая лексема — это символьная лексема, которая состоит из одного символа. 6) В любом другом случае следующая лексема — символьная лексема, которая состоит из следующего символа и символов, следующих непосредственно за ним, если они содержатся в одной с ним строке следующей таблицы: ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz < > +- /*\ I? #Ш [ ] {} (см. правила 1, 3, 6) , ; ( ) (см. правило 5; это — символы-одиночки) " (см. правило 4, где подробно описаны лексемы-цепочки) 0123456789 (см. правило 3, где подробно описаны числовые лексемы) У, (см. правило 2, где подробно описаны комментарии) Наилучший способ выучить шесть приведенных правил о лексемах — выполнить упражнение 6.2, после чего вы сможете прочитать любой файл точно так же, как это делает компьютер. ► Упражнение 6.2 Какие лексемы обнаружит METAFONT в следующей (бессмысленной) строке: xx3.1.6..[[a+-bc_d.e] ]"а У." <|>(($1. 5"+-"»» У. weird? ► Упражнение 6.3 Подвергните критике утверждение: "METAFONT игнорирует все пробелы во входных данных". ► Упражнение 6.4 Верно ли, что, если изменить синтаксис для числовых лексем, введя четвертую альтернативу — '(цепочка цифр). \ то это не повлияет на смысл программ для METAFONT? Сколько мы ни искали, но не смогли обнаружить даже крошечных признаков.* ФИЛИМОН ГОЛЛАНД, Camden's Britania (1610) Вмешались неблагоприятные знаки. - УИЛЬЯМ КАУПЕР, Гомерова "Иллиада" (1791) * Слово token (лексема) в английском языке имеет смысл "признак, знак". — Прим. перев.
7 Переменные УУ.Л, ,Л /fl/MU\\\\j__ v\\*/ЧГ^у
Глава 7: Переменные 65 Одно из важнейших понятий в системе METAFONT — это понятие переменной. Переменная (variable) есть величина, которая может принимать множество различных значений. Это понятие является одним из важнейших в математике и играет значительную роль почти во всех языках программирования. Суть идеи состоит в том, что в программах используются данные, а значения этих данных хранятся в небольших ячейках памяти компьютера. Каждая ячейка представляет собой переменную, и, присваивая ячейкам определенные имена, мы получаем доступ к хранящимся в них данным. Например, программа для рисования буквы О из файла io.mf в главе 5 содержит множество переменных. Некоторые из них, такие как 'х1Г и 'yl', представляют координаты. Другие, например 'up', представляют направления. Переменные 'ет#' и 'thin#' обозначают физические единицы длины, не зависящие от характеристик устройства вывода; аналогичные им переменные 'em' и 'thin' обозначают соответствующие единицы длины, выраженные в пикселях, которые зависят от характеристик устройства вывода. Из этих примеров видно, что переменные часто бывают взаимосвязаны. Существует четкая связь между 'ет#' и 'ет', между 'xl' и 'уГ; комманда 'penpos'1 устанавливает связь между 'xll', 'хГ и 'xlr'. Соотношения между переменными могут быть отражены в структуре их имен. Поэтому при осмысленном выборе имен переменных программы становятся намного понятнее. В предыдущей главе мы обсуждали лексемы — элементарные частицы, из которых строятся программы для METAFONT. Мы узнали, что лексемы бывают трех видов: числовые (представляющие числа), цепочки (представляющие текст) и символьные (представляющие все остальное). Сами по себе символьные лексемы не имеют смысла; любая символьная лексема может обозначать все, что пожелает программист. Однако отдельные символьные лексемы все же имеют примитивный смысл, уже определенный к моменту, когда METAFONT начинает выполнять операции. Например, '+' изначально означает "прибавить", а ';' означает "завершить текущую операцию и перейти к следующей части программы". Обычно примитивные значения таких лексем оставляют неизменными, но при желании любой символьной лексеме по ходу программы можно приписать новый смысл. Например, лексема *test_I' в программе io.mf вводится как имя подпрограммы, или макроса. Позже мы увидим, что системе METAFONT можно дать указание 'let plus=+', после чего 'plus' будет действовать точно так же, как '+'. Система METAFONT делит символьные лексемы на две категории, в зависимости от того, какой смысл они имеют в данный момент. Если на текущий момент символьная лексема обозначает одну из примитивных операций системы METAFONT или определена как макрос, то она называется вызовом (spark), в противном случае — ярлыком (tag). Почти все символьные лексемы — ярлыки, так как лишь немногие из них определены как вызовы. Однако в типичной программе для METAFONT присутствует множество вызовов, так как именно они побуждают систему к действиям. Так, среди символьных лексем в первых пяти строках файла io.mf имеются вызовы mode_setup ; := / define_pixels ( , ) и ярлыки em # pt cap thin thick о
66 Глава 7: Переменные (некоторые из них встречаются несколько раз). Ярлыки используются как обозначения переменных; вызовы в имена переменных входить Не могут. Имена переменных могут состоять из нескольких лексем, например, 'ет#\ Так, имя переменной 'xll' состоит из трех лексем, одна из которых — числовая. В системе METAFONT соотношения между переменными естественным образом отображаются в структуре их имен. В условных языках программирования, например в языке Паскаль, 'х1Г записывается как 'х[1] .1'. Обозначение 'х[1] .1' для переменной xll вполне допустимо и в программах для METAFONT, но, поскольку подобные переменные используются очень часто, сокращенная форма 'xll' гораздо удобнее. Формальные правила синтаксиса, в соответствии с которыми METAFONT трактует имена переменных, таковы: (переменная) —У (ярлык) (суффикс) (суффикс) —> (пустой суффикс) | (суффикс) (нижний индекс) | (суффикс) (ярлык) (нижний индекс) —> (числовая лексема) | [(числовое выражение) ] Имя переменной начинается с ярлыка, например, 'х', за которым следует суффикс (suffix), например, '11'. Суффикс может быть пустым, или содержать один или более нижних индексов, т.е. ярлыков, которые дописываются к основному ярлыку. Нижний индекс (subscript) — это номер, который позволяет формировать массивы однотипных переменных. Нижним индексом может быть либо одиночная числовая лексема, либо формула, заключенная в квадратные скобки. В последнем случае, формула должна давать некоторое числовое значение. К примеру, если к — переменная со значением 1, то 'х[1]', 'х[к]' и 'х[3-2к]' означают то же самое, что и 'хГ. Но 'х.к' — это не то же самое; это не ярлык 'х' с нижним индексом, равным значению переменной к, а ярлык 'х', к которому дописан ярлык 'к\ f Переменные 'xl\ 'х01' и 'xl .00' неразличимы. Поскольку нижним индексом может быть произвольная числовая лексема, индексы могут быть и дробными числами. Например, 'х1.5' — это то же самое, что и 'х[3/2]\ Заметим, однако, что переменные 'В007' и 'В.007' различны, так как последняя имеет дробный нижний индекс. f Считывая имя переменной, METAFONT выбирает максимальный (суффикс) из возможных. Иными словами, она всегда расширяет (суффикс), если после него стоит (нижний индекс) или (ярлык). f ►Упражнение 7.1 Как сослаться на переменную с двойным индексом 'а[1] [5]', не используя квадратные скобки? f ► Упражнение 7.2 На любую ли переменную можно сослаться, не используя квадратные скобки? /$\/>\^ Упражнение 7.3 JL JL Студент Джонатан Квик использовал 'а. plus Г как имя переменной в начале своей программы; позже он указал 'let plus=+'. Как он может после этого сослаться на переменную 'a. plus 1'? fB системе METAFONT имеется несколько специальных переменных, называемых внутренними величинами (internal quantities), которые сильно влияют на действия, выполняемые компьютером. Так, существует внутренняя величина 'fontmaking', которая отвечает за генерирование tf m файла; внутренняя величина 'tracingtitle' управляет выводом на экран заголовков, таких, как "The letter 0". Еще одна — 'smoothing' —
Глава 7: Переменные 67 влияет на процесс оцифровки кривых. (Полный список внутренних величин системы METAFONT приведен в главе 25.) Имена внутретренних величин действуют как ярлыки, но к ним нельзя дописывать суффикс. Таким образом, правило синтаксиса для величины (переменная) нужно заменить двумя чуть более сложными правилами: (переменная) —У (внешний ярлык) (суффикс) | (внутренняя величина) (ярлык) —> (внешний ярлык) | (внутренняя величина) f> Упражнение 7.4 Верно ли, что всякая (переменная) может использоваться в качестве суффикса? /^\/$\ Символы '['иТв правилах синтаксиса для нижнего индекса обозначают произ- JL JL вольные символьные лексемы, которые на текущий момент имеют тот же смысл, что и примитивные символьные лексемы системы METAFONT, соответствующие левой и правой квадратным скобкам. Эти символы вовсе не обязаны быть квадратными скобками. Напротив, если смысл лексем '[' и ']' был изменен, то квадратные скобки уже нельзя использовать для выделения нижнего индекса. Это касается всех символьных лексем во всех правилах синтаксиса, которые встретятся нам в дальнейшем. Для системы METAFONT важно не то, как выглядит та или иная лексема, а то, какой смысл она имеет в данный момент. Программы для METAFONT в этой книге записываются в двух разных стилях. Иногда мы записываем переменные, используя курсив и настоящие нижние индексы, например, 'em' и 1Х2Г'> В других случаях мы записываем их, имитируя стиль пишущей машинки, например, 'em' и 'х2г\ Стиль пишущей машинки используется, если мы заботимся главным образом о том, какими средствами располагает программист при наборе текста программы. Если же нас в первую очередь интересует смысл программы, а не ее представление в символах ASCII, то мы используем более изящный стиль. Нетрудно додуматься, как преобразовать эти изящные символы в лексемы, понятные системе METAFONT. f Вообще говоря, курсивом мы будем выделять только ярлыки (например, em, я, г), тогда как полужирным и обычным шрифтами будут набираться имена вызовов (например, draw, fill, cycle, rotated, sqrt). Ярлыки, которые состоят не из букв, а из специальных символов, будут иногда записываться особым образом; например, ет# и z2* могут принимать вид ет# и z'2} соответственно. Почти все переменные, которые фигурировали в наших предыдущих рассуждениях, имели числовые значения. На самом деле в системе METAFONT переменные могут принимать значения восьми различных типов. Это может быть: ■ булева переменная (тип boolean), принимающая значения true и false; ■ переменная-цепочка (тип string), значением которой является цепочка символов ASCII; ■ переменная-путь (тип path), значением которой является (возможно, кривая) линия; ■ переменная-перо (тип реп), значением которой является форма кончика пера; ■ переменная-рисунок (тип picture), значением которой является целый массив пикселей; ■ переменная-преобразование (тип transform), которая определяет операцию масштабирования, вращения, сдвига, отражения и/или наклона; ■ переменная-пара (тип pair), значением которой является пара чисел; ■ числовая переменная (тип numeric), значением которой является число.
68 Глава 7: Переменные Если вы хотите, чтобы значение переменной было не числом, а чем-то другим, то должны вначале объявить тип этой переменной. Однако если вы сошлетесь на переменную, тип которой не был заранее объявлен, то METAFONT не станет возражать, если только вы не попытаетесь использовать ее там, где она непременно должна иметь нечисловое значение. Объявить тип проще простого. Нужно просто назвать один из восьми типов, а затем перечислить те переменные, которым вы хотите присвоить этот тип. Например, объявление pair right, left, а.р означает, что переменные right, left и а.р будут иметь тип pair, поэтому далее можно писать такие уравнения, как right = -left = 2а.р = (1,0). Эти уравнения, между прочим, определяют значения переменных right = (1,0), left = (—1,0) и а.р = (.5,0). (В базовый формат METAFONT эти значения right и left встроены изначально.) Правило объявления переменных с нижним индексом несколько сложнее, так как METAFONT настоятельно требует, чтобы все переменные, имена которых отличаются только нижним индексом, имели одинаковый тип. Например, вы можете определить типы так, чтобы переменная а была числовой, переменная а.р — парой, переменная a.q — пером, а.г — путем, а переменная а\ — цепочкой. Но если а\ — цепочка, то и другие переменные — аг, оз и т.д. — должны быть цепочками. Дело в том, что система METAFONT позволяет вводить только "общие" нижние индексы, которые в объявлениях представляются пустыми квадратными скобками — '[]'• Например, объявление path г, r[], x[]arc, f [] [] присваивает г и всем переменным вида г [г], х[г]агс и f[i][j] тип path. Это объявление не влияет на тип или значения других переменных, например, гЦагс; оно относится только к тем переменным, которые в нем упоминаются. Объявление типа уничтожает все предыдущие значения объявляемых переменных. Например, в результате приведенного выше объявления переменные вида г, г [г], я [г] arc и f[i][j] становятся неопределенными, даже если до этого их значениями были какие-то пути. Все заново объявленные переменные будут заново определяться "с чистого листа" и получат новые подходящие значения исходя из последующих уравнений. ► Упражнение 7.5 Переменные числового типа можно и не объявлять. Может ли по какой-либо причине понадобиться написать 'numeric х'? f Формальные правила синтаксиса для объявлений типа четко разъясняют соглашения по поводу грамматики. Если объявляемая переменная начинается с символьной лексемы, которая до этого была "вызовом", то эта лексема сразу же утрачивает прежний смысл и становится "ярлыком". (объявление) —> (тип) (список объявляемых) (тип) —> boolean | string | path | реп | picture | transform | pair | numeric
Глава 7: Переменные 69 (список объявляемых) —> (объявляемая переменная) | (список объявляемых), (объявляемая переменная) (объявляемая переменная) —> (символьная лексема) (объявляемый суффикс) (объявляемый суффикс) —> (пусто) | (объявляемый суффикс) (ярлык) | (объявляемый суффикс) [ ] ► Упражнение 7.6 Найдите три ошибки в следующем "объявлении": 'transform t42,24t, ,t,path\ Существа, стоящие на низших ступенях развития, отличаются большей изменчивостью, чем те, которые стоят на высших ступенях. — ЧАРЛЬЗ ДАРВИН, Происхождение видов (1859) Среди переменных наибольший интерес, возможно, представляет Бета (р) Персей, или Алгол, так как его период достаточно мал. — Дж. НОРМАН Л ОКИ ЕР, Элементы астрономии (1870)
Алгебраические выражения
Глава 8: Алгебраические выражения 71 Программы для системы METAFONT состоят из алгебраических формул, которые называются выражениями {expressions). Эти формулы — алгебраические в том смысле, что в них присутствуют как константы, так и переменные. Комбинируя константы и переменные посредством математических операций, программист может с относительной легкостью определить великое множество объектов. Мы уже неоднократно встречались с примерами выражений. Теперь наша цель — более системно изучить, что же мы можем. Основная идея состоит в том, что выражение — это либо переменная (например, 1х\ ), либо константа (например, '20'), либо оно состоит из оператора (например, '+') и его операндов (например, 'xi 4- 20'). Эти операнды, в свою очередь, представляют собой выражения, которые строятся таким же образом и, возможно, заключены в скобки. Например, L{x\ -f 20)/(а?2 — 20)' — выражение, обозначающее отношение двух подвыражений. Можно выдумать очень сложные алгебраические выражения, но даже самые замысловатые из этих конструкций будут строиться из простых частей по простым правилам. Математики в течение тысяч лет разрабатывали удобные способы записи формул. Затем появились ученые-информатики и нарушили веками устоявшиеся традиции. Основная причина, по которой понадобилось внести изменения, состояла в том, что компьютеру трудно обрабатывать такие двумерные конструкции, как 12-20 V 3 Гораздо легче вводить и расшифровывать одномерные последовательности лексем. Поэтому в языках программирования такие формулы, как правило, записываются в одну строку при помощи круглых и квадратных скобок и звездочек: (х[1]+20)/(х[2]-20)+sqrt(а**2-(2/3)*sqrt(Ъ)). METAFONT поймет не только такую формулу, но и ту, в которой используются более краткие обозначения, напоминающие стандартные математические соглашения: (xl+20)/(x2-20)+sqrt(a**2-2/3sqrt Ъ). В предыдущих главах мы убедились, что METAFONT допускает запись 'х2' вместо {х[2]'. Точно так же, вместо '2*х' можно писать *2х', и '2/Зх' — вместо '(2/3)*х\ В программах для METAFONT такие операции встречаются на каждом шагу, поэтому язык системы изначально был устроен так, чтобы их поддерживать. С другой стороны, язык METAFONT все же не совсем избавлен от неудобств, присущих компьютерным языкам. Произведение х на к вы все-таки должны записывать, как 'х*к', а переменную х с нижним индексом к — как 'х [к]', чтобы не путать ее с переменной с суффиксом 'х.к'. В предыдущей главе мы узнали, что существует восемь типов переменных: числовые переменные, булевы, переменные-цепочки и так далее. Те же самые типы могут иметь и выражения. METAFONT оперирует не только числовыми, но и булевыми выражениями, выражениями-цепочками и т.д. Например, '(0,0) .. (жы/О' — это выражение, значением которого является путь, полученное применением оператора '..' к подвыражениям '(0,0)' и l(xi,yi)\ Эти подвыражения, в свою очередь, имеют значения типа pair, и построены из числовых величин. Каждая операция дает результат, тип которого может быть определен по типам операндов; более того, простейшие выражения (переменные или константы) всегда имеют определенный тип. Таким образом, вычислив значение выражения, машина всегда знает, с величиной какого типа она имеет дело.
72 Глава 8: Алгебраические выражения Если выражение содержит несколько операторов, то система METAFONT должна решить, какую операцию выполнить первой. К примеру, в выражении 'о — 6 + с' нужно сначала вычислить 'а — b\ а затем прибавить с. Если сначала вычислить СЬ + с\ то результат будет не таким, каким он должен быть согласно обычным математическим правилам. С другой стороны, в таком выражении, как 'а — Ь/с\ обычно подразумевается, что в первую очередь будет выполнена операция 'Ь/с'; умножение и деление обычно выполняются прежде, чем сложение и вычитание, если только обратный порядок не указан при помощи скобок, как, например, в выражении '(а — Ь)/с'. Общее правило гласит, что вначале вычисляется значение выражения в скобках, а затем операции выполняются в порядке их приоритета предшествования; если две операции имеют одинаковый уровень приоритета, то первой выполняется та, которая стоит левее. Например, 'а — 6/с' эквивалентно (а — (6/с)', так как деление имеет приоритет перед вычитанием. А 'а —6+с' эквивалентно '(а — 6) + с', так как при одинаковом порядке приоритета предшествования операции выполняются слева направо. Операторы удобно представлять себе как маленькие магниты, которые притягивают к себе операнды; магниты V и '/' сильнее, чем магниты '+' и '—', поэтому они сильнее притягивают свои операнды, и их мы выполняем в первую очередь. Система METAFONT выделяет четыре (и только четыре) уровня приоритета предшествования. Самые сильные магниты — те, которые прикрепляют '2' к 'х' и 'sqrt' к '6' в таких выражениях, как '2я' и 'sqrt 6'. Следующие по силе — мультипликативные операторы, такие как V и '/'. Затем идут аддитивные операторы, такие как '+' и ' —'. Самые слабые магниты — это такие операторы, как '..' или '<'. Например, выражение: a + sqrt Ъ/2х < с эквивалентно формуле (a+((sqrtb)/(2x))) < с, в которой расставлены все скобки. ► Упражнение 8.1 Расставив в формуле 'zl+z2. .z3/4*5. .z6-7*8z9' скобки, укажите точный порядок, в котором METAFONT будет выполнять соответствующие операции. fB математических текстах, как правило, стараются избегать скобок внутри таких же скобок. Поэтому многие привыкли писать {a + [(Sqrt6)/(2x)]}<c вместо приведенной выше формулы с круглыми скобками. Однако профессиональные математики обычно стараются использовать только круглые скобки, потому что квадратные и фигурные скобки имеют другое, более важное значение. В этом отношении система METAFONT ведет себя как профессионал. Фигурные и квадратные скобки ('{}' и ' []') она резервирует для специальных целей, поэтому их не следует использовать вместо круглых. /$\/^\ Если вы хотите иметь альтернативу круглым скобкам, то можете указать delimiters [[ ]]; delimiters {{ }} После этого двойные квадратные и фигурные скобки можно будет использовать в таких формулах, как {{a+[[(sqrt b)/(2x)]]»<c.
Глава 8: Алгебраические выражения 73 Символьная лексема Ч{' не имеет никакого отношения к Ч' и не имеет никакого примитивного значения. Поэтому вы можете приписать ей какой угодно смысл. Команда delimiters определяет новую пару разделителей. В таких формулах, где используются разделители нескольких типов, система METAFONT проверяет соответствует ли каждой открывающей скобке закрывающая скобка такого же типа: '[[' — ']]', '{{' — '}}', 'С — ')'. Это значительно облегчает поиск ошибок в громоздких выражениях. Однако, как правило, во введении дополнительных разделителей нет необходимости, так как громоздкие выражения встречаются нечасто, а четкие правила предшествования операторов делают большинство скобок попросту излишними. У внимательного читателя может возникнуть вопрос: "Погодите-ка! А нет ли здесь противоречия? Минуту назад мне говорили, что '2/Зх' означает '(2/3)*х', а теперь появились правила предшествования, согласно которым '2/Зх' означает '2/(Зх)\ Как же это понимать?" Это действительно очень важный момент, но противоречия тут нет. Просто существует еще одно правило, о котором мы до сих пор не упоминали. Если происходит деление двух числовых лексем, то сила притяжения V сильнее, чем обычно; в данном случае оператор V имеет такой же порядок приоритета предшествования, как и оператор умножения, который подразумевается в записи 'Зх'. Поэтому, как и утверждалось ранее, все операции в '2/Зх' выполняются слева направо. (Это очень хорошее правило, так как оно почти всегда совпадает с замыслом человека, пишущего программу. Однако нужно помнить, что в случае, когда а не является числовой лексемой, 'а/Зх' означает 'а/(3х)'.) В силу этого правила программы для системы METAFONT в этой книге часто содержат дроби вида '§#', которым в файле должно соответствовать '2/Зх'. Такие "двухэтажные" дроби используются только в том случае, когда и числитель, и знаменатель представляют собой числа; конструкции типа 'а/Зх' всегда будут представляться в виде 'а/Зх', а не ' ^ ' Система METAFONT умеет выполнять несколько десятков операций, которые до сих пор не упоминались. Давайте рассмотрим некоторые из них, чтобы иметь возможность воспользоваться ими в случае необходимости. Учиться строить выражения лучше и интереснее всего, работая на компьютере, поэтому вам нужно написать следующий коротенький файл с именем expr.mf: string s[]; sl="abra"; pathp[]; pl=(0,0)..(3,3); p2=(0,0)..(3,3)..cycle; tracingonline:=l; scrollmode; forever: message "gimme an expr: "; sO:=readstring; show scantokens sO; endfor fllpH первом чтении этой главы вам не обязательно вникать в содержимое файла expr.mf, поскольку в нем используются такие возможности системы METAFONT, которые будут подробно описаны позже. Но если вам не терпится, то вот расшифровка. В строке 1 объявляется, что все переменные вида Sk являются цепочками, и значение si устанавливается равным "abra". В строке 2 объявляется, что все переменные вида Рк являются путями, a pi и рг определяются как простые пробные пути. В строке 3 системе METAFONT дается команда показывать дополнительную информацию, то есть не только записывать ее в log-файл, но и выводить на экран. Здесь же устанавливается режим 'scrollmode', в котором компьютер не будет останавливаться, выдавая сообщения об ошибках. В строках 4 и 5 задается бесконечный цикл, выполняя который METAFONT считывает с клавиатуры некоторое выражение и показывает соответствующее значение. Если вы запустите METAFONT и в ответ на запрос имени входного файла наберете 'ехрг', то система METAFONT вначале считает файл expr.mf, а затем
Глава 8: Алгебраические выражения скажет вам 'gimme an ехрг\* Вот тут-то самое интересное и начинается: вы можете ввести любое выражение, и METAFONT вычислит и выведет на экран его значение. Попробуйте сами: напечатайте '2+2' и нажмите (return); в ответ получите '» 4\ Вас это не удивляет? Тогда попробуйте еще вот это: Вы печатаете И в результате получаете 1.2-2.3 -1.1 1.3-2.4 -1.09999 1.3*1000 1300.00305 2.4*1000 2399.9939 3/8 0.375 .375*1000 375 1/3 0.33333 1/3*3 0.99998 0.99999 0.99998 1-epsilon 0.99998 1/(1/3) 3.00005 1/3.00005 0.33333 .1*10 1.00006 l+4epsilon 1.00006 Из этих примеров мы видим, что METAFONT допускает небольшие погрешности в арифметических расчетах, так как оперирует лишь числами, кратными 65*36. Например, разность 1.3 — 2.4 не равна в точности —1.1, поскольку 1.3 чуть больше, чем Щ, а 2.4 чуть меньше, чем Щ. Небольшие погрешности увеличиваются при умножении на 1000, но даже после этого возникающими неточностями можно пренебречь, так как они составляют всего лишь крошечные доли пикселя. Вам может показаться странным, что значение произведения 1/3 на 3 оказывается равным не .99999, а .99998. Дело в том, что обе десятичные дроби представляют число §§§§§, но METAFONT записывает его как .99998 потому, что оно ближе к .99998, чем к .99999. В базовом формате METAFONT определена величина epsilon, равная £5536' т0 есть наименьшее значащее число, большее нуля. Таким образом, 1-epsilon равно §§§§§, a l+4epsilon равно §§§§§. Вы печатаете И в результате получаете 4096 4095.99998 (и сообщение об ошибке) inf inity 4095.99998 1000*1000 32767.99998 (и сообщение об ошибке) infinity+epsilon 4096 100*100 10000 .1(100*100) 1000.06104 (100*100)/3 3333.33333 Если вы попытаетесь ввести число, большее или равное 4096, то система METAFONT сообщит вам: 'Enormous number has been reduced' ("Пришлось сократить слишком * От англ. "Give me an expression" — "Дай мне какое-нибудь выражение". — Прим. перев.
Глава 8: Алгебраические выражения 75 большое число"). В базовом формате METAFONT величина infinity определена как 4096 - epsilon и является наибольшей допустимой числовой лексемой. С другой стороны, оказывается, что в промежуточных вычислениях могут фигурировать и большие числа; METAFONT не обращает на это внимания, если только конечный результат не превышает 32768. f ►Упражнение 8.2 Набрав '100*100/3' вместо 4100*100)/3', вы получите '3333.33282'. Почему? /$N/$N В некоторых случаях система METfiFONT производит вычисления гораздо более JL JL точно, чем можно было бы ожидать, имея в виду приведенные выше примеры. Дело в том, что многие внутренние вычисления производятся с числами кратными не 2~16, а 2~28. Например, если t = 3, то деление 'l/3t' даст в результате ровно 1 (а не .99998); то же самое произойдет, если написать '1/3(3)'. Теперь попробуем ввести более сложные выражения, в которых используются как константы, так и неопределенные переменные. (Вы выполняете эти примеры или просто читаете книгу? Будет гораздо лучше, если вы сами введете эти выражения и посмотрите, что при этом произойдет. Более того, вам никто не запрещает вводить другие выражения!) Вы печатаете И в результате получаете b+а а+Ь а+Ь а+Ь b+a-2b а-Ъ 2(а-Ъ+.5) 2а-2Ъ+1 .5(Ъ-а) -0.5а+0.5Ъ .5[а,Ъ] 0.5а+0.5Ь 1/3[а,Ъ] 0.66667а+0.ЗЗЗЗЗЬ 0[а,Ъ] а а[2,3] а+2 t[a,a+l] t+a a*b b (и сообщение об ошибке) 1/Ъ b (и сообщение об ошибке) При сложении переменных система METAFONT предпочитает располагать их в определенном порядке; поэтому 'а + 6' и '6 + а' дают одинаковый результат. Обратите внимание на то, что конструкция с помещением в промежуточное положение с.5[а, Ь]' определяет среднее чисел а и 6, как говорилось в главе 2. METAFONT не позволяет ни перемножать две неизвестные числовые величины, ни делить на неизвестное число. В системе METAFONT все выражения с неизвестными представлять собой "линейные формы", то есть суммы переменных с постоянными коэффициентами, плюс некоторая необязательная константа. (Теперь вы можете попробовать напечатать 't [а, Ь]', чтобы посмотреть, какое сообщение об ошибке будет выдано.) Вы печатаете И в результате получаете sqrt 2 1.41422 sqrt 100 10
76 Глава 8: Алгебраические выражения sqrt 100*100 sqrt(100*100) sqrt 100(100) sqrt sqrt 100(100) sqrt .01 0.09998**2 2**1/2 sqrt 2**2 sqrt -1 sqrt a 1000 100 100 10 0.09998 0.01 1.41422 2 0 (и сообщение об ошибке) а (и сообщение об ошибке) Поскольку оператор sqrt обладает более сильным "магнетизмом", чем *, то формула sqrt 100*100 вычисляется как (sqrt 100)* 100. Но в формуле 'sqrt 100(100)' вначале вычисляется 100(100), по той причине, что '(sqrt 100) (100)' не является правильным выражением и операции в 'sqrt 100(100)' должны выполняться справа налево. Если вы не уверены, в каком порядке будут выполняться операции, то всегда можете вставить скобки. Однако, набравшись опыта, вы увидите, что правила предшествования системы METAFONT вполне естественны. ► Упражнение 8.3 Вычисляется ли значение 'sqrt 2**2' как '(sqrt 2)**2' или как 'sqrt(2**2)'? Некоторые выражения имеют значения true ("истина") или false ("ложь"). Как мы увидим позже, при помощи таких выражений программы для METAFONT можно приспосабливать к различным специальным условиям. Вы печатаете 0<1 0=1 а+1>а а>=Ъ "abc"<=MbM •■В">,,а!" "Ь">,,а?н (1,2)0(0,4) (1,2)<(0,4) (1,а)>(0,Ъ) numeric а known а not pen а known "a" and numeric 1 (0>1) or (а<а) 0>1 or а<а И в результате получаете true false true false (и сообщение об ошибке) true false true true false true true false true true false а (и сообщения об ошибках) Лексемы '>=', '<=' и 'о' обозначают, соответственно, отношения "больше или равно", "меньше или равно" и "не равно". При сравнении цепочек, состоящих более, чем из одного символа, система METAFONT пользуется правилами лексикографического
Глава 8: Алгебраические выражения 77 упорядочения. В случае одиночных символов она сравнивает их ASCII-коды. Поэтому все заглавные буквы рассматриваются как меньшие по сравнению со строчными. (См. приложение С.) При сравнении пар система METAFONT сравнивает сначала координаты х, и только если они не равны, сравнивает координаты у. Тип выражения можно проверить посредством выражения 'pair а', значение которого будет истинным тогда и только тогда, когда а является парой. Выражение 'known а7 будет истинным тогда и только тогда, когда значение а известно. f* Упражнение 8.4 Каковы причины сообщений об ошибках в выражении '0>1 or а<а' ? fBcn оставшаяся часть этой главы отмечена знаком опасного поворота, поэтому при первом чтении вы можете смело ее пропустить (если, конечно, не увлечены настолько, что не можете остановиться). /$К В системе METAFONT выражения могут содержать множество менее привычных, JL но весьма полезных операций. Например, операторы 'max' и 'min' позволяют отыскать максимум и минимум среди чисел, цепочек или пар: Вы печатаете И в результате получаете тах(1,-2,4) 4 minU,-2,4) -2 maxO'aV'b'V'ab") "b" min(Ma,,,"b",,,abM) "ам max((l,5),(0,6),(l,4)) (1,5) min((l,5),(0,6),(l,4)) (0,6) max(.5a+l,.5a-l) 0.5a+l Числа можно перевести в целые несколькими способами: Вы печатаете И в результате получаете floor 3.14159 3 floor -3.14159 -4 floor -epsilon -1 floor infinity 4095 ceiling 3.14159 4 ceiling -3.14159 -3 round 3.14159 3 round -3.14159 -3 round(l.l,2.8) (1,3) round(3.5,-3.5) (4,-3) round a a+0.5 (и сообщение об ошибке) 8 mod 3 2 -8 mod 3 1 .8 mod .3 0.2 Оператор 'floor' вычисляет наибольшее целое число, которое меньше или равно его операнду; в математике эту величину часто обозначают как [х\. В базовом формате METAFONT имеется также аналогичная операция \х] — 'ceiling', которая находит наименьшее целое число, большее или равное х. Кроме того, 'round х' означает ближайшее к х целое
78 Глава 8: Алгебраические выражения число; METAFONT вычисляет эту величину при помощи формулы [х 4- .5J; если округляется пара, то формула применяется к обоим компонентам. Остаток от деления х на у записывается как 'х mod г/' и вычисляется по формуле 'х — у[х/у\\ Вы печатаете И в результате получаете abs -7 7 abs(3,4) 5 length(3,4) 5 3++4 5 300++400 500 sqrt (300**2 + 400**2) 181.01933 (и сообщения об ошибках) 1++1 1.4142 0 ++ -7 7 Б+-+4 3 Операция ++' называется пифагоровым сложением] а-Ь+6 — это то же самое, что у/о^+Ь2. Вероятно, если бы операция ++ была доступна более широкому кругу компьютерных приложений, то в программах можно было бы избежать большинства операций по извлечению квадратного корня, поскольку квадратные корни используются в основном для вычисления длин. Заметим, что a-h+fc-f-fc = y/a2 + b2 -he2. Кроме того, справедливы тождества: a -f + 6 = 6 ++ а и (a ++ 6) -f-f с = a ++ (6 ++ с). Для вычисления \/а2 + б2 лучше использовать именно пифагорово сложение, так как при подсчете а2 и б2 могут возникать слишком большие числа, даже если значение о +4- 6 невелико. Имеется также обратная операция — пифагорово вычитание, которое обозначается как '+-+'; величина а -\ 1-6 равна \Ja2 — б2. f* Упражнение 8.5 Подбирая эти примеры автор был немало удивлен, когда в ответ на '0++-7' машина выдала ответ '0'. Почему это не должно удивлять? &)&)* Упражнение 8.6 JL JL (Для математиков.) Несмотря на то что операция пифагорова сложения является ассоциативной и коммутативной, METAFONT считает, что 5++4++2++2 = 7 = 2 ++ 2 ++ 4 ++ 5, а 2 ++ 4 ++ 5 ++ 2 = 6.99998. Почему? ^Для обозначения тригонометрических функций синуса и косинуса в системе М ETR- FONT используются имена 'sincT и 'cosd'. Дело в том, что в METAFONT операции производятся с углами, выраженными в градусах.* Но, оказывается, что программистам не так уж часто приходится обращаться непосредственно к операторам 'sind' и 'cosd', поскольку функции 'dir' и 'angle' обеспечивают почти все потребности, возникающие при разработке шрифтов. Вы печатаете И в результате получаете sind 30 0.5 cosd 30 0.86603 sind -30 -0.5 cosd 360 1 sind 10 ++ cosd 10 1 dir 30 (0.86603,0.5) dir -90 (0,-1) * "Градус" по английски — degree, отсюда и окончание 'd'. — Прим. перев.
Глава 8: Алгебраические выражения 79 angle(1,1) 45 angle(1,2) 63.43495 angle(1,-2) -63.43495 sind 63.43495 / cosd 63.43495 1.99997 angle up 90 angle left 180 angle(-1000,-epsilon) -180 angle dir 60 60.00008 angle(0,0) 0 (и сообщение об ошибке) В базовом формате METAFONT функция 'dirx' определена как пара (cosd х, sind я); это вектор, который указывает направление, образующее угол х градусов с правой полуосью горизонтальной оси. Обратный оператор 'angle' определяет угол, соответствующий данному вектору. /$\<£s Логарифмы и экспоненты вычисляются несколько необычным способом. Это дела- JL JL ется для того, чтобы повысить точность вычислений с участием чисел из конечного диапазона, используемых в METAFONT. Значения mlog а: = 256 Inх и техрх = ех'256 дают вполне удовлетворительные результаты при подсчете х ** у по формуле mexp(y * mlogx). Вы печатаете И в результате получаете mlog 2 177.44568 техр mlog 2 2 техр 8 mlog 2 256 техр 256 2.71828 mlog 2.71828 255.99954 mlog 2.71829 256.00098 15 mlog 2 2661.68518 техр 2661.68518 32767.99998 техр 2661.68519 32767.99998 (и сообщение об ошибке) техр-2661.68519 0.00003 f Кроме того, METAFONT генерирует два вида случайных чисел. Маловероятно, что, выполняя эксперимент самостоятельно, вы получите именно те значения, которые приведены в следующих примерах, так как, каждый раз запрашивая у компьютера новое случайное число, вы получаете другой результат (если только вы не задали "затравочное значение", как объясняется в главе 21). Вы печатаете И в результате можете получить uniformdeviate 100 47.4241 uniformdeviate 100 97.28148 uniformdeviate -100 -36.16279 (normaldeviate,normaldeviate) (0.46236,-1.87648) Значение выражения 'uniformdeviate 100' есть случайное число в промежутке между 0 и 100; значение выражения 'normaldeviate' есть нормально распределенная случайная величина со средним ноль и единичным стандартным отклонением. В главе 21 объясняется, что это значит, а также приводится несколько примеров применения этой величины. * Кроме всех этих операторов, применяемых к числам, в системе METAFONT существует большой набор операций, применяемых к парам. Некоторые из них
80 Глава 8: Алгебраические выражения показаны в следующих примерах: Вы печатаете right (1,2)+(3,4) 1/3(3,10) z2-zl .2[zl,z2] 3z z scaled 3 z xscaled 2 yscaled 1/2 z shifted (2,3) z shifted 3right z slanted 1/6 z rotated 90 z rotated 30 xpart(z rotated 30) ypart(z rotated 30) (1,2)*(3,4) (l,2)zscaled(3,4) (a,b)zscaled(3,4) (a,b)zscaled dir 30 (l,2)dotprod(3,4) (a,b)dotprod(3,4) dir 21 dotprod dir 51 (3,4)dotprod((30,40)rotated 90) И в результате получаете (1,0) (4,6) (1,3.33333) (-xl+x2,-yl+y2) (0.8x1+0.2x2,0.8yl+0.2y2) (Зх,3у) (Зх,3у) (2х,0.5у) (х+2,у+3) (х+3,у) (х+0.16667у,у) (-У,х) (-0.5у+0.86603х,0.86603у+0.5х) -0.5у+0.86603х 0.86603у+0.5х (3,4) (и сообщение об ошибке) (-5,10) (За-4Ъ,4а+ЗЪ) (0.86603a-0.5b,0.5a+0.86603b) 11 3a+4b 0.86603 0 (Напомним, что система METAFONT преобразует 'z$' в '(х$,у$)', где $ — произвольный (суффикс).) Названия почти всех представленных здесь операций говорят сами за себя. Когда точка или вектор вращается, то она/он поворачивается вокруг точки (0,0) против часовой стрелки на заданное число градусов. Вычисляя координаты результата вращения, METAFONT сам использует синусы и косинусы нужным образом, так что вам не нужно запоминать соответствующие формулы. Хотя посредством '*' нельзя умножать пару на пару, вы можете использовать операцию 'zscaled', аналогичную умножению комплексных чисел. Например, поскольку умножение (1 + 2г) на (3 + 4г) дает -5 + Юг, то (1,2) zscaled (3,4) = (—5,10). Существует и другой тип умножения, переводящий пары в числа: (а, 6) dotprod (с, d) = ас -f bd. Это, так называемое, "точечное произведение" (dot product), которое в математических текстах обычно записывается как '(а,Ь) • (с, d)\ Оно равно произведению а++Ьис++с(, умноженному на косинус угла между векторами (а,6) и (с,d). Поскольку cosd90° = 0, два вектора перпендикулярны друг другу тогда и только тогда, когда их точечное произведение равно нулю.* $ Существуют и такие операции, которые производятся над цепочками, путями и переменными других типов; их мы рассмотрим в последующих главах. Пока что мы ограничимся лишь несколькими примерами. При этом следует помнить, что в файле expr.mf переменная s с произвольным индексом определяется как цепочка, аре * Если не принимать во внимание эффекты, связанные с округлением чисел в системе METAFONT, то понятие точечного произведения нар в точности соответствует понятию скалярного произведения, используемому в векторной алгебре. — Прим. перев.
Глава 8: Алгебраические выражения 81 произвольным индексом — как путь. Кроме того, переменной si было приписано значение "abra", переменной pi — значение '(0,0) .. (3,3)', а переменной рг — значение '(0,0) .. (3,3) ..cycle'. Вы печатаете s2 sl&"cadMftsl length si length pi length p2 cycle pi cycle p2 substring (0,2) of si substring (2,infinity) of si point 0 of pi point 1 of pi point .5 of pi point infinity of point .5 of p2 point 1.5 of p2 point 2 of p2 point 2+epsilon of point -epsilon of point -1 of pi direction 0 of pi direction 0 of p2 direction 1 of p2 Pi p2 P2 И в результате пол unknown string s2 "abracadabra" 4 1 2 false true "ab" "ra" (0,0) (3,3) (1.5,1.5) (3.3) (3,0) (0,3) (0,0) (0.00009,-0.00009) (-0.00009,0.00009) (0,0) (1,1) (4,-4) (-4,4) Длина пути есть число шагов '..', которое в нем содержится; для того чтобы определить, является ли путь циклическим, можно использовать конструкцию 'cycle (путь)'. Если вы наберете просто 'рГ, то увидите определение пути pi с контрольными точками: (0,0)..controls (1,1) and (2,2) ..(3,3) Точно так же, 'р2' — это путь (0,0)..controls (2,-2) and (5,1) ..(3,3)..controls (1,5) and (-2,2) ..cycle a 'subpath (0,1) of p2' — подпуть пути 'p2' вида (0,0)..controls (2,-2) and (5,1) ..(3,3) Выражение 'point t of рг определяет положение точки, которая движется вдоль пути рг, стартуя из исходной точки (0,0) в момент t = 0, затем достигает точки (3,3) в момент, когда t = 1, и т.д.; значение этого выражения в момент t = 1/2 — это промежуточная точка третьего порядка, полученная посредством конструкции, которую мы рассматривали в главе 2, с использованием промежуточных контрольных точек (2, —2) и (5,1). Поскольку рг — циклический путь длины 2, то point (t + 2) of рг = point t of рг для любого t. Путь pi не является циклическим, поэтому для него точки, соответствующие t < 0, совпадают
82 Глава 8: Алгебраические выражения с точкой 0, а при t > 1 они совпадают с точкой 1. Выражение 'direction t of (путь)' похоже на 'point t of (путь)'; оно задает вектор направления движения в момент времени t. f Обход путей не обязательно совершается с постоянной скоростью. Например, на диаграмме справа показаны положения point t of рг, соответствующие двадцати значениям £, выбранных с одинаковым интервалом. В данном случае в момент времени 1.0 движение происходит быстрее, чем в момент 1.2; но точки распределены достаточно хорошо, и понятие дробного времени может быть полезным. Кроме прочего, на диаграмме видно, что путь рг — не совсем точное приближение окружности: тут нет симметрии между левой и правой частями, несмотря на то, что участок кривой между точками 1 и 2 является зеркальным отражением участка кривой между точками 0 и 1. Это несоответствие вполне естественно, поскольку путь рг был определен всего по двум заданным точкам, (0,0) и (3, 3). Для того же, чтобы форма пути достаточно точно повторяла форму окружности, нужно задать не менее четырех точек. &)&) Для сращивания путей можно использовать операцию сцепления ('ft'). Она во jl JL многом напоминает операцию сцепления цепочек. К примеру, если вы напечатаете 'р2 ft pi', то получите путь длины 3, который образован разрывом циклического соединения в пути рг и присоединением к нему пути pi: (0,0)..controls (2,-2) and (5,1) ..(3,3)..controls (1,5) and (-2,2) ..(0,0)..controls (1,1) and (2,2) ..(3,3) Крайние точки сращиваемых путей должны совпадать в месте сцепления. /gK/§K Вы можете "замедлить ход времени" путем сцепления подпутей, задаваемых ука- JL JL занием дробных значений времени. Вот, например, что получиться, если вы зададите выражение вида 'subpath (0, .5) of р2 ft subpath (.5,2) of р2 ft cycle': (0,0)..controls (1,-1) and (2.25,-0.75) ..(3,0)..controls (3.75,0.75) and (4,2) ..(3,3)..controls (1,5) and (-2,2) ..cycle Когда для подпути 'subpath (0, .5) of рг' время t пробегает значения от 0 до 1, вы получаете те же самые точки, как если бы t для рг пробегало бы значения от 0 до .5; когда для 'subpath (.5,2) of рг время t пробегает значения от 0 до 1, вы получаете те же самые точки, как если бы t для рг пробегало значения от .5 до 1; но если для подпути 'subpath (.5, 2) of рг время t пробегает значения от 1 до 2, вы получите тот же отрезок пути рг от 1 до 2. fB заключение этой главы, давайте рассмотрим правила предшествования, в соответствии с которыми METAFONT решает, какие операции должны выполняться в первую очередь. Несмотря на то что неформальное понятие "магнетизма" дает наглядную картину происходящего, именно правила синтаксиса позволяют ответить на вопрос однозначно в сомнительных случаях. f Четыре уровня приоритета предшествования соответствуют четырем типам формул, которые называются, соответственно, первичными, вторичными, третичными формулами и выражениями. Первичная формула — это либо переменная, либо константа, либо какая-нибудь единица с прочной внутренней связью, как например '2х' или 'sqrt 2';
Глава 8: Алгебраические выражения 83 вторичная формула — это либо первичная формула, либо последовательность первичных формул, связанных между собой мультипликативными операторами, такими как '*' или 'scaled'; третичная формула — это либо вторичная формула, либо последовательность вторичных формул, связанных между собой аддитивными операторами, такими как '+' или '++'; наконец, выражение — это либо третичная формула, либо последовательность третичных формул, связанных между собой внешними операторами, такими как '<' или '..'. Например, выражение a+b/2>3c*sqrt4d составлено из первичных формул 'а', 'Ъ', '2', 'Зс' и 'sqrt4d'; последняя из этих первичных формул содержит в себе '4d', также первичную формулу. Подформулы 'а', 'Ь/2' и '3c*sqrt4d' являются вторичными формулами; а подвыражения 'а+Ъ/2' и '3c*sqrt4d' являются третичными формулами. fEom заключить выражение в скобки, то оно превращается в первичную формулу, которую, в свою очередь, можно использовать для построения больших вторичных и третичных формул, и т.д. ^Хотя полный список правил синтаксиса выражений очень длинный, все же большинство из них укладывается в достаточно простую схему. Если а, /3 и 7 — произвольные типы: numeric, boolean, string и т.д., то (переменная-а) означает "переменная типа а", (первичное /3) означает "первичная формула, значение которой имеет тип Р" и так далее.* Почти все правила синтаксиса имеют следующую структуру: (первичное а) —> (переменная-а) | (константа типа а) | ((выражение-а) ) | (оператор, перевод, тип /? в тип а) (первичное J3) (вторичное а) —> (первичное а) | (вторичное /?)(мульт. оп-р, переводящий типы /3 и j в тип а) (первичное 7) (третичное а) —> (вторичное а) | (третичное /?)(адд. оп-р, переводящий типы (3 и 7 в тип а) (вторичное 7) (выражение-а) —> (третичное а) | (выражение-/?) (внешн. оп-р, перевод, типы /3 и 7 в тип а) (третичное 7) Эта схема, будучи неполной, все же достаточно точно отражает общую идею устройства синтаксиса системы. /$\ Правила синтаксиса для всех типов выражений приводятся в главе 25. Мы рас- JL смотрим здесь лишь несколько случаев для выражений типа numeric и pair, чтобы вы смогли прочувствовать общую ситуацию: (первичное числовое) —> (элементарное число) | (элементарное число) [ (числовое выражение) , (числовое выражение) ] | length (первичная цепочка) | length (первичный путь) | length (первичная пара) | angle (первичная пара) | xpart (первичная пара) | ypart (первичная пара) | (числовой оператор) (первичное числовое) * То есть, когда мы говорим "первичное числовое", "вторичная пара", "третичное перо", то имеем в виду, соответственно, "первичная формула, значение которой есть число", "вторичная формула, значение которой есть пара", "третичная формула, значение которой есть перо". — Прим. перев.
84 Глава 8: Алгебраические выражения (элементарное число) —У (числовая переменная) | (первичная числовая лексема) | ((числовое выражение)) | normaldeviate (первичная числовая лексема) —У (числовая лексема) / (числовая лексема) | (числовая лексема, за которым не следует 7 (числовая лексема)') (числовой оператор) —У | sqrt | sind | cosd | mlog | техр | floor | uniformdeviate | (скалярный оп-р умножения) (скалярный оп-р умножения) —У (плюс или минус) | (числовая лексема, за которой не следует + или - или числовая лексема) (вторичное числовое) —У (первичное числовое) | (вторичное числовое) (умножить или поделить) (первичное числовое) (умножить или поделить) —у * \ / (третичное числовое) —У (вторичное числовое) | (третичное числовое) (плюс или минус) (вторичное числовое) | (третичное числовое) (пифагоровы плюс или минус) (вторичное числовое) (плюс или минус) —У + | - (пифагоровы плюс или минус) —У ++ | +-+ (числовое выражение) —У (третичное числовое) Синтаксис точно описывает все тонкости операции деления и тому подобных вещей. Например, при помощи этих правил можно вывести, что выражение *sind-l/3x-2' интерпретируется как '(sind(-(l/3x)))-2\ Заметим, что первый знак минус в этой формуле рассматривается как "скалярный оператор умножения", который имеет первичный уровень приоритета, в то время, как второй — означает вычитание и является элементом формулы типа (третичное числовое). Уровень приоритета операции "помещение в промежуточное положение" ('£[а,6]') считается первичным. f Некоторые из операций, которые мы пока не рассматривали, хотя и не присутствуют непосредственно в приведенных правилах, но укладываются в ту же общую схему. Например, как мы увидим позже, формулы 'ASCII(первичная цепочка)' и 'xxpart (первичное преобразование)' являются дополнительными альтернативами в правилах синтаксиса для формул типа (первичное числовое). С другой стороны, некоторые из операций, которые мы рассматривали в этой главе, отсутствуют в правилах синтаксиса, поскольку не являются примитивными операциями самой системы METAFONT, а определяются в базовом форматном файле (см. приложение В). Например, операция 'ceiling' аналогична операции 'floor', а операция '**' аналогична операции '*'. В главе 20 описывается, как правильно дополнять правила встроенного синтаксиса системы METAFONT. Так что при желании можно включать в них дополнительные операции. f> Упражнение 8.7 Как METAFONT интерпретирует '2 2'? (Здесь между двойками стоит пробел.) /^ч/^Л Упражнение 8.8 JL JL В соответствии с файлом expr.mf, значение '1/2/3/4' равно 0.66667; значение 'а/2/3/4' равно 0.375а. Объясните почему. f Правила синтаксиса для типов (выражение-пара) и (числовое выражение) похожи, поэтому их удобнее рассматривать вместе. (первичная пара) —У (переменная-пара) | ((числовое выражение) , (числовое выражение)) | ((выражение-пара)) | (элементарное число) [ (выражение-пара) , (выражение-пара) ] | point (числовое выражение) of (первичный путь) | (скалярный оп-р умножения) (первичная пара)
Глава 8: Алгебраические выражения 85 (вторичная пара) —> (первичная пара) | (первичная пара) (умножить или поделить) (первичное числовое) | (вторичное числовое) * (первичная пара) | (вторичная пара) (преобразователь) (преобразователь) —> rotated (первичное числовое) | scaled (первичное числовое) | shifted (первичная пара) | slanted (первичное числовое) | transformed (первичное преобразование) | xscaled (первичное числовое) | у scaled (первичное числовое) | zscaled (первичная пара) (третичная пара) —> (вторичная пара) | (третичная пара) (плюс или минус) (вторичная пара) (выражение-пара) —> (третичная пара) /$\ ►Упражнение 8.9 JL Исходя лишь из тех примеров, которые были рассмотрены в этой главе, попробуйте угадать правила синтаксиса для формул типов (первичная цепочка), (вторичная цепочка), (третичная цепочка) и (выражение-цепочка). [Указание; оператор '&' имеет такой же приоритет предшествования, как и '..'.] Там сидела девушка такой красоты, что ни нарисовать, ни словами выразить. ■ ЯКОБ и ВИЛЬГЕЛЬМ ГРИММ, Сказки (1815) Он с изумлением взглянул на это выражение. — ЭМЕЛИ БРОНТЕ, Грозовой перевал (1847)
Уравнения ?ФЕ55.
Глава 9: Уравнения 87 В системе METAFONT переменные приобретают значения в уравнениях (equations), которые выражают задуманные программистом соотношения. В предыдущей главе мы увидели, что алгебраические выражения составляют богатый язык, который позволяет работать как с числовыми, так и с графическими соотношениями. Поэтому можно точно описать множество различных объектов, устанавливая отношения равенства между определенными алгебраическими выражениями. Главное, что должен знать об уравнениях человек, составляющий программу для METAFONT, это: (1) как переводить интуитивные образы объектов на язык формальных уравнений и (2) как по формальным уравнениям восстанавливать интуитивные образы объектов. Иными словами, важно научиться, во-первых, писать уравнения и, во-вторых, читать уравнения, написанные вами или кем-нибудь другим. Это не так сложно, как может показаться на первый взгляд. Лучше всего учиться этому, побольше практикуясь и делая обобщения на основе конкретных примеров. Поэтому мы начнем эту главу с того, что дадим перевод на обычный язык изрядного количества уравнений. Уравнение а = 3.14 3.14 = о mode = smoke 2/з = 0 х9 = 0 хц = curve-sidebar Х\ = х2 Перевод Значение а должно быть равным 3.14. Число 3.14 должно быть значением а. (Это означает то же самое что и 'а = 3.14'; правую и левую стороны уравнения можно менять местами, при этом смысл его не изменится.) Значение mode должно быть равно smoke. (В базовом формате системы METAFONT величине 'smoke' присваивается специальное значение, поэтому, если программа mode_setup вызывается при mode = smoke, то, как объясняется в главе 5 и приложении Н, компьютер будет генерировать "дымовые" оттиски.) Координата у точки 3 должна быть равной нулю; то есть точка 3 должна лежать на базовой линии. (В терминах базового формата METAFONT точка 3 может быть представлена также в виде z$, что является краткой формой записи пары координат (#з>2/з)-) Координата х точки 9 должна быть равной нулю; то есть точка 9 должна лежать на левом краю ограничивающей рамки текущего символа. Координата х точки 1/ должна быть равной значению переменной с именем curve.sidebar. Таким образом, мы располагаем точку zu на некотором расстоянии от левого края рамки. Точки 1 и 2 должны иметь одинаковые координаты х\ то есть они должны занимать одинаковое положение по горизонтали, так, чтобы одна находилась непосредственно над или под другой.
88 Глава 9: Уравнения У4=2/5 + 1 Точка 4 должна быть расположена на один пиксель выше точки 5. (При этом, однако, точки 4 и 5 могут находиться далеко друг от друга; в этом уравнении ничего не говориться о том, как связаны координаты хА и хъ.) у6 = у7 + 2 гага Точка б должна быть расположена на один миллиметр выше точки 7. (Программа mode.setup, определенная в базовом формате, устанавливает значение переменной гага равным числу пикселей в миллиметре, исходя из разрешения, определяемого переменными mode и тад.) хА —и) — .01 in Точка 4 должна лежать внутри ограничивающей рамки, на расстоянии одной сотой дюйма от ее правого края. (Программа beginchar, определенная в базовом формате, устанавливает значение переменной w равным ширине, в пикселях, символа, который изображается в данный момент.) ?/4 = -5/i Точка 4 должна лежать на середине пути от базовой линии до верхнего края элемента набора. (Программа beginchar, определенная в базовом формате, устанавливает значение переменной h равным высоте, в пикселях, текущего символа.) ув = —d Точка б должна лежать ниже базовой линии, на нижнем краю ограничивающей рамки. (Напомним, что каждый символ ограничен прямоугольной рамкой, верхний левый угол которой имеет координаты (0, h), верхний правый угол — (w,h), нижний левый угол — (0, — d), а нижний правый угол — (w, — d). Переменная d представляет глубину рамки относительно базовой линии. Значения переменных w, 1ги d могут меняться от символа к символу, так как в компьютерных шрифтах, рамки отдельных символов не обязательно должны иметь одинаковый размер.) ys = .5 [Л, — d\ Точка 8 должна лежать посередине между верхним и нижним краями ограничивающей рамки. w — х$ = |хб Расстояние от точки 5 до правого края рамки должно составлять две трети от расстояния между точкой б и левым краем рамки. (Выражение w — х*, определяет расстояние от точки 5 до правого края, поскольку w — координата правого края.) zq = (0,0) Точка 0 должна находиться в точке отсчета текущего символа, то есть лежать на пересечении базовой линии и левого края элемента набора. Это уравнение содержит в себе пару уравнений: хо — 0 и у о = 0, поскольку, если пара координат удовлетворяет некоторому уравнению, то координаты х и у также должны
Глава 9: Уравнения 89 ему удовлетворять. (В базовом формате METAFONT определена переменная origin со значением (0,0); поэтому данное уравнение можно также записать в виде lz0 = origin1.) z9 = (tu, h) Точка 9 должна совпадать с правым верхним углом ограничивающей рамки текущего символа. top zg = (.5iu, /i) Если центр пера, которое выбрано в данный момент, находится в точке 8, то его верхний край должен совпадать с верхним краем ограничивающей рамки. Кроме того, координата xg должна быть равна .5и>; то есть, точка 8 должна находиться на равном расстоянии от левого и правого краев рамки. (Более подробные примеры использования операторов Чор\ cbot\ 'Z/fc' и 'г£' содержатся в главе 4.) z\ = | [2:5, zq] Точка 4 должна отсекать три седьмых части от отрезка между точками 5 и 6. ^12 — *п — z\\ — zi3 Вектор, ведущий из точки 11 в точку 12, должен быть равен вектору, ведущему из точки 13 в точку 14. Иными словами, точка 12 должна быть расположена относительно точки 11 так же, как точка 14 расположена относительно точки 13 (на том же расстоянии, и в том же направлении). zz — Z2 = Точки 3 и 4 должны находиться на одинаковом рас- = (z4 — Z2) rotated 15 стоянии от точки 2, но вектор направления в точке 3 должен быть повернут на 15 градусов против часовой стрелки, относительно вектора направления в точке 4. ► Упражнение 9.1 Переведите на обычный язык следующие уравнения: (a) x-j — 9 = х\\ (b) z-i = (аг4, .5[у4,2/б]); (с) Ift z2i = rt z20 + 1. ► Упражнение 9.2 Убедитесь, достаточно ли ваших знаний об уравнениях для того, чтобы записать уравнения, соответствующие следующим объектам: (а) точка 13 должна быть настолько ниже базовой линии, насколько точка 11 выше базовой линии; (Ь) точка 10 должна быть расположена на один миллиметр правее и на один пиксель ниже точки 12; (с) точка 43 должна отсекать одну треть отрезка между левым верхним и правым нижним углами элемента набора. Давайте снова рассмотрим шесть точек (2:1,2:2, zs,Z4,zs, zq), которые мы так часто использовали в примерах в главах 2 и 3. Немного изменив обозначения, можно определить эти точки как (*i,2/i) = (0,/i); (ж2, Ы = (-5м;, Л); (я3,уз) = (w>hh 0*4,2/4) = (0,0); (хъ,уь) = (.5гу, 0); (а?6, Уе) = (ю, 0). Существует множество способов задать эти точки путем записи ряда уравнений. Например, это можно сделать при помощи только что приведенных уравнений; кроме того, вместо длинных обозначений (xi,yi), ..., (яб>2/б)> можно использовать
90 Глава 9: Уравнения краткие обозначения z\, ..., zq. Но есть и другие способы, позволяющие задать эти точки, и в то же время пояснить, как они соотносятся друг с другом. Можно, например, задать координаты а; и у по отдельности: хх = Х4 = 0; Х2 = х$ = .5w; xz = xq = w\ 2/1=2/2 = 2/3 = Л; 2/4 = 2/5 = 2/6 = 0. METAFONT позволяет записывать сразу несколько уравнений, используя более одного знака равенства. Например, lyi = 2/2 = 2/з = Л* означает то же, что и три уравнения: 2/1 = 2/2, 2/2 = 2/з и у3 = Л. Для того, чтобы задать координаты шести точек, нужно написать двенадцать уравнений, поскольку каждое уравнение задает одно значение, а координат у шести точек в общей сложности — двенадцать. Однако каждое уравнение для пар считается за два уравнения для одиночных чисел. Именно поэтому в первой последовательности уравнений нам удалось обойтись шестью знаками '=', тогда как во второй пришлось использовать двенадцать таких знаков. Давайте рассмотрим еще один способ определения наших шести точек, путем задания их положений по отношению друг к другу: Z\ — <*4 = Z2 — 25 = 23 — Zq\ Z2 — Z\ — Zz ~ Z2 = Zs — 24 = Zq — Z5; Z4 = origin; zz = (w,h). Вначале мы указываем, что векторы из точки z± в точку 2i, из точки z^ в точку Z2 и из точки zq в точку zz равны между собой. Затем задаем равенство векторов из z\ в Z2, из Z2 в zz, из z^ в z^ и из z^ в Zq. И наконец, угловые точки z\ и zz мы задаем непосредственно. Таким образом, мы имеем семь уравнений для пар координат, а этого более чем достаточно, чтобы задать интересующие нас шесть точек. Но оказывается, что этих семи уравнений недостаточно! Например, следующие шесть точек: z\ — 24 = (0,0); z2 = z5 = (.5гу, .5h); zz = z6 = (iu, h) также удовлетворяют этим уравнениям. При более пристальном рассмотрении мы понимаем, почему формулы Z\ — Z4 = Z2 — Z5 И Z2 — Z\ = 25 — Z4 эквивалентны. (Прибавьте к обоим сторонам первого уравнения z^ — Zi, и получите lzs —Z4 = Z2-Z1'.) Точно так же Z2—Z5 = zz—zq — то же самое, что nzz—Z2 = zq-25. Два из семи уравнений не содержат новой информации, поэтому на самом деле мы задали лишь пять уравнений, а этого недостаточно. Для того чтобы решение стало единственным, необходимо задать еще одно соотношение, например, kz\ = (0,/i)\ f> Упражнение 9.3 (Для математиков.) Найдите такое решение данной системы из семи уравнений, в котором zi = Z2. Найдите также решение, в котором zi = zq. Изначально переменные в программе для METAFONT не имеют никаких значений, за исключением некоторых из них, например, smoke или origin, которым в базовом формате METAFONT приписываются специальные значения. Кроме того, когда вы переходите к новому символу с помощью команды beginchar, все предыдущие значения, которые, возможно, были присвоены я и у, уничтожаются. Значения определяются постепенно по мере того, как компьютер считывает уравнения и
Глава 9: Уравнения 91 пытается отыскать для них решения, которые бы согласовывались с уравнениями, встречавшимися в программе ранее. Чтобы определить значения десяти переменных, нужно иметь десять уравнений. Если задано только девять уравнений, то может оказаться, что ни одна из переменных не определена. Например, имея девять уравнений 0о = 01 = 02 = 0з = 04 = 05 = 06 = 07 = 08 = 09, мы не можем определить значение ни одной из переменных д. Но если задать еще одно уравнение 00 + 01 = 1, то METAFONT сможет вывести, что значения всех десяти д равны |. Система METAFONT всегда вычисляет значения как можно большего числа переменных, исходя из считанных на данный момент уравнений. Например, считав два уравнения а + Ъ + 2с = 3; а-Ь-2с= 1; машина определит, что а = 2 (т.к., сложив эти уравнения, мы получим '2а = 4'); но все, что ей будет известно о переменных бис, — это то, что Ь + с = 1. В любом месте программы та или иная переменная является либо "известной" (known), либо "неизвестной" (unknown), в зависимости от того, можно ли вывести его значение из имеющихся уравнений единственным образом. Примеры выражений из главы 8 показывают, что METAFONT умеет производить множество различных вычислений с неизвестными переменными. Однако в некоторых случаях величину можно использовать только тогда, когда ее значение известно заранее. Например, METAFONT может умножить неизвестное число или пару на известное число, но не может перемножить две неизвестные переменные. Уравнения могут следовать в любом порядке, за исключением тех случаев, когда требуется поместить некоторые уравнения перед другими для того, чтобы в последних значения основных переменных были известными. К примеру, для уравнений *а+6+2с = 3; а-Ь-2с = 1; 6+с = 4' система METAFONT отыщет решение (а, 6, с) = (2,7, —3) независимо от того, в каком порядке они будут записаны (скажем, '6+с = 4; а—Ь-2с = 1; а+Ь+2с = 3'). Но если бы уравнения имели вид 'а+&+2с = 3; а —6 —2с = 1; а*(Ь+с) = 8', то вы бы не смогли поставить на первое место последнее уравнение, так как METAFONT отказался бы умножать неизвестную величину а на неизвестную величину Ь + с. Ниже приводится список основных операций, которые METAFONT может выполнять с неизвестными величинами: — (неизвестная) (неизвестная) -I- (неизвестная) (неизвестная) — (неизвестная) (неизвестная) * (известная) (известная) * (неизвестная) (неизвестная) / (известная) (известная) [(неизвестная), (неизвестная)] (неизвестная) [(известная), (известная)] В некоторых операциях базового формата METAFONT, определенных в прилож- нии В, также участвуют неопределенные величины. Например, можно указать
92 Глава 9: Уравнения top (неизвестная), hot (неизвестная), Ift (неизвестная), rt (неизвестная) и даже penpos(суффикс)((неизвестная), (известная)). Если известна разность а—6, то в программе для METAFONT допустимо выражение '(неизвестная)[а,6]', а также сравнение переменных а и 6 в булевых выражениях типа 1а < 6\ Величина а — 6 может быть известной, даже если сами а и 6 неизвестны. Вас может удивить, как системе METAFONT удается постоянно обновлять свои знания, основываясь на обрывочной и неполной информации, поступающей к ней из разных уравнений. Чтобы понять это, лучше всего проследить, как все происходит, попросив компьютер показывать вычисления, которые он обычно скрывает. Сделать это можно, например, так: запустите METAFONT и наберите \tracingequations:=tracingonline:=1; в ответ на приглашение '**'. (Не забудьте набрать '\\ и используйте ':-', вместо '='. В главе 27 мы увидим, что систему METAFONT можно попросить отслеживать многие виды выполняемых ею действий.) Теперь наберите а+Ъ+2с=3; в ответ машина скажет: ## с=-0.5Ъ-0.5а+1.5 поскольку именно так она восприняла ваше уравнение. (Символы '##' в этой строке указывают на то, что это информация, связанная с tracingequations.) Теперь наберите а-Ъ-2с=1; METAFONT прочтет это как 'а-Ъ-2(-0.5Ъ-0.5а+1.5)=Г, поскольку она уже знает, как заменить переменную с выражением, в котором присутствуют только а и Ь. Это новое уравнение можно упростить, раскрыв скобки и приведя подобные в его левой части. В результате получится '2а-3=1\ Поэтому METAFONT ответит ## а=2 и снова настанет ваша очередь что-нибудь напечатать. Наберите showdependencies; METAFONT ответит с=-0.5Ъ+0.5 показывая, что есть всего одна переменная, значение которой зависит от остальных, и уравнение, выражающее эту зависимость, теперь имеет вид 'с = —0.56 + 0.5'. (Уравнение 'с = —0.56 — 0.5а + 1.5', выражавшее эту зависимость ранее, упростилось, поскольку теперь стало известным еще одно значение, 'а = 2'.) Наконец, наберите Ъ+с=4; Компьютер ответит ## Ъ=7 #### с=-3 Строка, которая начинается с '##', содержит утверждение, которое система METAFONT вывела из уравнения, считанного ею только что; а строка, которая начинается с '####', содержит косвенное следствие этого прямого результата, если он позволяет определить значение переменной, которая ранее была зависимой.
Глава 9: Уравнения 93 f Только что начатый нами эксперимент интересно было бы продолжить, вводя по одной следующие строки и наблюдая, что при этом происходит: а'+Ъ' + .бс^З; a'-b'-.Bc'-l; g0=gl=g2=g3=g4; shovdependencies; gO+gl=l; zl-z4=z2-z5=z3-z6; z2-zl=z3-z2=z5-z4=z6-z5; z4=origin; z3=(v,h); xl=0; y6=0; v=2h=100; end. Заметьте, что в ответ на шестую строку ^z\—z\ — • •') система METAFONT выдаст четыре уравнения, а в ответ на следующую ('гг — 2i = •••') — только два. Это происходит потому, что, как мы видели ранее, в этой строке очень много ненужной информации. ^Этот сеанс работы с компьютером показывает, что METAFONT имеет дело с двумя видами неизвестных числовых переменных: зависимыми и независимыми. В начале своей жизни всякая переменная является независимой, но с каждым новым уравнением одна из независимых переменных становится либо зависимой, либо известной. В каждой строке с '##' в начале, выданной командой tracing equations, показывается переменная, которая только что стала зависимой или известной, а также эквивалентное ей уравнение, в котором фигурируют только независимые переменные. Например, строка '## с=-0.5Ь-0.5а+1.5' означает, что переменная с только что стала зависимой, и она равна — ^6 — ^а + 1.5, где переменные а и 6 — независимы. Точно так же, '## а=2' означает, что переменная а только что из независимой превратилась в известную. Когда независимая переменная v становится зависимой или известной, происходит обновление всех выражений, эквивалентных независимым переменным, так что они более не зависят от г»; в процессе этого обновления некоторые из них или все они могут превратиться из независимых в известные, после чего будет выдана строка с '####' в начале. /gK/gN> Считывая уравнение, в котором фигурируют только числовые величины, система JL JL METAFONT заменяет все известные переменные их значениями, а все зависимые — эквивалентными им выражениями. Получающееся в результате уравнение можно представить в виде СИЛ Н VCmVm = С*, где все с представляют собой ненулевые константы, а все v являются независимыми переменными; а — числовая константа, которая может быть нулем. Если какой-либо коэффициент Ck мал настолько, что скорее всего он был бы равен нулю не будь в вычислениях ошибок округления, то он заменяется нулем и соответствующая переменная Vk удаляется из уравнения. При т = 0 уравнение рассматривается либо как лишнее (если значение а равно нулю или очень мало), либо как несовместное (в противном случае). Но если га > О, то система METAFONT выбирает независимую переменную Vk, для которой коэффициент с* имеет наибольшее значение, и переписывает уравнение в виде ## vk - (а - CiVi Ck-lVk-l - Cfc+iVfc+i cmVm)/ck. Переменная Vk становится зависимой (если m > 1) или известной (если га = 1).
94 Глава 9: Уравнения Несовместными (inconsistent) называются уравнения, которые не имеют решения. Например, если вы укажете 0 = 1, то система METAFONT выдаст вам сообщение об ошибке, в котором будет говориться, что уравнение "привирает на 1" ( "off by 1"). Менее очевидное противоречие возникнет, если указать, например, 'а = 6+1;Ь = с+1;с = а + Г; здесь последнее уравнение "привирает на 3", если верить предыдущим уравнениям, которые говорят, что с = 6 — 1 = а — 2. После того как работа компьютера будет возобновлена, он просто проигнорирует несовместное уравнение. Лишними (redundant) называются уравнения, не несущие новой информации. Например, уравнение '0 = 0' — лишнее. Уравнение 'а = b 4- с' также является лишним, если до этого вы уже указали, что 'с = а — Ь\ Если вы вставите между двумя числовыми выражениями лишнее уравнение, то METAFONT остановится, выдав сообщение об ошибке, так как обычно это указывает на недочет в программе. Но если уравнение, связывающее переменные-пары, влечет за собой одно или два лишних уравнения, связывающих числовые переменные, то сообщение об ошибке не выдается. Например, уравнение 42з = (0,/i)' не вызовет сообщения об ошибке, если даже до этого было установлено, что хз = 0 или уз = h или и то, и другое. Иногда для того, чтобы привести уравнение к такому виду, в котором система METAFONT сможет его обрабатывать приходится над ним поработать. Например, если у — независимая или зависимая переменная, вы не можете указать х/у = 2, поскольку METAFONT позволяет делить только на известные величины. А вот уравнение х = 2у по сути то же, но не вызывает у компьютера затруднений. Кроме того, это уравнение корректно даже в том случае, когда у — 0. Способность системы METAFONT запоминать предыдущие уравнения ограничивается, как сказано выше, уравнениями, выражающими линейные зависимости. Математик, возможно, захочет представить условие х > 0 в виде уравнения 'х = absx', но с таким условием METAFONT работать не умеет. Точно так же, система METAFONT не справится с уравнением 'ж = floor х', которое выражает тот факт, что х — целое число. Решения систем уравнений, содержащих модули чисел и/или операцию 'floor', могут быть очень сложными, а METAFONT вовсе не претендует на роль математического гения. Приведенные выше правила объясняют, как независимая переменная может стать зависимой или известной. При исключительных обстоятельствах возможно и обратное: зависимая переменная снова может стать независимой. Предположим, например, что в приведенном выше примере за уравнением 'а 4- Ь 4- 2с = 3' следует уравнение 'd — Ь 4- с 4- а/4'. Тогда зависимых переменных будет две: ## с=-0.5Ъ-0.5а+1.5 ## d=0.5b-0.25a+1.5 Теперь, предположим, что далее следует объявление 'numeric а', означающее, что предыдущее значение переменной а должно быть уничтожено. Система METAFONT не может просто так стереть независимую переменную, от которой что-то зависит. Поэтому она выбирает зависимую переменную, которая займет место переменной а. Компьютер выдаст сообщение ### 0.5а=-0.5Ъ-с+1.5 означающее, что, прежде чем переменная а будет уничтожена, во всех уравнениях зависимости 0.5а будет заменено на —с— ^6+§. Переменная с теперь снова является независимой; команда 'showdependencies' покажет, что теперь единственная зависимая переменная —
Глава 9: Уравнения 95 это переменная с/, равная 0.756 4- 0.5с + 0.75. (Это верно, поскольку, исключив а из двух данных уравнений, мы получим Ad = ЗЬ+2с+3.) На роль независимой обычно выбирается та переменная, которая в уравнении зависимости имеет наибольший коэффициент при уничтожаемой переменной. f Часто возникает потребность указать, что та или иная точка принадлежит той или иной линии. Это можно сделать, используя специальную функцию базового формата METAFONT, которая называется 'whatever' и обозначает безымянную числовую переменную, которая при каждом ее использовании имеет новое неизвестное значение. Например, z\ = whatever [z2,zz] означает, что точка 1 лежит где-то на прямой, проходящей через точки 2 и 3. (Выражение £[22,23] представляет всю эту прямую, если t принимает все значения от —со до -Ьоо. Мы хотим, чтобы точка z\ совпадала с £[22,23] при некотором значении £, безразлично каком.) Выражение * whatever [22, 23]' корректно, если известна разность 22 — 23; обычно его используют, когда известны обе точки — 22 и 2з, то есть когда они заданы предыдущими уравнениями. /$К Вот еще несколько примеров уравнений, в которых используется l whatever \ а JL также их перевод. Эти уравнения более интересны по сравнению с теми непритязательными уравнениями, которые мы рассмотрели в начале этой главы, поскольку они выявляют поразительную способность компьютера выводить строго определенные значения из нестрогих утверждений. Уравнение Перевод 25 — 24 = whatever * dir 30 Угол наклона отрезка между точками 4 и 5 должен составлять 30° над горизонтом. (Это уравнение можно переписать как '24 = z$ + whatever * dir 30'. В таком виде оно означает, что для того, чтобы попасть в точку 4 из точки 5, нужно сместиться на некоторое расстояние в направлении dir 30.) z7 — 2б = whatever * (23 — 22) Прямая, проходящая через точки 6 и 7, должна быть параллельна прямой, проходящей через точки 2 и 3. penposg(whatever ,60) Угол наклона имитируемого пера в точке 8 должен равняться 60°; ширина пера не установлена, поэтому она будет определена другим уравнением. f* Упражнение 9.4 Как можно попросить METAFONT вычислить точку 2, лежащую на пересечении ПрЯМЫХ 21 . . 22 И 2з • . 24, вСЛИ ТОЧКИ Z\, 22, 23 И 24 ИЗВвСТНЫ? f> Упражнение 9.5 Даны пять точек — 2i, 22, 23, 24 и 25. Объясните, как вычислить точку 2, лежащую на прямой 2i .. 22, такую, что прямая z .. 23 параллельна прямой 24 .. 25. f> Упражнение 9.6 Какое уравнение в системе METAFONT выражает тот факт, что прямая, проходящая через точки 11 и 12, перпендикулярна прямой, проходящей через точки 13 и 14? f> Упражнение 9.7 (Для математиков.) Даны три точки — zi, 22 и 23. Объясните, как вычислить расстояние от точки 2i до прямой, проходящей через точки 22 и 23.
96 Глава 9: Уравнения ► Упражнение 9.8 (Для математиков.) Даны три точки — zi, 2:2, zz и длина /. Объясните, как вычислить на прямой zi .. z$ две точки, которые находятся на расстоянии / от точки z\. (Считайте, что / больше, чем расстояние от точки z\ до прямой.) ► Упражнение 9.9 До сих пор функция 'whatever' использовалась только в уравнениях, связывающих пары чисел, но не использовалась в уравнениях, связывающих обычные числа. Объясните, почему уравнения типа 'а -I- 26 = whatever' были бы бесполезны. Все уравнения, которые рассматривались ранее в этой главе, связывали только числовые выражения или выражения-пары. Но, на самом деле, в системе METR- FONT уравнения могут связывать величины любого из восьми типов. Например, если si и 52 — цепочки, то вы можете написать sl="go"; sl&sl=s2 Этим вы установите значения si ="go" и S2 ="gogo". Более того, если далее вы зададите уравнения s3=s4; s5=s6; s3=s5; s4=sl&"sh" то машина сможет определить, что se ="gosh". Однако уравнения, в которых фигурируют переменные нечисловых типов, обладают далеко не всеми свойствами "числовых" уравнений. Например, из уравнения ,,h,,&s7=,,heck" невозможно определить, что S? = "eck", поскольку оператор сцепления к применим только к известными цепочкам. Если после объявления 'string s[]' задать уравнение 'sl=s2=s3', то команда 'show sO' даст результат 'unknown string sO'; но команда 'show si' даст результат 'unknown string s2'. Точно так же, команды 'show s2' и 'show s3' дадут в результате 'unknown string s3' и 'unknown string si', соответственно. Вообще, если приравнять несколько нечисловых переменных, то они будут указывать друг на друга в определенном циклическом порядке.
Глава 9: Уравнения 97 Пусть "X" — это роспись моего отца. — ФРЕД АЛЛЕН, Vogues (1924) ВСЕ ЖИВОТНЫЕ РАВНЫ МЕЖ СОБОЙ, НО НЕКОТОРЫЕ БОЛЕЕ РАВНЫ, ЧЕМ ДРУГИЕ — ДЖОРДЖ ОРУЭЛЛ, Скотный двор (1945)
10 Присваивания
Глава 10: Присваивания 99 Как объяснялось в предыдущей главе, переменные обычно обретают значения в уравнениях. Но существует и другая возможность, когда вместо знака '=' используется знак ':='. Например, когда в программе io.mf из главы 5 нам нужно было определить значение переменной stem#, мы указали: stem# := trial.stem * pt# Оператор ':=' (знак равенства с двоеточием) означает: "стереть предыдущее значение переменной и присвоить новое". Назовем это действие операцией присваивания (assignment). В программе io .mf было удобнее задать значение stem# не посредством уравнения, а посредством присваивания, потому что переменная stem# в пределах одного шрифта принимала несколько различных значений. Иначе это можно было бы сделать, указав: numeric stem#; stem# = trial_stem * pt# (то есть, прежде чем использовать переменную stem# в уравнении, мы специально уничтожаем ее предыдущее значение, делая ее неопределенной). Как видим, эта конструкция более громоздка. Переменная, которая находится слева от ':=', может присутствовать и в правой части присваивания. Например, code := code + 1 значит: "увеличить значение переменной code на Г'. Если рассматривать эту запись как уравнение, она будет бессмысленной, поскольку уравнение 'code = code + 1' несовместно. В момент, когда вычисляется значение 'code + Г в правой части, переменная code имеет прежнее значение, так как старые значения не уничтожаются до последнего момента — они сохраняются вплоть до присваивания нового значения. f ►Упражнение 10.1 Можно ли добиться эффекта ''code := code 4- 1' посредством уравнений и объявлений типа numeric, не используя присваиваний? В присваивании величина слева от ': =' должна быть переменной. Нельзя, например, указать 'code+lr^code'. И, что более важно, не разрешены такие присваивания, как Чх,у) : = (0,0)', хотя, если переменная w объявлена как пара, вы можете указать 'w: = (0,0)\ Следовательно, присваивание 'zl:=z2' неверно, так как представляет собой сокращение недопустимой конструкции 4x1,х2) : = (yl,y2)'; не следует забывать, что zl на самом деле не переменная, а пара переменных. В ограничении из предыдущего абзаца нет ничего страшного, поскольку в программах для METAFONT присваивания играют сравнительно скромную роль. В программах, как правило, лучше использовать уравнения, а не присваивания, так как уравнения указывают на соотношения между переменными в декларативной форме. Тот, кто слишком часто использует присваивания, все еще находится во власти старого стиля, присущего императивным языкам программирования. Старый стиль предполагает необходимость подробно объяснять компьютеру все, что он должен сделать. Однако механизм решения уравнений системы METAFONT позволяет поручить большую часть работы компьютеру, освобождая нас от этого сложного стиля программирования. Значения переменной до и после присваивания различны. Поэтому использование присваиваний часто определяет порядок следования утверждений в
100 Глава 10: Присваивания программе. В этом смысле уравнения проще присваиваний, так как их обычно можно записывать в любом удобном для вас порядке. Безусловно, присваивания имеют свою область применения, иначе система METAFONT вообще не стала бы морочить себе голову этим ':='. Но опытные программисты, пишущие на языке METAFONT, стараются использовать их как можно реже — только тогда, когда на то имеются серьезные причины, поскольку, как правило, уравнения более очевидны и информативны. Внутренние величины системы METAFONT, такие, как tracing equations, всегда имеют известные значения. Поэтому изменить их можно только при помощи присваиваний. Эксперимент из главы 9 начинался с присваивания \tracingequations:=tracingonline:=1; Как видим, присваивания, как и уравнения, можно записывать подряд, по нескольку в одном утверждении. Вот полные правила синтаксиса для присваиваний: (уравнение) —> (выражение) = (правая сторона) (присваивание) —> (переменная) : = (правая сторона) (правая сторона) —> (выражение) | (уравнение) | (присваивание) Правила позволяют комбинировать уравнения и присваивания. Например, 'а + b = с := d 4- е' — это то же самое, что присваивание 'с := d + е' и уравнение la + b = с\ В такой комбинации уравнения с присваиванием, как la + b = b := 6+1', при вычислении значения выражения используется старое значение 6. Например, если до присваивания 6 было равно 3, то результат будет таким же, как для (а + 3 = 6:=3 + Г; таким образом, значение 6 будет установлено равным 4, а значение а — равным 1. ► Упражнение 10.2 Допустим, вы хотите "обновить" переменную х, то есть сделать ее абсолютно независимой от предыдущих значений, но при этом вы не хотите потерять значения переменных xi и £2- Вы не можете указать 'numeric х[]\ так как этим уничтожите предыдущие значения всех Хк- Что можно сделать вместо этого? ► Упражнение 10.3 Примените METAFONT к следующей короткой программе: string s[]; si = S2 = S3 = S4; 55 = «в; $2 := ss; showvariable s; и объясните полученный результат. Если от переменной v, которой присваивается новое значение, зависят другие переменные, то эти переменные не меняются в соответствии с выполненным присваиванием. Они по-прежнему действуют так, будто зависят от предыдущего (неизвестного) значения v. Например, если после уравнения '2ti = 3v = tu' идет присваивание kw := б', то значения и и v не станут известными, но METAFONT будет по-прежнему помнить, что v = .66667ti. (Это — не новое правило, а следствие уже упомянутых правил. Как говорилось в главе 9, при уничтожении независимой переменной какая-нибудь зависимая переменная может стать независимой и занять ее место.) ► Упражнение 10.4 Примените METAFONT к программе: tracing equations := tracingonline := 1; а = 1; а:=а + 6; а:=а + 6; а := а + 6; show а, 6; и объясните полученный результат.
Глава 10: Присваивания 101 Сначала это поручение* его обрадовало, но, по мере того как нарастала усталость, он все больше раздражался. — К. Е. МАЛФОРД, Hopalong Cassidy (1910) (left part) ::= {variable) := (left part list) ::= (left part) | (left part list)(left part) (assignment statement) ::= (left part list)(arithmetic expression) \ (left part list)(Boolean expression) — ПИТЕР HOP и др., Доклад об алгоритмическом языке ALGOL 60 (1960) * Слово assignment (присваивание) в английском языке имеет также смысл "поручение". Это, кстати, объясняет смысл рисунка в начале главы. — Прим. иерее.
11 Увеличение и разрешение
Глава 11: Увеличение и разрешение 103 При помощи одной и той же программы для METAFONT можно генерировать шрифты для множества различных устройств печати. Для этого программист должен составить программу так, чтобы можно было изменять установки разрешения. В базовом форматном файле, описанном в приложении В, вводится ряд соглашений, благодаря которым вносить такие изменения становится довольно просто. В настоящей главе рассматриваются эти соглашения. Предположим для определенности, что у нашего компьютера есть два устройства вывода. Одно из них называется cheapo и имеет разрешающую способность 200 пикселей на дюйм (что составляет приблизительно 8 пикселей на миллиметр); второе называется luxo и имеет разрешающую способность 2000 пикселей на дюйм. Наша цель — научиться составлять такие программы для METAFONT, которые позволяли бы генерировать шрифты для обоих устройств. Так, например, если файл newface.mf содержит программу генерирования нового шрифта, то было бы желательно иметь возможность генерировать шрифт с низким разрешением, обратившись к системе METAFONT с командами \mode=cheapo; input newface а шрифт с высоким разрешением — при помощи того же файла, но указав в начале сеанса \mode=luxo; input newface Файл newface.mf должен быть составлен так, чтобы его могли использовать и другие люди при собственных значениях mode, соответствующих характеристикам используемых ими устройств печати. В системе METAFONT базового формата это можно сделать, вызвав в начале файла newface.mf программу mode.setup. Программа mode_setup устанавливает значения таких переменных, как pt и mm, которые представляют количество пикселей, приходящееся соответственно на пункт и миллиметр. Например, когда mode = cheapo, значения их будут равны pt = 2.7674 и mm = 7.87402; когда mode = luxo, значения будут равны pt = 27.674 и mm = 78.74017. Программа newface.mf должна быть написана в терминах этих переменных так, чтобы в режиме cheapo массивы пикселей, образующие символы, были примерно в 10 раз уже и в 10 раз короче, чем те же схемы в режиме luxo. К примеру, проведя прямую из точки (0,0) в точку (3mm,0), в режиме cheapo мы получим линию длиной около 23.6 пикселей, а в режиме luxo — линию длиной около 236.2 пикселей. Первая из них будет иметь длину 3 mm, если ее печатать в режиме cheapo, а вторая будет иметь на вид 3 mm в длину, если ее печатать в режиме luxo. Дополнительные трудности связаны с увеличением шрифта, когда размер шрифта не соответствует его номинальному размеру. Например, нам может понадобиться набор шрифтов для режима cheapo, которые в два раза больше обычного, чтобы пользователи могли создавать слайды для диапроекторов. (Кроме того, результат можно уменьшить при печати до 50% его размера. Если затем размножить его при помощи соответствующего оборудования, то это позволит увеличить эффективное разрешение с 200 до 400.) Система TeX позволяет увеличивать полученный документ целиком в число раз, кратное 2, посредством команды типа '\magnif ication=2000'; отдельные шрифты в системе TeX также могут быть увеличены, например, таким образом: '\font\f=newfасе scaled 2000'. Существует стандартный способ получения шрифта, увеличенного в два раза, с использованием
104 Глава 11: Увеличение и разрешение соглашений базового формата METAFONT. Это можно сделать, указав, например, \mode=cheapo; mag=2; input newface; этим мы установим значения pt = 5.5348 и mm = 15.74803. Программа mode.setup определяет, известно ли значение переменной тад и, если оно неизвестно, устанавливает тад = 1. Если неизвестно значение переменной mode, то mode_setup устанавливает mode = proof. В базовом формате METAFONT, кроме pt и mm, вычисляются значения еще нескольких величин, которые могут использоваться для измерения длин. Они соответствуют единицам, которые "понимает" система TeX. Вот их полный список: pt типографский пункт (72.27 pt = 1 in) рс пика (lpc = 12pt) in дюйм (lin = 2.54 cm) bp большой пункт (72 bp = 1 in) cm сантиметр (100 cm = 1 метр) mm миллиметр (10 mm = lcm) dd дидот (1157dd= 1238 pt) ее цицеро (lcc=12dd) Все значения округляются до ближайшего целого числа пикселей, кратного 65^36. Хотя все эти физические единицы измерения доступны, в традиционных шрифтах они используются редко. Разработчики обычно вводят другие единицы измерения, такие, как 'em' и lx.height\ для того чтобы определять через них размеры букв. Такие величины имеют, как правило, специальные значения, которые меняются от одного шрифта к другому. Система METAFONT базового формата позволяет с легкостью вводить специальные единицы измерения, которые будут меняться в зависимости от разрешения и увеличения, точно так же, как pt и тт. Для этого вам нужно всего-навсего определить "точные" единицы измерения с такими же именами, как у пиксельных единиц измерения, но с дописанным в конце суффиксом '#'. Например, ега# и x_height# (печатается как 'ет#' и 'x_height#') — это точные единицы измерения, соответствующие ет и x_height. Для перечисленных выше стандартных единиц в базовом формате METAFONT заранее определены величины pt#, рс#, in#, bp#, сга#, тт#, dd# и сс#. Точные единицы, такие, как ега# и x_height#, всегда должны определяться через переменные, выраженные в единицах, не зависящих от разрешения, например, р£#, т# и т.п., чтобы их значения не менялись при изменении значений mode и тад. Знак '#' обеспечивает постоянство значений. После вызова программы mode_setup пиксельные единицы измерения можно вычислить, указав просто define_pixels(em, x_height). Эта команда представляет собой краткую форму записи присваиваний ет := ет# * hppp; x_height :— xJxeight# * hppp, где hppp — внутренняя переменная METAFONT, которая представляет число пикселей, приходящееся на пункт по горизонтали. В одном утверждении с командой define_pixels может перечисляться любое количество специальных единиц измерения. Заметим, что '#' — это не оператор, который переводит ет в ет#; ошибки округления будут зависеть от режима работы.
Глава 11: Увеличение и разрешение 105 В программе io.mf, рассмотренной нами в главе 5, имеется несколько единиц измерения, определяемых описанным образом. Кроме того, в ней имеется утверждение вида: define_blacker_pixels(£/itn, thick); Хотите знать, что это такое? Пожалуйста: согласно приложению В, это краткая форма записи присваиваний thin :— thin* * hppp + blacker; thick := thick* * hppp 4- blacker; Иными словами, здесь точные единицы измерения переводятся в неточные. Для этого их сначала переводят в пиксели, а затем к ним добавляют некий *blacker'. Переменная blacker — это специальная поправка, которая вносится для того, чтобы адаптировать шрифт к особенностям конкретного устройства вывода. Значение blacker определяется программой mode_setup по значению переменной mode. Например, в режиме cheapo желательно иметь blacker = 0.65, тогда как в режиме luxo наилучших результатов можно добиться при blacker = 0.1. Существует общее правило — прибавлять blacker к пиксельным переменным, определяющим ширину перьев или толщину ножек. Поэтому буквы будут несколько темнее для тех устройств, которые обычно делают их слишком светлыми. Понятие пикселя для различных устройств может варьироваться, поскольку зачастую в их основе лежат разные физические принципы. Автору, например, довелось однажды работать с устройством с очень большой разрешающей способностью. Но, работая с определенным типом фотографической бумаги, это устройство слишком сильно сжимало вертикальные линии, поэтому для получения приемлемых результатов необходимо было устанавливать blacker = 4. А для другого устройства с высокой разрешающей способностью достаточно было задать значение blacker равным лишь 0.2. Для того чтобы подстроить шрифт под конкретное устройство вывода, нужно поэкспериментировать. Но то, что такие поправки вносить стоит, подтверждает богатый опыт автора. Если значение mode есть proof или smoke, то значение переменной blacker выбирается равным нулю, поскольку в этих режимах работы исказить вывод заведомо невозможно. ► Упражнение 11.1 Верно ли (в предположениях данной главы), что при ''mode — cheapo; mag = 10' и 'mode = luxo' мы получим один и тот же шрифт? fB строке 7 программы io.mf содержится команда 'define_correctecLpixels (о)'. Это еще один, третий по счету, способ перевода настоящих физических единиц измерения в соответствующие пиксельные величины. В соответствии с приложением В переменная о определяется присваиванием о := round(o# * hppp * о.correction) + eps, где о .correction, как и blacker, представляет собой "волшебное число", зависящее от устройства вывода, для которого разрабатывается шрифт. В случае устройства с высокой разрешающей способностью, такого, как luxo, значение множителя o-correction разумно выбирать равным 1, тогда как работая с устройствами с такой низкой разрешающей способностью, как у cheapo, автор получал более удовлетворительные результаты со значением o-correction = 0.4. Причина в том, что величина 'о' характеризует так называемый перелет, то есть число пикселей, на которое определенные элементы символов выходят за базовую или какую-либо другую линию, к которой они визуально привязаны. При высоком разрешении кривые выглядят лучше при наличии такого перелета, но в случае низкого
106 Глава 11: Увеличение и разрешение разрешения это не так. Поэтому, как правило, имеет смысл сокращать величину перелета, умножая ее на о.correction. В режимах proof и smoke этот множитель равен 1.0, поскольку они соответствуют высокому разрешению. /gK/gK Для моделирования свойств устройств вывода используется также параметр fillin, JL JL который показывает, насколько диагональные штрихи темнее вертикальных или горизонтальных. Назовем угловым пиксель, цвет которого совпадает с цветом пяти из восьми соседних, но не совпадает с цветом остальных трех, и среди этих трех есть один "сосед" по горизонтали, один "сосед" по вертикали и один диагональный "сосед", лежащий между ними. Если видимая затемненность белого углового пикселя равна /i, а видимая затемненность черного углового пикселя равна 1 — /г, то fillin = /i — /2. (У правильного растрового рисунка должно быть /i = /2 = 0, но в силу различных физических факторов пиксели часто оказывают влияние на своих "соседей".) /$К/$\ В используемой вами реализации системы METAFONT каждое из устройств вы- JL JL вода, для которых вы будете генерировать шрифты, должно быть представлено символическим именем переменной mode, представляющей условный режим работы. Имена режимов не являются стандартными элементами языка METAFONT. Например, не известно, существуют ли на самом деле режимы cheapo и luxo, которые мы обсуждаем в этой главе. Базовый формат METAFONT предполагает возможность дополнения новыми режимами работы; в конце приложения В описано, как это делается. /$K/gK При помощи определенной в базовом форматном файле процедуры 'mode.def' JL JL можно с легкостью создать новый условный режим работы. Например, режим luxo, о котором мы здесь говорили, можно определить следующим образом: mode_def luxo = pixels-per.inch := 2000; 7, высокое разрешение, почти 30 пике, на дюйм blacker := .1; 7, делать линии чуть более темными о.correction := 1; 7. сохранять полный перелет fillin := 0.1; 7. компенсировать затемнение углов proofing := 0; 7. нет, мы не создаем пробные оттиски fontmaking := 1; 7. да, мы создаем шрифт tracingtitles := 1; enddef; У, да, показывать заголовки Имя режима работы должно представлять собой одиночную символьную лексему. Разрешение указывается посредством присваивания переменной pixels-per-point определенного значения; значения других единиц измерения (pt, mm и т.п.) будут вычислены по этому значению программой mode.setup. Кроме того, задавая режим работы, необходимо присвоить значения внутренним переменным blacker, o-correction и fillin (которые описывают характеристики используемого устройства), а также переменным proofing, fontmaking и tracingtitles (от которых зависит количество выходных данных). Как правило, в режимах, предназначенных скорее для генерирования шрифтов, чем для их предварительной разработки, значения proofing и fontmaking устанавливаются равными, соответственно, 0 и 1. Значение tracingtitles обычно выбирается равным 0 для шрифтов с низким разрешением (которые генерируются быстро) и равным 1 — для шрифтов с высоким разрешением (генерирование которых происходит медленнее), так как в тех случаях, когда процесс работы довольно продолжителен, желательно иметь подробные комментарии происходящего. /£\/$К Кроме только что рассмотренных семи обязательных величин 'pxxels-per-inch\ ..., JL JL 'tracingtitles1, при задании режима работы может присваиваться определенное значение переменной laspect-ratio*. Если значение aspect-ratio не указывается, то на выходе предполагается получить шрифт, пиксели которого имеют квадратную форму. Но если, например, программа mode_def устанавливает aspect-ratio := 5/4, это значит, что выходные пиксели предполагаются неквадратными, с соотношением сторон 5 к 4; то есть 5 вертикальных пикселей равняются 4 горизонтальным пикселям. Пиксельные единицы
Глава 11: Увеличение и разрешение 107 измерения в базовом формате METAFONT выражаются в горизонтальных пикселях. Поэтому, если при разрешении 2000 пикселей на дюйм задано отношение сторон 5/4, то на один дюйм приходится 2500 вертикальных пикселей; т.е. один квадратный дюйм состоит из 2500 строк пикселей, каждая из которых содержит по 2000 пикселей. (Иначе говоря, каждый пиксель имеет ^о Дюима в ширину и ^^ю Дюима в высоту.) В таком случае система METAFONT базового формата установит значение переменной currenttransform таким, что все команды draw и fill будут растягивать кривые в 5/4 раза по вертикали. Это компенсирует неквадратную форму пикселей, так что разработчику шрифта необязательно помнить о том, что пиксели не квадратные. Рассмотрим конкретный пример, из которого станет ясно, как применять на практике идеи разработки шрифтов, не зависящих от особенностей устройств вывода. Мы изучим файл logo.mf, генерирующий семь букв логотипа системы METAFONT. Кроме этого файла, имеются еще параметрические файлы logo 10. mf, logo9.mf и т.д., которые при помощи logo.mf генерируют шрифты различного размера. Например, если бы на самом деле существовал режим работы luxo, то для гипотетического принтера luxo шрифт, содержащий логотип METAFONT' с символами размера 10 пунктов, можно было бы создать, запустив METAFONT с командной строкой вида \mode=luxo; input logo10 Основная задача файла logo 10.mf — установить "точные" значения специальных величин. После этого загружается файл logo.mf, который выполняет всю остальную работу. Вот как выглядит весь файл logo 10.mf: У, 10-пунктовый логотип системы METAF0NT font.size 10pt#; '/, номинальный размер шрифта ht#:=6pt#; У, высота символов xgap#:=0.6pt#; У, горизонтальная поправка u#:=4/9pt#; У, единичная ширина s#:=0; У, дополнительный отступ слева и справа o#:=l/9pt#; У, величина перелета px#:=2/3pt#; У, толщина пера по горизонтали input logo У, теперь создать шрифт end У, и остановиться. Файлы logo9.mf и logo8.mf похожи на logo 10.mf и генерируют, соответственно, логотипы ' METAFONT' размера 9 пунктов и ' metrfont' размера 8 пунктов. Причем по мере уменьшения размера буквы становятся несколько шире, а пространство между символами значительно увеличивается: У, 9-пунктовый логотип METAF0NT У, 8-пунктовый логотип METAF0NT font.size 9pt#; font.size 8pt#; ht#:=.9*6pt#; ht#:=.8*6pt#; xgap#:=.9*0.6pt#; xgap#:=.8*0.6pt#; u#:=.91*4/9pt#; u#:=.82*4/9pt#; s#:=.08pt#; s#:=.2pt#; o#:=l/10pt#; o#:=l/12pt#; px#:=.9*2/3pt#; px#:=.8*2/3pt#; input logo input logo end end
108 Глава 11: Увеличение и разрешение Интересно сравнить шрифт, генерируемый logolO.mf, со шрифтом, генерируемым logo8.mf при увеличении mag=10/8. Значения величин ht, хдар и рх у этих шрифтов одинаковы, если учитывать увеличение. Но увеличенный 8-пунктовый шрифт имеет несколько большее значение и и положительное значение 5, в результате чего ' METAFONT' превращается в ' МЕТА FONT'. f Каждый шрифт имеет так называемый номинальный размер {design size), т.е. размер, при котором данный шрифт смотрится наиболее гармонично. В системе TeX пользователь может увеличить шрифт двумя способами: указав 'at' и нужный размер либо указав 'scaled' и коэффициент увеличения (отношение нужного размера к номинальному, умноженное на 1000). Например, 8-пунктовый логотип METAFONT в системе TeX можно увеличить в 10/8 раза, указав в исходном файле 'logo8 at 10pt' либо 'logo8 scaled 1250'. Если увеличение задается первым способом, то есть указанием нужного размера, то оно вычисляется посредством деления нужного размера на номинальный. Задать номинальный размер шрифта в системе METAFONT базового формата можно посредством команды font_size, как показано выше. (Если номинальный размер не указан, то по умолчанию система METAFONT устанавливает его равным 128pt.) В файле logo.mf задаются три специальные единицы измерения, которые выражаются через параметры, установленные в параметрическом файле. Эти единицы затем используются в программах, описывающих отдельные символы. После того как они определены, logo.mf переводит их значения в пиксельные единицы: У, Программы для логотипа METAF0NT У, (logolO.mf - типичный параметрический файл) mode_setup; ygap#: = (ht#/13.5u#)*xgap#; У, вертикальная поправка leftstemloc#:=2.5u#+s#; У, положение левых ножек barheight#: = .45ht#; У, высота перемычек def ine_pixels(s,u,xgap,ygap,left stemloc,barheight); py#: = .9px#; define_blacker_pixels(px,py); '/, перьевые единицы pickup pencircle xscaled px yscaled py; logo_pen:=savepen; define_corrected_pixels(o); Здесь нет ничего нового, за исключением команды 'scwepen', которая, как мы увидим в главе 16, позволяет неоднократно использовать выбранное на данный момент перо в последующих программах. За описанными только что исходными определениями в файле logo.mf следуют программы для каждой из семи букв. В качестве примера рассмотрим программу для буквы 'Е', в которой используются величины u#, s#, ht#, leftstemloc, barheight, xgap и logo.pen: beginchar(''E",14u#+2s#,ht#,0); pickup logo.pen; xl=x2=x3=leftstemloc; x4=x6=w-xl+o; x5=x4-xgap; yl=y6; y2=y5; уЗ=у4; bot yl=0; top y3=h; y2=barheight; draw z6—zl—z3—z4; draw z2—z5; labels(l,2,3,4,5,6); endchar;
Глава 11: Увеличение и разрешение 109 Основные аспекты программ для букв МиТ мы рассматривали в главе 4; программы для остальных букв мы рассмотрим позже. ► Упражнение 11.2 Каждая из величин — ht#, хдар, t/#, s#, о# и рт#, определенных в параметрическом файле, влияет на букву 'Е', которая определена этой программой. Для каждой из этих величин скажите, что произойдет с 'Е', если эта единица станет немного больше, а другие при этом не изменятся? f^ Упражнение 11.3 Составьте программу для буквы 'F' (которая очень похожа на 'Е'). f^ Упражнение 11.4 Основываясь на информации из глады 4, но используя стиль приведенной выше программы для буквы 'Е', напишите полные программы для букв 'М' и 'Т'. Ширина символов должна быть равной, соответственно, 18u# + 2s# и 13ti# -f 2s#. ^Файл logo.mf содержит также следующие загадочные инструкции, благодаря которым пары букв 'ТА' и ТО' в наборе будут располагаться ближе друг к другу, чем позволяют их ограничивающие рамки: ligtable МТ": "A" kern -.5u#; ligtable "F": "0й kern -u#; Если бы этих поправок не было, то логотип 'METAFONT' имел бы вид 'METAFONT'. В расположение заглавных букв очень часто, особенно в логотипах, приходится вносить такие поправки. Система TeX вносит эти поправки автоматически, если только разработчик шрифта обеспечил ее необходимой информацией при помощи команд ligtable. /£\ Наконец, файл logo.mf содержит еще четыре команды, обеспечивающие дополни- JL тельную информацию о том, как следует располагать буквы шрифта в наборе: font_quad 18u#+2s#; font_normal_space 6u#+2s#; font_normal_stretch 3u#; font„normal.shrink 2u#; Величина font .quad — это единица измерения, которая в системе TeX называется 'em'. Параметры font.normaLspace (нормальный пробел), font_normal_stretch (нормальное растяжение) и font_normaLshrink (нормальное сжатие) определяют промежуток между словами при наборе данным шрифтом. На практике такой шрифт, как logo 10 используется в основном для набора одного единственного слова — 'METAFONT', однако параметры, отвечающие за промежутки между словами, включены специально на тот случай, если кому- то вздумается набрать предложение вроде: 'RN EFFETE TOMRTO OF MONTRNR OFTEN RTE NONFRT TOFFEE'. f После слов 'font_size', 'font_quad' и т.п. можно, хотя и не обязательно, ставить знак '=' или ':=', если вам кажется, что файл от этого будет выглядеть лучше. f Отметим, что в командах кернинга ligtable и в определениях таких аппаратно- независимых параметров, как font_size и font_quad, должны присутствовать только точные единицы измерения. Полные правила использования ligtable и других команд, посредством которых программы системы METAFONT могут передавать важную информацию издательской системе, например, TeX, обсуждаются в приложении F. Дописывание этой дополнительной информации к программе для системы METAFONT после того, как разработка шрифта уже закончена, напоминает составление предметного указателя для книги после того, как она уже написана и вычитана в гранках.
110 Глава 11: Увеличение и разрешение f<^>* Упражнение 11.5 JL Какое самое длинное английское слово можно напечатать шрифтом logo9? f Теперь, рассмотрев файл logo.mf, давайте перечислим все из чего он состоит, поскольку это даст нам пример полного описания шрифта (несмотря на то, что данный шрифт содержит всего семь букв). ■ Файл начинается с определения специальных единиц измерения и перевода их в пиксельные единицы, при помощи команд mode_setup, define.pixels и т.п. ■ Затем следуют программы для отдельных букв. (Им зачастую предшествуют макроопределения подпрограмм, которые встречаются далее несколько раз. Например, в дальнейшем мы увидим, что буквы 'R' и 'О' логотипа нарисованы с использованием подпрограммы, которая дает половинку деформированного эллипса; определение этого макроса находится в начальной части файла logo.mf, непосредственно перед программами для букв.) ■ Наконец, в нем содержатся специальные команды типа ligtable и font .quad, определяющие параметры шрифта, полезные при позиционировании символов. ■ Файл дополняется параметрическими файлами, в которых определяются специальные величины для различных вариантов шрифта. Мы могли бы написать множество параметрических файлов, при помощи которых создали бы множество различных (но взаимосвязанных) вариантов логотипа METAFONT; таким образом файл logo.mf определяет меташрифт в смысле главы 1. /$К ►Упражнение 11.6 JL Как обобщить программы из файла logo так, чтобы перемычка не обязательно находилась на высоте, составляющей 45% высоты символа? fB параметрических файлах logo 10.mf, logo9.mf и logo8.mf так же, как и в первых строчках файла io.mf из главы 5, вместо уравнений ('=') использовались присваивания (*:='). Это противоречит совету из главы 10, где мы говорили, что уравнения нужно использовать всегда, за исключением тех случаев, когда присваивания абсолютно необходимы. Просто автор взял себе за правило использовать присваивания для определения специальных единиц измерения, так как ему очень часто приходилось создавать экспериментальные файлы, в которых специальные величины неоднократно изменялись. Начиная разработку новой буквы, неплохо поэкспериментировать с различными установками параметров. Это легко сделать, копируя определения специальных параметров из параметрических файлов в пробный файл, если эти параметры были определены не уравнениями, а присваиваниями. /£\ Пользователям системы l^K удобно иметь наборы шрифтов, размеры которых JL возрастают в геометрической прогрессии. Шрифт называется масштабированным на шаг увеличения 1 ('magstep 1'), если он увеличен в 1.2 раза; шрифт масштабирован на шаг увеличения 2 ('magstер 2'), если он увеличен в 1.2 х 1.2 = 1.44 раза; шрифт масштабирован на шаг увеличения 3 ('magstep 3'), если он увеличен в 1.2 х 1.2 х 1.2 = 1.728 раза, и так далее. Таким образом, если при наборе документа используется шрифт, масштабированный на шаг увеличения 2, а весь документ при этом масштабирован на шаг увеличения 1, то фактически шрифт будет масштабирован на шаг увеличения 3. Благодаря свойству аддитивности шагов увеличения возрастает вероятность того, что шрифты нужных размеров существуют. Базовый формат METAFONT позволяет использовать конструкции типа \mode=cheapo; mag=magstep 2; input logo9 в случае, когда нужно генерировать 9-пунктовый логотип METAFONT для принтера cheapo, увеличенный в 1.44 раза (т.е., на шаг увеличения 2). Можно также указать 'magstep 0.5'; тогда ТЕХ выполнит команду '\magstephalf', которая увеличивает шрифт в \/Г2 раза.
Глава 11: Увеличение и разрешение 111 /$К/>ч В базовом формате METAFONT точные единицы измерения выражаются в так JL JL называемых типографских пунктах, так что pt# оказывается равным 1. Однако в программах этот факт лучше не использовать. Например, лучше писать *em# := Wpt#\ хотя указание 'р*#' в данной конструкции — лишнее, и без него компьютер будет работать на несколько микросекунд быстрее. )► Упражнение 11.7 Предположим, вы хотите имитировать работу принтера с низким разрешением на устрюйстве печати с высокой разрешающей способностью. Пусть, например, от устройства luxo ожидается получить такой выход, какой дает устройство cheapo, где каждому пикселю устройсва chepo соответствует квадрат размером 10 х 10 из пикселей устройства luxo. Объясните, как это можно сделать со шрифтом logo 10, внеся соответствующие изменения в файл logo.mf. Выходной файл должен называться cheaplogo!0.2000gf. Великому Соблазну должно быть противопоставлено высочайшее Разрешение. — УИЛЬЯМ БАРКИТТ, Пояснения к Новому Завету (с. 1700) Изобретенное одними другие увеличат. — ДЖОНАТАН СВИФТ, Дневник современной женщины (1729)
12 Рамки
Глава 12: Рамки 113 Давайте теперь подробно рассмотрим ограничивающие рамки, в которые заключены отдельные символы. В прошлом печатный набор представлял собой прямоугольную металлическую матрицу, собранную из множества отдельных элементов, которые имели одинаковую высоту, но могли отличаться по ширине. В наши дни технология печати сильно изменилась, но устаревшие понятия могут использоваться в метафорической форме. В издательских системах типа Т$£ каждый символ мысленно помещается в прямоугольную рамку (box), а слова набираются так, чтобы рамки плотно примыкали одна к другой. Основное отличие между старыми и новыми соглашениями состоит в том, что теперь элементы набора могут иметь не только разную ширину, но и высоту. Например, когда система TfTJX производит набор строки 'A line of type.', она стыкует рамки, и в результате получается примерно следующее: 'р Дкза сД &тиГ. (Буква 'А' ограничена рамкой *□', которая сидит непосредственно на базовой линии, а буква 'у' — рамкой '□', которая опускается ниже базовой линии.) Системе TfrjX нет абсолютно никакого дела до того, что именно находится в рамке; задача TeX — правильно состыковать и разместить рамки, принимая во внимание только их размеры. А уж какими должны быть размеры рамок и символы в этих рамках — решать разработчику шрифта. Рамки — объекты двумерные, но мы приписываем им три измерения, поскольку вертикальная составляющая поделена на две части — высоту (над базовой линией) и глубину (под базовой линией). Размер рамки по горизонтали, конечно же, называется шириной. На приведенном ниже рисунке, изображена типичная рамка, и показаны ее так называемые точка отсчета и базовая линия: Точка отсчета «— ширина —> Все символы, которые использовались в качестве примеров в предыдущих главах, имели нулевую глубину, но очень скоро мы рассмотрим пример, где и высота, и глубина будут иметь ненулевые значения. Символ не обязательно должен находиться строго в границах своей рамки. Например, курсивные и наклонные буквы помещаются в обычные рамки, как если бы они не были наклонены, поэтому часто выступают за правые границы своих рамок. Сравним, к примеру, букву 'g' шрифта cmrlO, которым был набран текст оригинала этой книги, с буквой 'g' соответствующего наклонного шрифта (cmsllO): На рисунке наклонная 'g' изображена так, будто ее рамку перекосило: верх сдвинут вправо, низ влево, а базовая линия осталась на прежнем месте. Но в обоих случаях система TeX считает, что рамка имеет 5 pt в ширину, 4.3055 pt в высоту и 1.9444 pt в глубину. Хотя рамки наклонных букв выпрямлены, системе удается их правильно разместить благодаря тому, что они согласованы на уровне базовой линии.
114 Глава 12: Рамки fy рамок имеется еще один, четвертый параметр, который называется курсивной поправкой. В курсивной поправке содержится дополнительная информация для системы TJtjX о том, выходит ли буква за правую границу рамки. Например, курсивная поправка обычной буквы 'g' шрифта cmrlO равна 0.1389 pt, а для соответствующей наклонной буквы шрифта cmsllO курсивная поправка равна 0.8565 pt. Курсивная поправка прибавляется к ширине рамки при наборе таких математических формул, как g2 и g2 и в некоторых других случаях, которые описаны в книге Все про TeX. Определенная в базовом формате METAFONT команда beginchar устанавливает ширину, высоту и глубину рамки. Эти три параметра должны выражаться через точные величины, которые не меняются в зависимости от увеличения и разрешения, поскольку размер рамки символа никоим образом не должен зависеть от устройства, при помощи которого этот символ выводится. Очень важно иметь возможность определять документы так, чтобы они не изменялись, несмотря на то, что технология их печати будет постоянно совершенствоваться. При помощи METAFONT можно генерировать шрифты для новых устройств, определяя для них новые режимы работы, как описано в главе 11, но и в этих новых шрифтах каждый символ будет заключен в рамку с прежними параметрами. Таким образом, чтобы вывести на экран или распечатать при помощи нового оборудования выходные dvi- файлы системы TeX, их не придется как-либо изменять. Три параметра рамки задаются в команде beginchar в обратном алфавитном порядке: сначала задается ширина (width), затем высота (height) и, наконец, глубина (depth). Программа beginchar переводит значения этих величин в пиксели и присваивает их переменным iu, h и d. Кроме того, они округляются до ближайшего целого числа пикселей, так что значения to, h и d — всегда целые. Пиксели системы METAFONT — как квадратики на листе миллиметровой бумаги, границы которых лежат на линиях с целыми координатами. Левый край рамки лежит на линии х = 0, а правый край — на линии х = w\ на верхнем крае мы имеем у = /i, а на нижнем — у = —d. В каждой из строк содержится w пикселей, а в каждом столбце содержится /i-frf пикселей. Таким образом, всего в каждой рамке содержится wh + wd пикселей. Поскольку значения iu, h и d целочисленны, то они, вероятнее всего, не будут в точности совпадать со значениями параметров рамки, которыми оперирует аппаратно-независимая издательская система типа TeX. К примеру, для одних символов ширина будет чуть больше, для других — чуть меньше целого числа пикселей. Однако все же можно получать удовлетворительные результаты, если, стыкуя рамки, использовать их значения и>, а накопившуюся ошибку округления компенсировать за счет пробелов между словами, так, чтобы положение рамки не сильно смещалось по отношению к ее настоящему аппаратно-независимому положению. Разработчик шрифта должен стремиться к тому, чтобы разрабатываемые им символы хорошо сочетались при стыковке рамок целочисленной ширины. /$\/^\ Вам может не понравиться значение переменной iu, которое вычислит программа JL JL beginchar посредством округления аппаратно-независимой ширины до ближайшего целого числа пикселей. Например, вам захочется сделать букву 'т' на один пиксель шире при определенном разрешении, так, чтобы все ее ножки отстояли от соседних на одинаковое расстояние или чтобы она лучше сочеталась с буквой 'п'. В таком случае вы можете присвоить переменной w новое значение в любом месте между beginchar и end char. Это новое значение никак не повлияет на аппаратно-независимое значение ширины рамки, которым оперирует система TeX, но оно будет учитываться программным обеспечением при обработке dvi-файлов, в которых используется ваш шрифт.
Глава 12: Рамки 115 Вот пример символа с ненулевыми шириной, высотой и глубиной. Это левая круглая скобка из шрифта семейства Computer Modern, например cmrlO. Шрифты семейства Computer Modern генерируются программами, включающими множество параметров, поэтому этот пример иллюстрирует еще и принципы метадизайна. При помощи одной этой программы можно изобразить много разных левых скобок. Но прежде чем углубляться в детали определения метаскобки, давайте обратим внимание на то, как относительно просто задаются и используются размеры ее рамки. (0,Л) {w,h) "Left parenthesis"; numeric ht*, dp*; ht* = body.height*1', .5[ht*, -dp*] = axis*; beginchar("(",7tt#,/i*#, dp*); italcorr ht* * slant — .bu*; pickup fine.nib; penposl (hair — fine, 0); penpos2(.75[thin, thick] — fine, 0); penpos3(hair — /me, 0); rt xir = rt xzr = w — u; Ift X21 = xi — 4u; topyi = h; 2/2 = .5[у1,уз] = оайз\ filldraw zu{(z2i — zu) xscaled3} ... z<ii • • • {(*з/ - z2i) xscaled3}z3/ --Z3r{(z2r - 23r)xscaled3} •• -^2r ... {(zir - Z2r)xscaled3}zir --cycle; penlabels(l,2,3); endchar; (0,-d) (щ-d) Ширина этой левой скобки равна 7и*, где и* — специальный параметр, фигурирующий в определениях ширины всех символов семейства Computer Modern. Высота и глубина подобраны так, чтобы верхний и нижний края ограничивающей рамки находились на одинаковом расстоянии от воображаемой линии, которая называется осью. Эта линия играет важную роль при подготовке к печати математических текстов. (Например, при наборе таких дробей, как | система TeX помещает черту дроби на уровне оси; многие символы, например *+' и '—', как и скобки, размещаются так, чтобы их центр лежал на оси.) В случае нашей программы, мы задаем положение оси при помощи уравнения i.b[ht*, — dp*] = axis*\ тем самым помещая ее посередине между верхним и нижним краями. Кроме того, посредством уравнения lht* = body-height*' мы помещаем верхний край на высоте body-height*, которая равна максимальной высоте символа в шрифте. Для шрифта cmrlO величина body-height* равна 7.5pt*, a axis* = 2.5pt*, поэтому dp* = 2.bpt*, и высота скобки равна 10 pt. В приведенной выше программе для '(' используется команда filldraw, которая ранее не встречалась. В ее основе лежит сочетание двух команд — fill и draw, причем для рисования используется выбранное на данный момент перо. В одних шрифтах семейства Computer Modern символы имеют гладкие края, в других — ребристые. Это зависит от того, какое перо использовалось в команде filldraw. В нашем случае перо представляет собой круг с диаметром, равным fine. Если значение fine достаточно велико, то символы, полученные при помощи команды filldraw, будут иметь закругленные углы; если же fine = 0 (как, например, в cmrlO), то углы будут острыми.
116 Глава 12: Рамки Утверждение 'penpos^hair — fine,Oy делает ширину имитируемого пера с широким кончиком в положении 1 равной hair—fine] то есть, расстояние между zu и z\r будет равным hair —fine. Область между z\\ и z\r мы будем заливать при помощи пера с круглым кончиком, диаметр которого равен fine; центр этого кончика будет пробегать по прямой от точки гц до точки z\r. Следовательно, такое перо добавляет \fine к толщине изображаемой линии с каждой ее стороны. Таким образом, общая толщина линии в положении 1 будет равна \fine + (hair — fine) + \fine — hair. (В шрифтах семейства Computer Modern используется специальный параметр hair, который определяет толщину тончайших линий.) Точно так же, утверждение ipenpos2(-7b[thin, thick] — fine,®)1 делает общую ширину пера в положении 2 равной .75[thin, thick], что составляет | от разности двух других параметров, определяющих толщину линий в программах Computer Modern. Если при неизменных значениях параметров hair, thin и thick мы станем увеличивать значение fine, то в результате получим просто чуть более округлые углы в положениях 1 и 3, в остальном же начертание не изменится, если только значение fine не превысит значение hair. Вот, например, пять различных левых скобок, нарисованных с помощью нашей программы при различных установках параметров: cmrlO cmbxlO cmvttlO cmssdclO cmtilO u= 20 ht = 270 axis = 90 fine = 0 hair = 8 thin = 9 thick = 25 u= 23 ht = 270 axis = 90 fine = 0 hair = 13 thin = 17 thick =41 u= 21 ht = 250 axis = 110 fine = 22 hair = 22 thin — 25 thick = 25 u= 19 ht = 270 axis = 95 fine — 8 hair = 23 thin = 40 thick = 40 и = 18.4 ht = 270 axis = 90 fine — 7 hair = 8 thin = 11 thick = 23 Значения параметров выражены в пикселях и соответствуют режиму ргоо/ с разрешением 36 пикселей на пункт. (Таким образом, к примеру, значение и# в шрифте cmrlO равно ЩрЬ#.) Само имя шрифта cmbxlO, которое расшифровывается как полужирный растянутый (bold extended), говорит о том, что для него единичная ширина и немного больше, чем для cmrlO, а ширина пера — намного больше. У
Глава 12: Рамки 117 шрифта cmvttlO с переменной шириной символов, имитирующего стиль печатной машинки {variable-width typewriter), символы имеют гладкие края, а линии — почти постоянную толщину, поскольку значения параметров fine и hair лишь немного меньше значений thick и thin. Кроме того, у этого шрифта ось расположена несколько выше, а высота символов несколько меньше. Рубленный жирный сжатый шрифт cmssdclO (sans serif demibold condensed), похожий на тот, что используется в названиях глав этой книги, занимает промежуточное положение — для него thick = thin, но минимальная толщина линий значительно меньше, а значение fine обеспечивает небольшое закругление на углах. У курсивного шрифта cmtilO (text italic) концы закругленные, а символы имеют наклон 0.25; это значит, что при заливке командой filldraw каждая точка (х, у) смещается в положение (х + .2Ъу,у). f Вертикальная черта непосредственно справа от курсивной левой скобки показывает величину курсивной поправки данного символа, то есть четвертый параметр рамки, о котором упоминалось ранее. Эта величина была задана в нашей программе утверждением 'italcorr ht# * slant — .Ьи#\ где slant — параметр семейства Computer Modern, который равен нулю для всех ненаклонных шрифтов, но в случае шрифта cmtilO slant = .25. Выражение, следующее за italcorr, должно выражаться в точных единицах. Если значение этого выражения отрицательно, то курсивная поправка будет равна нулю; в противном случае она будет равна указанному значению. f Автору удалось получить вполне удовлетворительные результаты, придавая курсивной поправке значение, примерно равное .Ъи плюс максимальное расстояние, на которое символ выступает вправо за пределы своей рамки. Так, в нашем примере с левой скобкой ее правый верхний конец до наклона находится в точке (w — и, ht), поэтому после наклона его координата х будет равна w — и + ht * slant; это будет крайняя правая точка символа, если только slant > 0. Прибавив .Ъи, вычтя w и переписав это выражение в точных единицах, мы получим указанную в программе формулу. Заметим, что при slant = 0 выражение сокращается до 'italcorr — .5u#'; это значит, что ненаклонные левые скобки будут иметь нулевые курсивные поправки. f* Упражнение 12.1 Напишите программу для правой скобки, парной для рассмотренной левой. Читатель должен помнить, что соглашения базового формата METAFONT и соглашения, принятые для разработки шрифтов Computer Modern, не встроены в METAFONT; это всего лишь примеры того, как можно использовать систему. Для разработки других шрифтов могут быть с большим успехом использованы совершенно иные подходы. В нашей программе для левой скобки использованы соглашения: beginchar, endchar, italcorr, penlabels, pickup, penpos, Ift, rt, top, z и filldraw; все они несколько произвольным образом определяются в приложении В, как часть базового формата. Кроме того, в программе используются величины и, body-height, axis, fine, hair, thin, thick и slant; все они являются произвольными параметрами, которые автор решил ввести в свои программы для шрифтов Computer Modern. Поняв, как использовать подобные произвольные соглашения, вы сможете модифицировать их для собственных целей. ► Упражнение 12.2 (Для знатоков системы TeX.) Очевидно, что ширина ограничивающей рамки играет важную роль при наборе. А как TeX использует высоту и глубину рамки? Примитивные команды, посредством которых METAFONT получает информацию о размерах каждой рамки, очень редко используются непосредственно, поскольку
118 Глава 12: Рамки они изначально задуманы как составляющие таких команд высокого уровня, как beginchar и italcorr. Но если вы непременно хотите знать, что же происходит на низком уровне, то откроем вам секрет: существуют четыре внутренние величины charwd, charht, chardp и charic, значения которых во время выполнения команды shipout принимаются за размеры рамки выводимого символа, выраженные в типографских пунктах. (Примерами того, как можно манипулировать этими величинами, служат определения команд beginchar и italcorr, которые вы найдете в приложении В.) Кроме команды charwd и ее "сестричек", в системе METAFONT имеются еще четыре внутренние переменные, значения которых записываются в выходной файл при выполнении команды shipout. ■ charcode округляется до ближайшего целого числа, а затем переводится в число, заключенное между 0 и 255, прибавлением или вычитанием, если это необходимо, числа, кратного 256. Этот так называемый "с-код" обозначает позицию символа в шрифте. ■ charext округляется до ближайшего целого числа; полученное в результате число представляет собой вторичный код, при помощи которого можно различать два или более символов с одинаковым с-кодом. (Система TeX игнорирует charext, полагая, что любой шрифт может содержать не более 256 символов, но модификации этой системы для восточных языков при помощи charext имеют возможность работать со шрифтами, в которых символов гораздо больше.) ■ chardx и chardy представляют, соответственно, горизонтальный и вертикальный сдвиг, выраженный в пикселях. (В одних издательских системах используются обе эти аппаратно-независимые величины при переходе в следующее положение на странице из текущего, который происходит сразу после набора каждого символа. В других же, например асоциированном с TeX программном обеспечении dvi, полагается chardy = 0, но при этом chardx используется в качестве величины горизонтального сдвига, если только перемещение по горизонтали на chardx не вызывает слишком большого отрыва следующей позиции от той, которая получена сложением величин charwd и является аппаратно-независимой. Программа endchar, определенная в базовом формате METAFONT, не меняет значения chardy = 0, но устанавливает chardx := w непосредственно перед тем, как отправить символ на вывод. Этим объясняется обсуждавшийся нами ранее факт, что изменение w влияет на ширину пробела между смежными символами.) Два символа с одинаковым с-кодом должны иметь одинаковые размеры рамок и одинаковые сдвиги; в противном случае параметры второго символа аннулируют параметры первого. Для того чтобы определить, отправлялся ли ранее на вывод символ с определенным с-кодом, можно использовать булево выражение 'charexists с'. В заключение этой главы давайте рассмотрим программу, которая генерирует знак опасного поворота, раз уж этот символ так часто встречается в этой книге. Это специально изготовленный символ, который по замыслу должен располагаться в начале абзацев, в которых базовые линии отстоят друг от друга на 11 pt. Поэтому его нижняя граница лежит на 11 pt ниже его собственной базовой линии. Однако помещается он в рамку с нулевой глубиной, так как иначе система TeX подумала бы, что первая строка абзаца содержит очень "глубокий" символ, и сдвинула бы вторую строку вниз. baselinedistance# := 11р£#; define_pixels(6ase/inee/ts£ance); heavyline# := 50/36р£#; define_blacker_pixels(/ieavy/tne); beginchar (127, 25u#, h_height# + border#,0); "Dangerous bend symbol"; pickup pencircle scaled rulethickness; top yi = ||/i; Iftxi = 0; Xi + Xi = Xia + Xib = £46 + ^2o = #4 + #2 = ^4a + #26 = #36 + #3a = #3 + #3 = Щ X4a = X4b = XA + Щ X36 = #lo = Xl — 2tL] t/4 + 2/4 = 2/4a + 2/46 = У36 + 2/la = 2/3 + t/l = 2/3a + 2/l6 = 2/26 + 2/2a =2/2+2/2=0; 2/la = 2/16 = 2/1 - Yjh\ t/46 = 2/2a = 2/4 + ^fh\ ФФ &
Глава 12: Рамки 119 draw Zia . . Z\ . . Z\b Z2a • • ^2 • • Z2b zza .. zz .. zzb — ZAa • • z\ .. z^b — cycle; У, граница щита SlO = #11 = £12 = Xiz = .bw — U] X14 = X15 = Xi6 = X17 = W — £10; J/10 = 2/14 = рЛ; 6o* t/13 = — baselinedistance; z\\ = (zio .. z\z) intersectionpoint {z\a{zia — ZAb) -. 2i{ri(?M}); J/i5 = 2/u; 2/16 = У12 = —У11; 2/17 = 2/20 = 2/21 = У13; draw zn -- zio -- 214 -- 215; draw 212 -- 213; draw ziq -- zn\ % столбик Яго = w — X21; X21 — X20 = 16it; draw Z20 -- 221; 7. нижняя часть язе = w — X3i; хзв — X3i = 8u; Х32 = язз = язв; #31 = хз4 = Х35; У31 = -узе = ЩН\ узг = ~У35 = jfh; узз = ~У34 = ^Л; pickup pencircle scaled heavyline-, draw 232(232 — 231} • • 233 — 234 .. 235(236 — 235}; 7. опасный поворот pickup penrazor xscaled heavyline rotated (angle(232 — 231) + 90); draw 231 -- 232; draw 235 -- 236; 7. верхняя и нижняя линии labels (la, 16, 2a, 26,3a, 36,4a, 46, range 1 thru 36); endchar; Эта программа имеет ряд замечательных особенностей. (1) Первый параметр в beginchar — не цепочка, а число 127; этим мы помещаем символ в позицию шрифта номер 127. (2) Цепочку уравнений, например, 'а = w — 6; а = w — Ь'\ можно условно сократить до 'a -f 6 = а' 4- 6' = w\ (3) Связка '—' представляют собой условное обозначение линии с "бесконечным" натяжением, т.е. почти прямую линию, которая плавно переходит в соседние с ней кривые. (4) Оператор 'intersectionpoint' определяет точку пересечения двух путей; подробнее мы изучим ее в главе 14. Ну вот, мы снова в тех же рамках. — РАЙДЕР ХАГГАРД, Рассвет (1884) Рассказ также можно поместить в рамку. ДОРОТИ КОЛБОРН, Номенклатура газет (1927)
13 Рисуем, заливаем и стираем
Глава 13: Рисуем, заливаем и стираем 121 Рисунки, генерируемые системой METAFONT, состоят из крошечных пикселей, каждый из которых находится во "включенном" или "выключенном" состоянии. Поэтому можно представить, что компьютер работает с невидимой глазу "миллиметровой бумагой" и закрашивает на ней отдельные клетки, когда мы просим его нарисовать линию или залить область. На самом деле эта встроенная миллиметровка системы METAFONT устроена чуть сложнее. В процессе работы над рисунком пиксели могут быть, например, "дважды включены" или "трижды выключены". Каждому пикселю соответствует небольшое целочисленное значение, и при окончательном выводе изображения символа черными будут те пиксели, значения которых больше нуля. Например, две команды fill (0,3) - - (9,3) - - (9,6) - - (0,6) - - cycle; fill (3,0) - - (3,9) - - (6,9) - - (6,0) - - cycle дают следующий массив пикселей размера 9x9: 0001 1 1000 0001 1 юоо 0001 1 юоо 111222111 1 1 12221 1 1 1 1 12221 1 1 0001 11000 0001 1 1000 0001 1 1000 Дважды залитым пикселям соответствует значение 2. При применении команды fill к простой области, значения всех принадлежащих ей пикселей увеличиваются на 1. В случае же применения команды unfill все они уменьшаются на 1. Поэтому, команда unfill (1,4) -- (8,4) -- (8,5) -- (1,5) --cycle изменит приведенный выше массив, превратив его в 0001 1 юоо 0001 1 юоо 0001 1 юоо 111222111 100111001 111222111 0001 1 1000 000111000 0001 1 1000 Пиксели в центре не были стерты (т.е. при выводе рисунка будут черными), так как их значения по-прежнему положительны. Между прочим, из данного примера видно, что пиксели в METAFONT отделены друг от друга линиями с целыми координатами точно так же, как квадратики на листе миллиметровки. К примеру, нижний левый V в приведенном выше массиве чисел размера 9x9 соответствует пикселю с границей вида '(0,0) -- (1,0) -- (1,1) -- (0,1) -- cycle'. Координаты (х,у) внутренних точек этого пикселя лежат в промежутке от 0 до 1. ► Упражнение 13.1 Каковы координаты (х, у) четырех угловых точек центрального пикселя в рассматриваемом массиве размера 9x9? ► Упражнение 13.2 Какая картинка получилась бы, если бы в приведенных выше примерах команда unfill была применена перед командой fill?
122 Глава 13: Рисуем, заливаем и стираем ► Упражнение 13.3 Подберите такую команду unfill, которая, будучи применена после рассмотренных выше команд fill и unfill, давала бы следующий массив пикселей: 0001 1 1000 000101000 000101000 111212111 100101001 111212111 000101000 000101000 000111000 "Простой" называется такая область, граница которой не имеет самопересечений; в случае, когда линии границы пересекаются, проявляются более сложные эффекты. Например, результатом команды fill (0,1) -(9,1) -(9,4)- (4,4)- (4,0) - (6,0) - (6,3) - (8,3) - (8,2) - (0,2) -cycle будет следующий массив пикселей: 00001 1111 00001 1001 111122111 000011000 Как видим, некоторые пиксели получили значение 2, поскольку оказались залитыми дважды. Кроме того, имеется "дыра", в которой значения пикселей остались равными нулю, несмотря на то, что они окружены залитыми пикселями; эти пиксели считаются не принадлежащими области, тогда как дважды залитые пиксели считаются принадлежащими области дважды. ► Упражнение 13.4 Покажите, что первый массив 9 х 9 с пересечением, приведенный на предыдущей странице, можно получить при помощи одной команды fill. (Значения девяти центральных пикселей должны равняться 2 — так, как если бы заливались две отдельные области, хотя вы примените команду fill всего один раз.) ► Упражнение 13.5 Как вы думаете, каким будет результат команды 'fill (0,0) -- (1,0) -- (1,1) -- (0,1) -- (0,0)-(1,0)--(1,1)-(0,1)-cycle'? Команда fill может давать еще более странные эффекты, когда граничные линии пересекаются только в одном месте. Например, если вы укажете fill (0,2) - (4,2) - (4,4) - (2,4) - (2,0) - (0,0) -cycle, то МЕТА FONT выдаст следующий массив пикселей размера 4x4: 001 1 001 1 --оо --оо (Здесь символ '-' означает число —1.) Более того, машина сообщит вам, что обнаружен "необычный путь" с числом обходов, равным нулю! Что бы это значило? Говоря по-простому, это значит, что путь зацикливается сам на себе наподобие цифры 8, вследствие чего система METAFONT дает сбой, когда, следуя обычным правилам, пытается определить "внутреннюю" и "внешнюю" области кривой. f Любой циклический путь имеет так называемое число обходов, которое можно понимать следующим образом. Представьте себе, что вы ведете вдоль этого пути
Глава 13: Рисуем, заливаем и стираем 123 автомобиль и держите в руке цифровой компас, который подсказывает вам направление движения. Например, если путь имеет вид (0,0) - - (2,0) - (2,2) - (0,2) - cycle, то вы начинаете движение в направлении 0°, а затем четыре раза поворачиваете влево. После первого поворота компас покажет направление 90°, после второго — 180°, а после третьего — 270°. (Показания компаса увеличиваются при повороте влево и уменьшаются при повороте вправо; поэтому, теперь компас показывает 270°, а не —90°.) В конце этого циклического пути компас покажет 360°, а если вы совершите еще один обход, он покажет 720°. Точно так же, если вы обошли путь (0,0)-(0,2)-(2,2)-(2,0)-cycle (который совпадает с прежним, но обход совершается в противоположном направлении), то в начале пути ваш компас будет показывать 90°, а в конце 90°; в этом случае при каждом обходе показания компаса будут уменьшаться на 360°. Понятно, что, объехав несколько раз по кругу любой циклический путь, мы изменим показания компаса на значение, кратное 360°, поскольку направления в конечной и начальной точках будут совпадать. Число обходов по определению равняется £, если при обходе пути показания компаса меняются на t раз по 360°. Таким образом, для двух рассмотренных нами циклических путей числа обходов равны, соответственно, 1 и — 1; а "необычный путь" с предыдущей страницы, который дает как положительные, так и отрицательные пиксели, на самом деле имеет нулевое число обходов. /$\ Если заливаемый циклический путь имеет положительное число обходов, то при JL выполнении команды fill происходит следующее. Сначала, если это необходимо, путь "оцифровывается" так, чтобы в результате он целиком лежал на границах пикселей; иными словами, путь слегка искажается так, чтобы он принадлежал линиям, разделяющим пиксели на миллиметровке. (В примерах, которые мы до сих пор рассматривали в этой главе, в такой поправке не было необходимости.) Затем значение каждого пикселя увеличивается на j и уменьшается на fc, если горизонтальный луч, направленный влево от данного пикселя, j раз пересекает те участки нашего оцифрованного пути, обход которых совершается сверху вниз, и к раз — те его участки, обход которых совершается снизу вверх. К примеру, рассмотрим более детально путь с предыдущей страницы, который имеет самопересечение и содержит "дыру": a a a а^ а а а а' fe е е е* а а а а' Ъ Ъ ъ ъ. 7 /• ъ ъ. Ь Ь 61 с c\di 9 9 9+ ./i h h У пикселей, отмеченных буквой d, слева имеется j = 2 нисходящих и к = 1 восходящих участков пути, поэтому их значение возрастает в общей сложности на j — к = 1; точно так же обстоит дело с пикселями, отмеченными буквой д. Для пикселей, отмеченных буквой с, j = к = 1, поэтому они попадают в незалитую "дыру"; для пикселей, отмеченных буквой /, J = 2, a fc = 0, поэтому они заливаются дважды. Это правило применимо, поскольку, интуитивно понятно, что, если путь имеет положительное число обходов, то его внутренняя область должна лежать слева от него. f ►Упражнение 13.6 Верно ли, что в случае, когда циклический путь имеет положительное число обходов, команда fill увеличивает значение каждого пикселя на / —тп, если горизонтальный луч, проведенный от данного пикселя вправо, / раз пересекает участки оцифрованного пути, обход которых совершается снизу вверх, и m раз пересекает те его участки, обход которых совершается сверху вниз? (Например, для пикселей, отмеченных буквой е, имеем / = 2 и га = 1; для пикселей с имеем / = m = 1.)
124 Глава 13: Рисуем, заливаем и стираем fTo же правило действует и в случае, когда число обходов отрицательно, с той лишь разницей, что значения пикселей уменьшаются на j и увеличиваются на к. В этом случае внутренняя область лежит справа от пути. fEcjin же число обходов равно нулю, то внутренняя область может лежать как справа, так и слева. В этом случае METAFONT пользуется правилом для положительного числа обходов и выдает сообщение, что путь "необычный". Сообщения об ошибке можно избежать, установив 'turningcheck := 0'; тогда при заливке всегда будет использоваться первое правило, даже если число обходов отрицательно. Команда draw, определенная в базовом формате METAFONT, имеет два важных отличия от команды fill. Во-первых, в ней используется выбранное на данный момент перо, за счет чего путь становится "толще". Во-вторых, путь может быть и не циклическим. Существует еще и третье отличие, которое нужно отметить, хотя оно и не так важно. Дело в том, что команда draw может увеличить значения определенных пикселей более чем на 1, даже в случае, когда изображаемая фигура довольно проста. Например, массив пикселей )00 оооооооооооооо оооо"- — 0000 000 000 001 001 001 о 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 01 1 0000 1 1222 11112 11110 11110 11100 11100 11100 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 1 1000 0000000000 0000 22 1 1 1111 1111 1111 0111 0111 0111 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 001 1 0000 000000000 000000 ■ 0000 000 000 100 100 100 1 10 1 10 1 10 1 10 110 1 10 110 1 10 1 10 1 10 1 10 1 1 о 1 10 1 10 1 10 1 10 1 10 000000000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0001 001 1 001 1 001 1 001 1 001 1 001 1 0001 0001 0000 0000 0000 0000 0000000 0000000 0000000 0000000 00000 0000 000 00 о о 1 1 1 1 1 1 1 1 1 о 000 0000000 00000000 000001 1 1 — '111 111 111 111 111 111 111 111 111 111 111 111 12 1 111 1 20 1 10 100 100 000 0000 000000 00000000 0000 — 1 1 1 1 1 1 1 1 1 0000000000000000 1000000000000000 1 1 1 1 1 1 1 1 1 1 1 1 1 о 00 0000 000000000000 0000000000 00000000 0000000 "000000 00000 0000 0000 1000 1000 1 100 1 100 1110 1110 1110 1110 1110 1110 1 100 1 100 1000 0000 000000 2 .2 22 21 22221 0011: 0001 ] 0001 ] 00001 00001 оооос оооос оооос 00000000000000000000 получен с помощью двух команд draw. Фигура слева получена посредством команд pickup penrazor scaled 10; % перо шириной 10 и высотой О draw (6,1){«р} .. (13.5,25) .. {down}{2l, 1). Нетрудно догадаться, каким образом некоторые из верхних пикселей в этом массиве приобрели значение 2. Действительно, если бы мы провели вдоль данного пути тончайшим пером, то по этим пикселям перо прошлось бы дважды. Но фигуру справа, полученную при помощи команд pickup pencircle scaled 16; draw (41,9) .. (51,17) .. (61,9) пояснить не так просто. Казалось бы, откуда в этом случае взяться в массиве двойкам? Работа системы METAFONT с толстыми перьями представляет собой довольно сложный процесс, который мы сейчас не будем рассматривать. Давайте пока считать, что это такой необычный процесс, при котором время от времени происходит "разбрызгивание" и капли краски попадают на участки, не подлежащие заливке. При этом иногда значения пикселей оказываются равными 3 и более. Но если не обращать внимания на эти аномалии и просто рассматривать множество пикселей, получивших положительные значения, мы увидим вполне обычную фигуру. Пример с левой скобкой из главы 12 иллюстрирует действие filldraw — команды, которая требует цикличности пути (как fill) и использует выбранное на данный момент перо (как draw). Значения пикселей увеличиваются в области,
Глава 13: Рисуем, заливаем и стираем 125 которая получилась бы, если бы вы нарисовали заданный путь выбранным на данный момент пером, а затем залили область, ограниченную полученным контуром. Значения некоторых пикселей из этой области могут увеличиться на 2 и более. Число обходов заданного пути должно быть отличным от нуля. Кроме команд fill, draw и filldraw, существует еще команда drawdot, упоминавшаяся в начале главы 5. В ней вы должны указать только одну точку; значения пикселей в окрестности этой точки увеличиваются на 1 посредством выбранного на данный момент пера. В главе 24 объясняется, почему при этом результат лучше, чем при рисовании пути, состоящего из одной точки. /$\ Имеется также команда undraw, аналогичная команде unfill; она уменьшает JL значения пикселей на то же число единиц, на которое команда draw их увеличила бы. Кроме того, как вы могли догадаться, имеются команды unfilldraw и undrawdot, обратные к командам filldraw и drawdot, соответственно. fEtfiH вы попытаетесь использовать undraw и/или unfill совместно с draw и/или fill, то очень скоро обнаружите, что вам чего-то не хватает. В базовом формате METAFONT определена команда cullit, которая заменяет все отрицательные значения пикселей нулем, а все положительные — единицей. Эта операция "подравнивания" позволяет стирать ненужные части рисунка невзирая на причуды команд draw и undraw и то, что при наложении областей возможна их двойная заливка. /£\ Команда 'erase fill с' представляет собой краткую форму записи последователь- JL ности команд 'cullit; unfill с; cullit'; она обнуляет значения пикселей, которые находятся внутри циклического пути с, а значения остальных пикселей устанавливает равными 1, если до этого они имели положительные значения. (Первая команда cullit устанавливает все значения равными 0 или 1, а следующая за ней команда unfill заменяет значения пикселей внутри с на 0 или отрицательное значение. Наконец, команда cullit избавляет нас от отрицательных значений, которые могли бы в дальнейшем исказить результаты заливки и рисования.) Вместе с erase можно также использовать команды draw, filldraw и drawdot; например, 'erase draw р' является краткой формой записи последовательности команд 'cullit, undraw р; cullit' в которой выбранное в данный момент перо используется как стирающий ластик, применяемый к пути р. fKy6 на рисунке справа наглядно иллюстрирует один из эффектов, которых несложно добиться при помощи стирания. Вначале определяются восемь точек и рисуется "задний" квадрат; затем две стороны "переднего" квадрата стираются более широким пером; наконец, при помощи обычного пера рисуются все остальные линии: s# := bpt#\ define_pixels(s); */, сторона квадрата zi =(0,0); z2 = (s,0); z3 = (0, s); zA = (s, s); for A; = 1 upto 4. Zk+4 = Zk + (|s, ^s); endfor pickup pencircle scaled Apt; draw 25 - - zq - - z& - - 27 - - cycle; pickup pencircle scaled 1.6pt; erase draw Z2 -- 24 -- 23; pickup pencircle scaled Apt] draw z\ - - zi - - z\ - - 23 --cycle; for к = 1 upto 4: draw Zk -- Zk+4] endfor. Вот как выглядит этот куб в номинальном размере: 'dP'. f* У пражнение 13.7 Модифицируйте приведенную выше конструкцию с операциями рисования и стирания так, чтобы получить невозможный куб вида 'dP\
126 Глава 13: Рисуем, заливаем и стираем f> У пражнение 13.8 Напишите программу для METAFONT, которая рисовала бы символ вида '^\ [Указания: Данный символ имеет ширину 10 pt, высоту 7pt и глубину 2pt. Такой "звездо- подобный" путь можно определить пятью точками, соединенных "натянутыми" линиями, следующим образом: pair center] center = (.bw1 2pt)\ numeric radius] radius = bpt] for к = 0 upto 4: zk = center + (radius,0) rotated(90 + ™k), endfor def :: = .. tension 5 .. enddef; path star] star = го :: 2:2 :: z\ :: z\ :: z$ :: cycle; Для того чтобы создать иллюзию переплетения кривых, вам, вероятно, понадобится работать с подпутями пути star, а не рисовать сразу весь этот путь.] f* Упражнение 13.9 Что делает команда 'fill star\ если star — это путь, определенный выше? f^ Упражнение 13.10 Определите макрос 'overdraw' так, чтобы команда overdraw с, где с — циклический путь без самопересечений, сначала стирала внутреннюю область с, а затем рисовала выбранным на данный момент пером границу с. (Этот макрос можно будет использовать, например, в программе path 5; S= ((0,1). .(2,0). .(4,2).. (2,5.5) .. (0,8) .. (2,10) .. (3.5,9)) scaled 9pt] for к = 0 upto 35: overdraw fullcircle scaled 3 mm shifted point fc/35 * length S of 5; endfor для построения показанной здесь замысловатой буквы S.) /$\/$\^ Упражнение 13.11 JL JL Компания Mobius Watchband Corporation, производящая ремешки для наручных часов, имеет логотип такого вида: Объясните, как нарисовать его (или нечто очень похожее) при помощи METAFONT. /^s В главе 7 указывалось, что переменные могут иметь тип 'picture', а в главе 8 — JL что и выражения могут иметь тип 'picture', но до сих пор мы не встречали ни одного примера, где присутствовали бы переменные-рисунки или выражения-рисунки. В базовом формате METAFONT имеется специальная переменная-рисунок currentpicture, содержащая рисунок, который обрабатывается в данный момент. Вы можете ее скопировать, приравняв к ней собственную переменную типа picture. Например, если вы укажете в начале вашей программы 'picture v[]\ то сможете затем написать уравнение vi = currentpicture] Таким образом вы приравниваете vi к рисунку, который к этому времени нарисован, т.е. присваиваете переменной vi значение, равное массиву пикселей, который в данный момент представляет переменная currentpicture.
Глава 13: Рисуем, заливаем и стираем 127 Н Рисунки можно складывать и вычитать; например, vi + V2 обозначает рисунок, значения пикселей которого представляют собой суммы значений пикселей в рисунках vi и V2. "Инвертированный" знак опасного поворота, которым отмечен данный абзац получен при помощи программы, приведенной в конце главы 12, с небольшим изменением: вместо команды 'endchar' в нее подставлена последовательность команд picture dbend \ dbend = currentpicture] endchar; % конец обычного знака опасного поворота beginchar(0,25u#, hJxeight* + border*, 0); fill (0, -llpt) --{w, -llpt) -- (w,h) -- (0,h) --cycle; currentpicture := currentpicture — dbend] endchar; 7, конец обращенного знака опасного поворота Все значения пикселей в переменной dbend равны нулю или положительны; поэтому, после вычитания dbend из прямоугольника, положительные значения будут иметь те пиксели, которые находятся внутри данного прямоугольника и имеют нулевое значение в dbend. fB главе 15 мы увидим, что рисунки можно также сдвигать, отражать и вращать на угол, кратный 90°. К примеру, утверждение вида 'currentpicture := currentpicture shifted SrighV сдвигает весь нарисованный на текущий момент рисунок на три пикселя вправо. /$>s В системе METfi FONT имеется константа-рисунок nullpicture, значения всех пик- JL селей которого равны нулю. Команда 'clear it' определяется в базовом формате METAFONT как краткая форма записи присваивания 1 currentpicture :=nullpicture'. Обрабатываемый рисунок автоматически "очищается" при выполнении команд beginchar и mode.setup, так что обычно нет необходимости использовать команду 'clearit'. ^Ниже приведены формальные правила синтаксиса для выражений типа picture. В системе METAFONT сравнительно немного встроенных операций, производимых с целыми рисунками, и подчиняются они тем же правилам синтаксиса, что и аналогичные операции для чисел и пар, с которыми мы уже сталкивались. (первичный рисунок) —> (переменная-рисунок) | nullpicture | ((выражение-рисунок)) | (плюс или минус) (первичный рисунок) (вторичный рисунок) —> (первичный рисунок) | (вторичный рисунок) (преобразователь) (третичный рисунок) —> (вторичный рисунок) | (третичный рисунок) (плюс или минус) (вторичный рисунок) (выражение-рисунок) —> (третичный рисунок) f "Суммарный вес" рисунка есть сумма всех значений его пикселей, деленая на 65536; эту числовую величину вы можете вычислить, указав totalweight (первичный рисунок). Деление на 65536 предотвращает ошибку переполнения в случае, когда рисунок очень большой. Функция totalweight почти всегда возвращает число, модуль которого не превосходит 0.5. В таком случае, чтобы получить целочисленную сумму значений всех пикселей, вы можете смело делить это число на величину epsilon (т.к. epsilon = 1/65536). f Давайте теперь снова сядем за компьютер и попытаемся найти значения некоторых простых выражений-рисунков при помощи универсальной программы expr.mf из главы 8. Когда METAFONT выдаст сообщение 'gimme an expr', наберите hide(fill unitsquare) currentpicture
128 Глава 13: Рисуем, заливаем и стираем и машина ответит: » Edge structure at line 5: row 0: 0+ 1- | Вы спросите, что все это значит? Так вот, 'hide' — это операция из базового формата METAFONT, которая позволяет незаметно вставить команду или последовательность команд внутрь некоторого выражения; эти команды выполняются до того, как остальная часть данного выражения будет прочитана. В нашем случае это команда 'fill unitsquare\ которая устанавливает равным 1 значение одного пикселя в обрабатываемом рисунке, поскольку величина unitsquare определена в базовом формате METAFONT как путь вида (0,0) -- (1,0) -- (1,1) -- (0,1) -- cycle. Значение currentpicture выведено на экран в виде 'row 0: 0+ 1-'; это значит: "в строке 0 значения пикселей увеличиваются в точке х = 0, и уменьшаются в точке х = 1". В своем внутреннем представлении рисунков система METAFONT запоминает лишь вертикальные границы пикселей, или грани, при переходе через которые меняются значения пикселей. Например, в только что выведенном на экран рисунке таких граней всего две, и обе они находятся в строке с номером 0, то есть в полосе, где координаты у меняются от 0 до 1. (Строка с номером к содержит вертикальные грани, у которых координаты х — целые, а координаты у меняются от к до к + 1.) Благодаря тому что во внутреннем представлении хранятся не целые массивы из пикселей, а только структура граней, METAFONT может эффективно работать при высоких разрешениях, поскольку количество граней в рисунке, как правило, прямо пропорционально разрешению, тогда как общее число пикселей пропорционально квадрату разрешения. Таким образом, десятикратное увеличение разрешения вызывает всего лишь десятикратное (а не стократное) увеличение объема задействованной памяти и времени выполнения. Продолжая наш эксперимент, объявим переменную-рисунок V и произведем заливку еще нескольких пикселей: hide(picture V; fill unitsquare scaled 2; V=currentpicture) V Получим массив пикселей вида \ }• Структура граней в нем будет представлена как » Edge structure at line 5: row 1: 0+2- I row 0: 0+2- 0+ 1- I Если теперь мы наберем '-V, то изменятся только знаки при числах: >> Edge structure at line 5: row 1: 0-2+ | row 0: 0-2+ 0- 1+ I (Сейчас вы должны сидеть за компьютером и проделывать эти эксперименты.) Более интересная картина трансформации рисунка получается, если напечатать 'V rotated-90'; в результате появится рисунок \ J, расположенный ниже основной линии, поэтому компьютер выдаст следующую структуру граней: » Edge structure at line 5: row -1: | 0++ 1- 2- row -2: |0+ 2- Здесь '++' обозначает грань, при переходе через которую вес увеличивается больше, чем на 2. В данном случае грани указаны после вертикальных черточек 'I', тогда как в предыдущих примерах они стояли до вертикальных черточек. Это означает, что система METAFONT рассортировала грани по их координатам х. Каждая команда fill или draw
Глава 13: Рисуем, заливаем и стираем 129 привносит в рисунок новые грани, но они не сортируются до тех пор, пока системе METAFONT не потребуется просмотреть их слева направо. (Напечатайте V rotated-90 rotated 90 и вы увидите, как выглядит сам рисунок V с упорядоченными гранями.) Выражение V + V rotated 90 shifted 2right дает структуру, в которой присутствуют и сортированные, и несортированные грани: » Edge structure at line 5: row 1: 0+ 2- I 0+ 2- row 0: 0+2- 0+ 1- I 0+ 1+ 2— Вообще говоря, при сложении рисунков происходит смешение несортированных и сортированных граней в каждой строке по отдельности. /£\/£\^ Упражнение 13.12 JL JL Что произойдет, если вы теперь напечатаете 'hide(cullit) currentpicture'? Проверьте свой ответ экспериментально. /$К/$\>* Упражнение 13.13 JL JL Угадайте (и проверьте), что произойдет, если вы напечатаете выражение (V + V + V rotated 90 shifted 2right - V rotated-90 shifted 2up) rotated 90. [Эту устрашающую формулу вы должны напечатать в одну строку, хотя она не уместилась в одну строку этой книги.] /$s/gK Если вы укажете рисунок 'V rotated 45', то METAFONT ответит вам, что вращение JL JL на 45° — слишком трудная операция. (Проверьте это.) Действительно, квадратные пиксели нельзя вращать на угол, не кратный 90°. Но lV scaled-1' система воспримет нормально, и вы получите » Edge structure at line 5: row -1: 0 2+ 0 1+ I row -2: 0 2+ I /gK/£s^ Упражнение 13.14 JL JL Почему 'V scaled-1' — не то же самое, что '-V? &)&)* Упражнение 13.15 JL JL Поставьте эксперимент с 'V shifted (1.5,3.14159)' и поясните результат. /$К/£\^ Упражнение 13.16 JL JL Угадайте и проверьте результат для 'V scaled 2'. <4*Х$>* Упражнение 13.17 JL JL Почему машина сообщала о структуре граней в пятой строке? /$\/£\ Пожалуй, достаточно компьютерных экспериментов. Попробуйте только напеча- JL JL тать 'totalweight V/epsilon' — просто, чтобы убедиться, что сумма значений всех пикселей в V равна 5. fBce команды, которые мы до сих пор рассматривали в этой главе — fill, draw, filldraw, unfill и т.д. — не являются примитивами METAFONT; они представляют собой макросы базового формата, определенные в приложении В. Давайте теперь рассмотрим операции низкого уровня, которые METAFONT на самом деле выполняет над рисунками и которые остаются "за кулисами". Вот правила синтаксиса:
130 Глава 13: Рисуем, заливаем и стираем (команда, применяемая к рисунку) —> (команда addto) | (команда cull) (команда addto) —> addto (переменная-рисунок) also (выражение-рисунок) | addto (переменная-рисунок) contour (выражение-путь) (список with) | addto (переменная-рисунок) doublepath (выражение-рисунок) (список with) (список with) —> (пустой список) | (список with) (предложение с with) (предложение с with) —► vithpen (выражение-перо) | vithveight (числовое выражение) (команда cull) —> cull (переменная-рисунок) | (сохраняя или отбрасывая) (выражение-пара) | (команда cull) withweight (числовое выражение) (сохраняя или отбрасывая) —> keeping | dropping В этих командах (переменная-рисунок) должна содержать известный рисунок; команда преобразует этот рисунок, а полученный результат присваивает в качестве нового значения той же переменной. f Первая форма команды addto имеет вид addto V also Р1 и означает примерно то же, что и 'V := V + Р\ Но утверждение с addto действует более эффективно, поскольку, прибавляя Р, она уничтожает старое значение V; этим она экономит и память, и время. Ранее в этой главе мы обсуждали инвертированный знак опасного поворота, который, как тогда утверждалось, был получен при помощи утверждения ' currentpicture := currentpicture — dbend\ Тут мы немного приврали; на самом деле это была команда 'addto currentpicture also —dbend\ f Другие формы команды addto устроены несколько сложнее. Но в общих чертах они (неформально) описываются следующей таблицей, где V = currentpicture и q = currentpen: Базовый формат Соответствующие примитивы METAFONT fill с addto V contour с unfill с addto V contour с withweight —1 draw p addto V doublepath p withpen q undraw p addto V doublepath p withpen q withweight —1 filldraw с addto V contour с withpen q unfilldraw с addto V contour с withpen q withweight —1 Вторая форма команды addto имеет вид 'addto V contour p\ со следующими за ней необязательными расширениями, скажем, 'withpen q1 или 'withweight w\ В этом случае р должно быть циклическим путем, каждое перо q должно быть известно, каждый вес w после округления до ближайшего целого должен быть равен одному из чисел —3, —2, — 1, +1, +2 или +3. Если задано несколько перьев или весов, то последнее значение аннулирует все предыдущие. Если перо не указано, то оно принимается равным 'nullpen'; если не указан вес, то его значение принимается равным +1. Таким образом, во второй форме команды addto указываются: переменная-рисунок V, циклический путь р, перо q и вес w. В случае, когда tumingcheck < 0, эта команда имеет следующий смысл: если q = nullpen, то путь р оцифровывается, и значение каждого пикселя увеличивается на (j — k)w, где j и к — это количество вертикальных участков пути слева от данного пикселя, обход которых совершается, соответственно, сверху вниз и снизу вверх (как объяснялось ранее в этой главе). Если перо q — не нулевое, то действие будет таким же, с той лишь разницей, что путь р заменяется другим путем, "обвалакивающим" путьр, форма которого будет зависеть от формы пера q\ этот преобразованный путь оцифровывается и заливается, как и в предыдущем случае. (Преобразованный путь может иметь самопересечения, в результате чего, как мы видели ранее, могут образовываться странные "кляксы". Но он будет достаточно хорошо себя вести, если путь р определяет выпуклую область, то есть, если, объезжая путь р против часовой стрелки, автомобиль никогда не сворачивает вправо.)
Глава 13: Рисуем, заливаем и стираем 131 /gs/gs Если в тот момент, когда выполняется команда 'addto.. .contour', значение JL JL turningcheck > О, то действие ее будет таким же, как только что было описано, при условии, что р имеет положительное число обходов. Однако, если путь р имеет отрицательное число обходов, то действие команды будет зависеть от того, является ли перо q сложным или простым. (Сложным называется перо, граница которого содержит по меньшей мере две точки.) Если число обходов отрицательно и перо простое, то вес w заменяется на — w. Если число обходов отрицательно и перо сложное, то вы получите сообщение об ошибке, в котором будет сказано, что "путь имеет обратное направление обхода". Наконец, если число обходов равно нулю, то вы получите сообщение о "необычном пути", за исключением случая, когда перо q простое и turningcheck < 1. В базовом формате METAFONT устанавливается turningcheck := 2; макрос filldraw, определенный в приложении В, позволяет избежать ошибки "обращенного пути" посредством точного обращения пути с отрицательным числом обходов. <^> Мы упоминали, что команда 'fill (0,2) -- (4,2) -- (4,4) -- (2,4) -- (2,0) — (0,0) — JL cycle' заставляет систему METAFONT пожаловаться на необычный путь; давайте рассмотрим подробнее сообщение об ошибке, которое вы получите: > О ENE 1 NNE 2 (NNW WNW) WSW 3 SSW 4 WSW 5 (WNW NNW) NNE О ! Strange path (turning number is zero). Что это значит? Числа обозначают "время": в начальной точке пути время равно 0, в следующей ключевой точке — 1, и так далее, до возврата в начальную точку. Аббревиатуры типа 'ENE' обозначают азимут, например, "на восток от северо-востока".* METAFONT определяет, в каком из восьми "октантов" происходит движение на каждом участке пути, и ENE обозначает все направления, заключенные между 0° и 45°, включительно. Таким образом, движение по этому конкретному необычному пути начинается в направлении из октанта ENE в момент времени 0, затем, по прошествии времени 1, путь сворачивает в направлении из октанта NNE. В скобках приводятся аббревиатуры октантов, через которые путь "перескакивает" не совершая движения: так, в нашем случае путь "перескочил" через октанты NNW и WNW, перейдя сразу к WSW. По последовательности октантов можно вычислить число обходов. Поэтому, если вы считаете, что путь на самом деле вполне обычный, то по аббревиатурам октантов можете определить, в каком месте система METAFONT сделала неправильный поворот. (Более подробно о необычных путях рассказывается в главе 27.) /$К/$\ Третья форма команды addto имеет вид 'addto V doublepath р' со следующими JL JL за ней необязательными расширениями, которые определяют перо q и вес tu, как и во втором случае. Если путь р — не циклический, то этот случай сводится ко второму заменой р на удвоенный путь 'р& reverseр& cycle'. (Исключение составляет случай, когда р состоит из одной точки, тогда новый путь — это просто 'р .. cycle'.) Если же путь р — циклический, то этот случай сводится к двум командам addto второго типа, в одной из которых путь р обращается; в обоих этих командах величина turningcheck игнорируется. /$\ Когда р — очень маленький циклический путь, а перо q очень большое, команда JL 'draw р' или, в общем случае, 'addto V doublepath р withpen q} может давать аномальный результат: пиксели, которые должны покрываться пером независимо от того, где на пути р они расположены, могут сохранить прежние значения. Выход из такой необычной ситуации прост — достаточно вставить дополнительную команду 'draw г' или 'addto V doublepath z withpen q\ где z — произвольная точка пути р, чтобы закрасить пиксели, оставшиеся непокрытыми. f Команда cull преобразует переменную-рисунок так, что значения всех пикселей становяться равными либо 0, либо некоторому указанному весу iu, где w определяется так же, как в команде addto. Задается пара чисел (о, 6), где а должно быть меньше * Здесь используются стандартные обозначения сторон света: N — север, S — юг, W — запад, Е — восток. — Прим. перев.
132 Глава 13: Рисуем, заливаем и стираем либо равно Ь. Команда cull с расширением 'keeping (а, 6)' присваивает каждому пикселю новое значение, равное w тогда и только тогда, когда его старое значение v принадлежало отрезку о < v < Ь; команда cull с расширением 'dropping (а, 6)' присваивает каждому пикселю новое значение, равное w тогда и только тогда, когда его старое значение v не принадлежало этому отрезку. Таким образом, команда 'cullit' — аббревиатура команды cull currentpicture keeping (1, infinity) или cull currentpicture dropping (—infinity ,0) (обе они означают одно и то же). В более сложном примере cull V5 dropping (—3,2) withweight —2 значения пикселей в Vs меняются на —2, если они были меньше либо равны —4 или если они были больше или равны 3; значения пикселей, заключенные между —3 и +2, обнуляются. f Команда cull не должна менять нулевые значения пикселей на ненулевые. Например, METAFONT не позволяет вам указывать 'cull V\ keeping (0,0)', поскольку это привело бы к тому, что бесконечно много пикселей приобрело значение 1. f ►Упражнение 13.18 Каким будет результат следующей последовательности команд? picture V[]; Vi = V2 = currentpicture; cull Vi dropping (0,0); cull V2 dropping (—1,1); currentpicture := Vi — V2; f> Упражнение 13.19 Даны переменные-рисунки Vi и Vb, о которых известно, что значения всех пикселей в них равны 0 или 1. Объясните, как заменить V\ на (a) Vi О Vi; (b) Vi U V2; (с) Vi Ф Vi. [В пересечении рисунков Vi П Vi единицы стоят на тех местах, где единицы стоят как в Vi, так и в Кг; в объединении V\ U V2 нули стоят в тех местах, где нули стоят как в Vi, так и в Vi; симметрическая разность, или селективное дополнение, Vi Ф Vi, имеет единицы в тех местах, где значения Vi и Vi не совпадают.] <£><£>► Упражнение 13.20 JL JL Объясните, как проверить равенство двух переменных-рисунков. f/^s^ Упражнение 13.21 JL Просмотрите определения команд fill, draw и т.д., приведенные в приложении В, и определите, каким будет результат следующих утверждений: a) draw р withpen q\ b) draw p withweight 3; c) undraw p withweight w\ d) fill с withweight —2 withpen q\ e) erase fill с withweight 2 withpen currentpen; f) cullit withweight 2. /^►Упражнение 13.22 Определите макрос safefill так, чтобы команда 'safefill с' увеличивала на 1 значения всех тех пикселей рисунка currentpicture, значения которых изменились бы, в результате выполнения команды 'fill с\ (В отличие от fill, команда safefill никогда не останавливается, выдавая сообщение о "необычном пути"; более того, она никогда не увеличивает значения пикселей больше, чем на 1, и не уменьшает значения пикселей, даже если циклический путь с — довольно "дикого" вида.)
Глава 13: Рисуем, заливаем и стираем 133 /gK/gs^ Упражнение 13.23 JL JL Объясните, как заменить символ его "контуром": черные пиксели должны менять свой цвет на белый, если четыре ближайших пикселя также черные, поскольку эти пиксели лежат внутри символа. (Ближайшие по диагонали пиксели не в счет.) /$\/$К>> Упражнение 13.24 JL JL В игре Джона Конвея "Жизнь" пиксели могут быть живыми или мертвыми. Каждый пиксель граничит с восемью соседними. В (п + 1)-ом поколении живыми оказываются те пиксели, которые в n-ом поколении были мертвы, но имели в точности трех живых "соседей", или же были живы и имели в точности двух или трех живых "соседей". Напишите небольшую программу для METAFONT, которая последовательно выводила бы на экран поколения живых пикселей. Вычеркивай и правь, вставляй и совершенствуй, Уменьши, увеличь, строку перемести; Коль сложится не в лад, имей в виду, придется Тебе и ногти грызть, и голову скрести. — ДЖОНАТАН СВИФТ, О поэзии: Рапсодия (1733) Возможность осмыслить, которую дает компьютерная графика, для нас более значима, чем просто возможность производить картинки. — АЙВЕН Э. САЗЕРЛЭНД, ScetChpad (1963)
Пути
Глава Ц: Пути 135 Границы заливаемых областей и траектории движения перьев представляют собой "пути", которые могут быть заданы при помощи общего метода, описанного в главе 3. Благодаря тому что в системе METAFONT переменные и выражения могут иметь тип path (путь), разработчик имеет возможность строить новые пути из старых множеством различных способов. Задача данной главы — завершить то, что было начато в главе 3. Мы начнем с обзора некоторых специальных средств базового формата METAFONT, облегчающих построение путей, а затем подробно изучим все имеющиеся в системе METAFONT методы работы с путями. Существует несколько стандартных видов путей, которые оказываются полезными для множества прикладных задач. Поэтому они заранее определяются как часть базового формата (см. приложение В). Например, quartercircle — это путь, представляющий собой четверть окружности единичного диаметра; путь проходит от точки (0.5,0) до точки (0,0.5). Таким образом, программа для METAFONT beginchar ("a", bpt#, 5р*#, 0); pickup pencircle scaled (Apt 4- blacker); draw quartercircle scaled lQpt; endchar; генерирует символ 'V и помещает его в позицию буквы 'а'. ► Упражнение 14.1 Напишите программу, которая помещает залитую четверть круга 'ь' в позицию буквы 'Ь'. ► Упражнение 14.2 Почему символы 'V и '*' имеют всего 5pt в ширину и в высоту, хотя генерируются при помощи пути 'quartercircle scaled lOpt'? f ►Упражнение 14.3 Используя повернутый путь quatercircle, создайте символ V в позиции буквы 'с'. f ►Упражнение 14.4 Используя путь quartercircle, создайте символ '<?' в позиции 'd'. В базовом формате METAFONT определен также путь halfcircle, который дает символ V"V; этот путь строится из двух четвертинок окружности следующим образом: half circle = quartercircle & quartercircle rotated 90. И, конечно же, имеется полная окружность единичного диаметра — fullcircle: fullcircle = halfcircle & halfcircle rotated 180 & cycle. Вы можете нарисовать окружность диаметра D с центром в точке (х,у), указав: draw fullcircle scaled D shifted (x,y); точно так же, 'draw fullcircle xscaled А у scaled Б' дает эллипс с осями А и В. Кроме окружностей и их частей, имеется также стандартный "квадратный" путь unitsquare. Это циклический путь, который начинается в точке (0,0), проходит в (1,0), далее в (1,1), далее в (0,1) и, наконец, возвращается в (0,0). Например, команда 'fill unitsquare1 увеличивает на единицу значение одного пикселя, о чем говорилось в предыдущей главе.
136 Глава Ц: Пути ► Упражнение 14.5 Используя fullcircle и unitcircle, создайте символы '(|))' и *<ф>' и поместите их в позиции букв V и 'f, соответственно. Символы должны иметь 10 pt в ширину и 10 pt в высоту, а их центры — лежать на 2.5 pt выше базовой линии. path 6птсЛ[], trunk; branchi =/le*((0,660), (-9,633), (-22,610)) к flex к flex branch 2 к flex к flex branchz & flex к flex к flex branchy & flex к flex к flex branch 5 к flex к flex branchy к flex к flex branch? к flex к flex branch 8 & /lex к flex к /Tex бгапсЛд & /lex & /lex branch io к flex к, flex к. flex branch ii & /lex &/lex & /lex branch i2 & /lex к flex к flex к flex trunk = & /lex к flex к flex 22,610), (-3,622), (17,617)) 17,617), (7,637), (0,660)) к cycle; /Iex((30,570), (10,590), (-1,616)) -1,616), (-11,592), (-29,576), (-32,562)) -32, 562), (-10,577), (30,570)) к cycle; /lex((-l, 570), (-17,550), (-40,535)) -40,535), (-45,510), (-60,477)) -60,477), (-20,510), (40,512)) 40, 512), (31,532), (8,550), (-1,570)) к cycle; /Iex((0,509), (-14,492), (-32,481)) -32,481), (-42,455), (-62,430)) -62,430), (-20,450), (42,448)) 42,448), (38,465), (4,493), (0,509)) к cycle; /iex((-22,470), (-23,435), (-44,410)) -44,410), (-10,421), (35,420)) 35,420), (15,455), (-22,470)) к cycle; /!ex((18,375), (9,396), (5,420)) 5,420), (-5,410), (-50,375), (-50,350)) -50,350), (-25,375), (18,375)) к cycle; /?ex((0,400), (-13,373), (-30,350)) -30,350), (0,358), (30,350)) 30, 350), (13,373), (0,400)) к cycle; /?ex((50,275), (45,310), (3,360)) 3, 360), (-20,330), (-70, 300), (-100,266)) -100,266), (-75,278), (-60,266)) -60,266), (0,310), (50,275)) к cycle; /Zex((10,333), (-15,290), (-43,256)) -43, 256), (8,262), (58,245)) 58, 245), (34,275), (10,333)) к cycle; = /Iex((8,262), (-21,249), (-55,240)) 55,240), (-51,232), (-53,220)) 53,220), (-28,229), (27,235)) 27,235), (16,246), (8,262)) к cycle; = /iex((0,250), (-25,220), (-70,195)) -70,195), (-78,180), (-90,170)) -90,170), (-5,188), (74,183)) 74,183), (34,214), (0,250)) к cycle; = flex((S, 215), (-35,175), (-72,155)) -72,155), (-75,130), (-92,110), (-95, 88)) -95,88), (-65,117), (-54,104)) 54,104), (10,151), (35,142)) .. /*ex((42,130), (60,123), (76,124)) 76,124), (62,146), (26,180), (8,215)) к cycle; 0,660) ---(-12,70) .. {curl5}(-28,-8) -28,-8), (-16,-4), (-10,-11)) -10,-11), (0,-5), (14,-10)) 14,-10), (20,-6), (29,-11)) к (29,-ll){curl4} .. (10,100)---cycle;
Глава Ц: Пути 137 Иногда приходится рисовать кривые достаточно сложного вида. Для того чтобы облегчить эту задачу, в базовом формате METAFONT определена операция lflex\ Конструкция iflex(zi^Z2,zsY обозначает путь lz\ .. 2:2(23 — z\) .. 2:3'; точно так же, lflex(zi,Z2, 2:3,24)' обозначает 'zi .. 2:2(2:4 — zi} .. 2:3(24 — zi} .. 2:4'; и вообще, /lea:(zi,Z2,...,zn_i,zn) обозначает путь вида Z\ . . Z2{zn — Zi} . . ••• . . Zn-i{zn — Zi} . . Zn. Смысл этой конструкции в том, что задаются две крайние точки и одна или несколько промежуточных точек, в которых движение по данному пути происходит в том же направлении, что и движение по прямой из z\ в zn; такие промежуточные точки легко определить на типичной кривой, и они будут естественными кандидатами на роль ключевых точек. Например, после того как будут подходящим образом заданы точки 2^, ..., 2:9, команда fill flex(zi,z2,z3) к flex(z3,z4,z5) Sz flex(zrt,z6,z7) & flex(z7, z$,z9, z\) & cycle произведет заливку следующей области: Именно такую форму имеет четвертая сверху ветка "Эль Пало Альто" — дерева, которое символизирует собой Станфордский университет. Для того чтобы определить тринадцать путей с предыдущей страницы, мы набросали эскиз этого дерева на листе миллиметровки, а затем, набирая программу на компьютере, прикинули "на глаз" координаты ключевых точек и подставили их в текст программы. (При наборе такой кучи данных справиться со скукой вам поможет хорошая теле- или радиопередача.) Всего в этой фигуре присутствует 47 "флексов", в большинстве своем ничем не примечательных; но путь branchy содержит интересный подпуть: flex(zuz2,z3) ..flex(z4:z5,z6), который раскладывается в последовательность Z\ • • 2:2(2:3 — Z\] . . 2:3 . • 2:4 . . 2:5(2:6 — 2:4} . . Zq. Несмотря на то что здесь присутствует два разных "флекса", все шесть точек соединяются гладкой кривой, поскольку в данном примере z% ф z±.
138 Глава Ц: Пути После того как пути определены, с их помощью мы можем легко создавать символы вроде показанного здесь черно-белого медальона: beginchar(nT",.5m#,1.25m#,0); (Определения тринадцати путей, приведенных выше); fill superellipse ((w,.bh), (.5u;,/i), (0, .5/i), (.5iu,0), .8); branch о = trunk; for n = 0 upto 12: unfill branch[n] shifted (150,50) scaled (ги/300); endfor endchar; Обрамляющий дерево овал представляет собой еще один стандартный путь, определенный в базовом форматном файле; называется этот путь superellipse. Для того чтобы создать фигуру такого типа, вы можете написать superellipse (right-point, top-point, leftjpoint, bottom-point, superness) где 'superness'' — показатель деформированности эллипса, который определяет, насколько сильно данная кривая будет отличаться от правильного эллипса. Вот, к примеру, четыре деформированных эллипса, которые нарисованы при разных значениях показателя деформированности superness с использованием пера pencircle xscaled 0.7pt yscaled 0.2p£ rotated 30: Значение superness должно быть заключено между 0.5 (при котором вы получите ромб) и 1.0 (при котором вы получите квадрат); как правило, используются значения близкие к 0.75. Символ '0' (ноль) из шрифта печатной машинки, используемого в этой книге, нарисован в виде деформированного эллипса с деформированностью 2~'5 « .707, что соответствует нормальному эллипсу; а буква '0' нарисована с деформированностью 2~25 « .841, чтобы ее было легче отличить от нуля. Двусмысленный символ '0' (которого в этом шрифте нет, но который система METAFONT, конечно же, может изобразить) занимает промежуточное положение между этими крайними случаями; его показатель деформированности равен .77. /£\/£\ Математически деформированный эллипс описывается уравнением вида: \х/а\^ + JL JL \у/Ь\Р = 1, где р — произвольный показатель. Деформированный эллипс имеет крайние точки (±о, 0) и (0, ±6), а также "угловые" точки (±сга, ±сгЬ), где а = 2~1^ — показатель деформированности. Касательная, проведенная к данной кривой в точке (аа,сг6), проходит в направлении (—а, 6) и, следовательно, параллельна прямой, проведенной из точки (а,0) в точку (0,6). Впервые деформированный эллипс описал Габриэль Ламэ в 1818 году, а Пит Гейн популяризировал специальный случай, когда /3 = 2.5 [см. Martin Gardner, Mathematical Carnival (New York. Knopf, 1975), 240-254]; в этом специальном случае показатель деформированности равен 2~'4 « .7578582832552. Программа superellipse из базового форматного файла генерирует не идеальный деформированный эллипс точно
Глава Ц: Пути 139 так же, как программа fullcircle генерирует не совсем правильную окружность, однако для практических целей их результаты вполне удовлетворительны. /gK/gK^ Упражнение 14.6 JL JL Попробуйте задать в superellipse показатель деформированности меньший 0.5 или больший 1.0; объясните, почему в таких случаях получаются непонятные фигуры. Давайте теперь рассмотрим подробнее символы, которые в определениях путей стоят между ключевыми точками. В базовом формате METAFONT имеется пять таких лексем: свободная кривая; ограниченная кривая; прямая линия; — "натянутая" линия; к сращивание. Вообще, когда вы пишите zq .. z\ .. (и т.д.) .. zn-i •• zn, система METAFONT отыскивает путь длины п, который, по ее мнению, является наиболее "красивой кривой", проходящей через точки zo, z\, ..., zn. Символ '...' значит примерно то же, что и '..'. Отличие его, как было сказано в главе 3, состоит лишь в том, что в тех случаях, когда это возможно, путь загоняется в рамки некоторого ограничивающего треугольника. На отрезке прямой 'zjt-i -- z^ путь, как правило, резко меняет направление в точках Zk-i и z^. Отрезок же вида lZk-i — Zk\ наоборот, плавно соединяется с соседними кривыми; то есть, путь входит в Zk-i и выходит из Zk в направлении Zk - Zk-i. (Этот тип соединения использовался нами при определении подпути trunk из рисунка Эль Пало Альто, а также при определении щита в программе для знака опасного поворота из главы 12.) Наконец, операция '&' означает объединение двух независимых путей в их общей точке точно так же, как для цепочек '&' означает сцепление. Все пять основных видов соединений можно проиллюстрировать на примере следующего, несколько несуразного пути: z0 = (0,100); л = (50,0); z2 = (180,0); for п = 3 upto 9: z[n] = z[n - 3] 4- (200,0); endfor draw zo .. z\ — z<i... {up }z$ к zz .. Z4--Z5... {up}z6 к ZQ...Z7 --- ZS .. {up}zg. f Операция *...' обычно применяется, если указаны одно или оба смежных направления (например, 1{ир}\ как в данном примере). В конструкции flex из базового формата METAFONT, на самом деле, используется именно '...', ане'..', как утверждалось ранее. В определенных ситуациях, благодаря этому удается избежать появления точек перегиба.
140 Глава Ц: Пути f Такой путь, как 'zo — z\ — zi\ практически неотличим от ломаной линии lzo -- z\ -- Z2 . Только увеличив предыдущий путь, вы увидите, что его линии не являются идеально прямыми; они будут чуточку искривлены так, чтобы в z\ кривая была "гладкой", несмотря на то, что в этой точке путь довольно круто сворачивает. (Это значит, что применяются операции автоокругления, описанные в главе 24.) Например, путь (0,3) — (0,0) — (3,0) эквивалентен пути: (0,3) .. controls (-0.0002,2.9998) and (-0.0002,0.0002) .. (0,0) .. controls (0.0002, -0.0002) and (2.9998, -0.0002) .. (3,0) тогда как путь (0, 3) -- (0,0) -- (3,0) состоит из двух идеально прямых отрезков: (0,3) .. controls (0,2) and (0,1) .. (0,0) .. controls (1,0) and (2,0).. (3,0). f> Упражнение 14.7 Путь unitsquare определяется в базовом формате METAFONT как '(0,0) -- (1,0) -- (1,1) -- (0,1) -- cycle'. Объясните, как определить такой же путь, не используя '--' и не задавая точных направлений, а используя только '..' и '&'. /$\/$\ Иногда бывает нужно взять некоторый путь и заменить в нем все операторы JL JL соединения на *—' независимо от того, какими они были прежде; ключевые точки при этом не изменяются. Для этой цели в базовом формате METAFONT предусмотрена операция tensepath, выполняющая данную процедуру. Например, tensepath unitsquare = (0,0) --- (1,0) --- (1,1) --- (0,1) ---cycle. Когда система METAFONT решает, какую кривую следует нарисовать на месте '..' или '...', ей приходится учитывать особые соглашения относительно начальной и конечной точек. Она должна позаботиться о том, чтобы путь начинался и заканчивался как можно более изящно. Обычно наилучшее решение состоит в том, чтобы сделать первый и последний отрезки пути предельно близкими по форме к дугам окружностей. Поэтому такой незамысловатый путь длины 2 как *2о •• zi •• z2 оказывается достаточно точным приближением единственной дуги окружности, проходящей через точки (20,2:1,22). Вы можете изменить это принимаемое по умолчанию поведение на концах. Для этого вы должны указать либо точное направление, либо величину "загиба". Если вы установите величину загиба меньшей 1, то вблизи крайних точек кривизна пути будет уменьшаться (т.е. поворот будет менее крутым); если же вы укажете величину загиба, большую 1, то кривизна будет возрастать. (См. подпуть trunk в определении Эль Пало Альто, приведенном выше в этой главе.) Вот несколько пар круглых скобок, нарисованных при разной величине загиба. Все они получены при помощи утверждения 'penstroke 2roe{curlc} .. z\e .. {curlc}^2e'; при разных значениях с получаем разные скобки: величина загиба 0 12 4 infinity результат () () () () () (В шрифтах семейства Computer Modern для определения скобок используется более общая схема, описанная в главе 12; там вместо величины загиба в крайних точках указываются точные направления, так как это дает лучшие результаты в нестандартных ситуациях, когда символы очень высоки или очень широки.) /£\ Величина загиба должна быть неотрицательной. Если загиб очень велик, то JL METAFONT, вместо того чтобы делать очень крутой поворот в крайних точках изменяет весь остальной путь так, чтобы его кривизна была относительно небольшой в соседних точках.
Глава Ц: Пути 141 fB главе 3 указывалось, что мы можем изменить вид используемых по умолчанию кривых, задав нестандартную величину "натяжения" между точками или указав непосредственно все контрольные точки, которые должны использоваться в четырехточечном методе. Чтобы до конца понять, какие пути может строить система METAFONT, давайте рассмотрим полный перечень правил синтаксиса для выражений-путей. Общие правила таковы: (первичный путь) —у (первичный путь) | (переменная-путь) | ((выражение-путь)) | reverse (первичный путь) | subpath (выражение-путь) of (первичный путь) (вторичный путь) —> (вторичная пара) | (первичный путь) | (вторичный путь) (преобразователь) (третичный путь) —> (третичная пара) | (вторичный путь) (выражение-путь) —> (выражение-пара) | (третичный путь) | (подвыражение-путь) (указатель направления) | (подвыражение-путь) (связка путей) cycle (подвыражение-путь) —> (выражение-путь) | (подвыражение-путь) (связка путей) (третичный путь) (связка путей) —> (указатель напр-я) (элементарная связка) (указатель напр-я) (указатель направления) —> (пустой указатель) | { (числовое выражение) } | {(выражение-пара) } | { (числовое выражение) , (числовое выражение) } (элементарная связка) —> & | .. | .. (натяжение) .. | .. (контрольные точки) .. (натяжение) —► tension (величина натяжения) | tent ion (величина натяжения) and (величина натяжения) (величина натяжения) —У (первичное числовое) | at least (первичное числовое) (контрольные точки) —> controls (первичная пара) | controls (первичная пара) and (первичная пара) Сразу бросается в глаза отсутствие в этих правилах операций '...', '--' и '—'. Дело в том, что они определяются в приложении В как макросы: есть сокращение записи '.. tension atleast 1 ..'; есть сокращение записи '{curl 1} .. {curl 1}'; — есть сокращение записи '.. tension infinity ..'. f Несмотря на то что в этих синтаксических правилах операция '- -' и ей подобные не упоминаются прямо, в них заложен широкий спектр возможностей. Поэтому давайте рассмотрим подробней их применение. В общем виде выражение-путь можно записать в виде: Ро ji Pi J2 ••• jn Pn, где каждое pk обозначает третичную формулу типа pair или path, а каждое jk — это "связка путей". Связка путей начинается и заканчивается "указателем направления", а между ними находится "элементарная связка". Указатель направления может быть пустым, может иметь вид '{curie}', где с > О, или быть вектором некоторого направления, заключенным в фигурные скобки. Так, например, '{UP}' указывает направление "вверх", поскольку в базовом формате METAFONT величина up определяется как пара (0,1). То же самое направление можно определить как '{(0,1)}' или как '{(0,10)}', или, опуская круглые скобки, — '{0,1}'. Если вектор указанного направления оказывается равным '(0,0)', то система METAFONT ведет себя так, как если бы направление не было указано вовсе; то
142 Глава Ц: Пути есть указатель '{0)0}' эквивалентен пустому указателю. Пустой указатель направления "заполняется" в строгом соответствии с правилами, которые мы обсудим позже. f Элементарные связки бывают трех основных видов: (1) '&' просто объединяет два пути, которые обязательно должны иметь одну общую крайнюю точку. (2) '.. tension a and /3 ..' значит, что должна быть определена некая кривая с "натяжениями" а и /?. Значения а и j3 должны быть больше или равны 3/4; мы обсудим натяжение чуть позже в этой главе. (3) '.. controls и and v ..' определяет кривую с промежуточными контрольными точками и и v. f Допускаются также и специальные сокращения, так что обычно длинных форм записи элементарных связок удается избежать: сама по себе связка '..' означает '. . tension 1 andl ..', связка '.. tension а ..' означает '. . tension q and а ..', a '.. controls и ..' означает '.. controls и and и ..'. ^До сих пор в наших примерах пути всегда строились по точкам; но, согласно правилам синтаксиса, мы можем написать, например, 'ро • • Pi • • рг\ где р сами являются путями. Что означает такая запись? А вот что: каждый такой путь заменяется последовательностью кривых с точно определенными контрольными точками; система METfi FONT раскладывает такие пути в соответствующие последовательности точек и элементарных связок типа (3). Например, '((0)0) .. (3,0)) Л (3,3)' фактически означает то же самое, что и '(0,0) .. controls (1,0) and (2,0) .. (3,0) .. (3,3)', так как '(0,0) .. (3, 0)' — это путь '(0,0) .. controls (1, 0) and (2, 0) .. (3,0)'. Если таким способом на подпути раскладывается цикл, то его циклическая природа при этом будет утеряна; просто его последняя точка будет точной копией первой. f Теперь давайте рассмотрим правила, по которым пустые указатели направления приобретают определенные значения в зависимости от того, что их окружает. Если пустой указатель направления стоит в начале или конце пути, или же сразу после оператора '&', то он благополучно заменяется на '{curl 1}'. Это правило нужно правильно трактовать в случае циклического пути, который не имеет ни начала, ни конца. Например, 'zo • • z\ & zi .. 22 . • cycle' эквивалентно lzo .. 2i{curl l}&{curl l}zi .. 22 .. cycle'. /$\ Если после некоторой точки стоит непустой указатель направления, а указатель, JL стоящий перед ней, пуст, то непустой указатель копируется в оба положения. Таким образом, например, '.. z{iu}' рассматривается как '.. {w}z{w}\ Если непустой указатель стоит перед точкой, а указатель, стоящий после нее, пуст, то непустой указатель точно так же копируется в оба положения, за исключением тех случаев, когда он следует за элементарной связкой, в которой контрольные точки указываются непосредственно. Указатель направления, стоящий непосредственно после связки '.. controls u and г; ..', всегда игнорируется. f Пустой указатель направления, стоящий после указанной непосредственно контрольной точки, приобретает значение, равное направлению смежного отрезка пути. Точнее, если и ф -г, то '.. z .. controls и and v ..' рассматривается как '.. {и — z}z .. controls и and v ..', а если и = z, то — как '.. {curl 1}г .. controls u and г; ..'. Точно так же, '.. controls it and г; .. z ..' трактуется так, как если бы за z следовало {z — v} в случае z ф v. В противном случае считается, что за z стоит {curl 1}. /£\/£\ Применив все три предыдущих правила, мы опять можем оказаться в ситуации, JL JL когда существуют точки, окруженные пустыми указателями направления. Система METAFONT должна подобрать подходящие направления в этих точках, и делает она это, применяя следующий алгоритм, предложенный Джоном Хобби [John Hobby, Discrete and Computational Geometry 1 (1986), 123-140]. Пусть задана последовательность zo{do} •. tensionao and/?i .. z\ .. tension ai and/fc .. 22 (и т.д.) zn_i .. tensionq„_i and/?n .. {dn}zn
Глава Ц: Пути 143 и в ней требуется определить внутренние направления. Мы будем рассматривать точки z как комплексные числа. Пусть Ik = \zk — Zk-i\ — расстояние между Zk-i и Zk, и пусть •фк = arg((<2fc_|_i — Zk)/(zk — Zk-i)) — угол обращения в точке Zk- Нам нужно отыскать векторы направлений u>o, tui, ..., u>n, такие, чтобы мы могли успешно заменить данную последовательность на zo{wo} .. tension Qo and/?i .. {iui}2i{tui} .. tension a\ and fc • . {^2)^2 (и т.д.) zn-i{wn-i} • • tensionQn-i and^n • • {wn}zn. Поскольку абсолютные величины векторов w не играют роли, а важны только их направления, то достаточно определить углы вк = aig(wk/(zk+i — Zk)). Кроме того, для удобства мы положим фк = arg((zjb — Zk-i)/wk), так чтобы Ок + Фк+Фк = 0. (*) В упомянутой статье Хобби вводится понятие "модельной кривизны", с помощью которого выводятся следующие уравнения для внутренних точек: &VK~-i(o*-i + Ф*) - з**) = ^Ci№+i(^ + **+0 -3^)- (**) Кроме того, нам нужно учесть граничные условия. Если do — точный вектор направления tuo, то мы знаем во\ в противном случае do есть 'curl 70', и мы выводим уравнение <*о(/?Г1(0о + 0i) - 30о) = 7o/?i(ao Ч*о +0i) - 30i). (***) Если dn — вектор направления wn, то мы знаем вп\ в противном случае dn есть 'curl7n\ и мы выводим: 02(o£ii(*»-i + Фп) - Ж) = 7n<4-i(/?« 4^-1 + Фп) - 30„-i). (***') Можно показать, что из условий а* > 3/4, рк > 3/4, 7* > 0 следует существование и единственность решения системы, состоящей из уравнений (*) и (**) при 0 < к < п, плюс два граничных уравнения; следовательно, искомые величины 0о, •••, 0п-\ и </>i, ..., фп определяются единственным образом. (Единственное исключение — это вырожденный случай, когда п = 7071 = !•) /^s/$s Похожая схема работает и в случае цикла, когда нет ни '{do}\ ни l{dny. В этом JL JL случае уравнения (*) и (**) имеют место для всех к. /gK/$K^ Упражнение 14.8 JL JL Выпишите уравнения, определяющие выбор направлений в циклическом пути длины 3 общего вида: 'го .. tension qo and/3i .. z\ .. tension qi and/?2 •. 22 •. tension аг and/Зз .. cycle'. (Вам не нужно решать эти уравнения.) /$\/gs Итак, с помощью этих уравнений мы определили направления во всех точках. JL JL Для того чтобы полностью определить путь, нам нужно теперь объяснить, как участок пути типа 'го{и>о} • • tension a and/3 .. {wi}zi* превращается в участок вида 'го . • controls и and v .. zi'; то есть мы хотим знать, каким магическим рецептом пользуется система METAFONT при выборе точек и и v. Если в = arg(iuo/(<zi — ^о)) и ф = arg((zi — zq)Iw\), то контрольные точки будут иметь вид и = zo + e*(zi - zo)f(0, ф)/а, v = zi - e~*(zi - *о)/(0, в)/р, где /(0,ф) определяется по еще одной формуле, принадлежащей Джону Хобби: 2 + v/2 (sin в - ± sin 0)(sin ф - ^ sin 6)(cos в - cos ф) 3(1 + ±(у/5 - 1) COS0 + |(3 - уД ) СОБф) ' <£)<£> Есть еще одна тонкость: если натяжениям а и/или /9 предшествует ключевое JL JL слово 'atleast', то значения а и/или /? при необходимости увеличиваются до минимальных значений, при которых точки и и v окажутся в пределах "ограничивающего треугольника", обсуждавшегося в конце главы 3.
144 Глава Ц: Пути f Какие же выводы из этих сложных правил могут сделать для себя те пользователи METAFONT, которые не сильны в математике? Самое важное то, что эти правила для путей инвариантны относительно операций сдвига, масштабирования и вращения. Иными словами, если все ключевые точки Zk пути сдвигаются, масштабируются и/или вращаются одинаково, то результат будет таким же, как при соответствующем сдвиге, масштабировании и/или вращении пути, определяемого ^трансформированными точками Zk (конечно, с точностью до возможных ошибок округления). Однако инвариантность теряется, если масштабирование производится независимо по каждому из направлений. /gK Кроме того, из этих правил следует, что если заданы смежные направления, то JL задание натяжений имеет вполне очевидную интерпретацию в терминах контрольных точек, так как в формулы для и и v натяжения аир входят только в качестве делителей. Это значит, что, например, при натяжении 2 контрольные точки становятся вдвое ближе к соседним ключевым точкам, а при значении натяжения, равном infinity, — подходят к ним вплотную; и наоборот, если значение натяжения меньше 1, то контрольные точки отдаляются. /gK Задание натяжений влияет также и на выбор системой METAFONT направлений JL движения в ключевых точках. Вот почему, например, конструкция lZk-i — Zk (которая означает KZk-\ .. tension infinity .. Zk ) влияет на больший путь при входе ъ Zk-i и выходе из Zk. fH3 правил следует, что, когда системе METAFONT приходится выбирать направления во всех точках между zq и zn, изменение положения точки zn влечет за собой изменение поведения кривой вблизи точки zq. Однако, как правило, это изменение пренебрежимо мало везде, за исключением точек в непосредственной близости от измененной. Вы можете сами в этом убедиться, посмотрев, например, какие контрольные точки выбирает система METAFONT для пути '(О,0) ..(1,0).. (2,0) .. (3,0) .. (4,0)... {ир}(5, у)' при разных у. /gk/gK^ Упражнение 14.9 JL JL Запустите METAFONT с файлом 'ехрг' из главы 8 и попросите машину посмотреть выражение-путь iunitsquare shifted (0,1) unitsquare shifted (1,0)'. Полученные результаты объясните. /gK/gK^ Упражнение 14.10 JL JL Мы уже упоминали о том, что в базовом формате METAFONT '--' есть сокращение от '{curl 1} .. {curl 1}'. Скажите, было бы какое-нибудь существенное отличие, если бы '--' определялось как '{curl 2} .. {curl 2}'? <J2*><4>>* Упражнение 14.11 JL JL Внимательно просмотрите синтаксис выражений типа (выражение-путь) и объясните, как система METAFONT интерпретирует путь '(0,0) .. (3,3) .. cycle{curl 1}'. f Давайте теперь вернемся к более простым вопросам, связанным с путями. После того как путь задан, с ним можно много чего делать, кроме рисования, заливки и тому подобного. Например, если р — путь, то вы можете обратить направление его обхода, указав 'reverse р'; обратным к 'го .. controlsu and v .. z\ является путь '21 .. controls v and и .. zq. * ► Упражнение 14.12 Верно ли для любого пути р равенство length reverse р = length р? fC путями удобно ассоциировать "время", представляя, что при обходе пути длины п время пробегает значения от 0 до п. (Мы уже иллюстрировали это понятие в главе 8 на примере пути р2, по форме очень близкого к окружности. Прежде чем
Глава Ц: Пути 145 продолжить чтение, вам не помешало бы перечесть то, что говорилось в главе 8 по поводу путей и подпутей.) Для заданного пути р = zo .. controls wo andvi .. zi (и т.д.) 2n-i .. controls un-i and и числа t система METAFONT определяет 'point t of p' следующим образом: если t < О, то результат есть точка zq\ если t > п, то результат есть точка zn; иначе, если к < t < к 4-1, то результат есть (t — k)[zk, itjt, Vk+i, 2fc+i], где мы обобщили обозначение *i[a, /?]' так, что t[a, /?, 7] означает f[i[a,/?],£[/?, 7]], а t[a,/?, 7, <5] означает *[*[а,/3,7]>*[А7>^]]- (Это — полином Бернштейна относительно i, см. главу 3.) Для данного циклического пути с = zo .. controls ио andt>i .. zi (etc.) zn-i .. controls un_i andi>n •. cycle и числа t система METAFONT определяет точку 'point t of с' в целом так же, с той лишь разницей, что вначале число t сокращается по модулю п до числа, лежащего в промежутке О < t < п. &>&>* Упражнение 14.13 JL JL Верно ли равенство: point t of (zo -- z{) = t[zo, zi]? /$N Для заданного пути p и двух значений времени t\ < £2 величина 'subpath (£1,^2) JL of р' — это подпуть, который состоит из всех точек 'point t of р\ где t пробегает значения от ti до t2- Если ti и t2 — целые числа, то нетрудно понять, как определяется этот подпуть. Например, в принятых выше обозначениях и предположении п > 4 subpath (2,4) of р = Z2 .. controls 112 andv3 . • 23 • • controls из andv4 .. z\. В случае с дробным временем мы "растягиваем время" на одном из отрезков кривой. Например, если 0 < t < 1, то мы имеем: subpath (0,£) of р = 2о .. controls t[zo, ио] andt[zo,uo,vi] .. £[20,^0,^1,21]; subpath (£, 1) of p = i[zo,tio, vi,zi] .. controls*[uo,t;i,zi] and£[tn,2i] .. zi. Вместе эти два пути охватывают все точки подпути 'zo .. controls uo and vi .. z\ . Чтобы получить подпуть 'subpath (*1,*г) of p' при 0 < t\ < t2 < 1, METAFONT применяет эту конструкцию дважды, вычисляя подпуть 'subpath (ti/*2,1) of subpath (0, £2) of p'. /gK/gK Операция 'subpath (t\, £2) of p' определяется для любой комбинации времен (*i, £2) JL JL и любого пути р согласно следующим правилам, п = length р. (1) Если ti > t2, то subpath (£1,^2) of р = reverse subpath (t2,ti) of p. Далее мы предполагаем, что ti < £2. (2) Если ti = t2, то subpath (£1,^2) of р = point t\ of p, путь нулевой длины. Далее мы предполагаем, что ti < £2- (3) Если t\ < О и путь р — циклический, то subpath (*1,£г) of р = subpath (ti +n, t2 + n) of p. Если ti < 0 и путь p — не циклический, то subpath (ti, £2) of p = subpath (O, max(0,*2)) of p. Далее мы предполагаем, что t\ > 0. (4) Если ti > п и путь р — циклический, то subpath (£1, £2) of р = subpath (t\ — n, £2 — n) of p. Если £i < n < £2 и путь p — циклический, то subpath (£1,^2) of p = subpath (£1,^2) of (p&cpk,cycle). Если *2 > n и путь p — не циклический, то subpath (£1,^2) of р = subpath (min(£i,n), n) of p. Далее мы предполагаем, что 0 < t\ < £2 < п. (5) Если t\ > 1, то subpath (£1,^2) of р = subpath (t\ — \,t2 — 1) of subpath (1, n) of p, где 'subpath (l,n) of p' — подпуть, полученный удалением из пути р первого участка. Далее мы предполагаем, что 0 < ti < 1. (6) Если t2 > 1, то subpath (£1,^2) of р = subpath (*i,l) of р & subpath (1,*г) of p. Далее мы предполагаем, что 0 < t\ < t2 < 1. (7) Остальные случаи были определены выше. <^/£\* Упражнение 14.14 JL JL Какова длина подпути 'subpath (2.718,3.142) of р'?
146 Глава Ц: Пути f Кроме 'point t of р\ в системе METAFONT имеются операции 'postcontrol t of р' и 'precontrol t of p\ которые обеспечивают непосредственный доступ к контрольным точкам пути. Пусть р = zo .. controlsио andui .. zi (etc.) zn-i .. controls un-i and Если t < n, то 'postcontrol £ of p' — первая контрольная точка подпути 'subpath (£, n) of p'; если же t > n, то 'postcontrol £ of p' есть точка zn. Если t > 0, то 'precontrol £ of p' — последняя контрольная точка подпути 'subpath (0, t) of р'; если же t < О, то 'precontrol t of р' есть точка zq. В частности, если число t — целое, то 'postcontrol t of р' есть точка tit, где 0 < t < n, a 'precontrol t of р' есть точка v*, где 0 < £ < п. f Возможность выделять ключевые и контрольные точки позволяет определить интересные операции, например, функцию interpath из базового формата METAFONT. Эта функция позволяет производить интерполяцию между путями. Так, если заданы два пути р и q длины п, то 'tnterpat/i(l/3,p,g)' даст вам путь длины п, точки которого имеют вид 1/3 [point t of р, point t of q], где 0 < t < п. Эту функцию можно определить при помощи довольно простой программы: vardef interpath (ехрг a,p,q) = for t — 0 upto length p — 1: a [point t of p, point t of q] .. controls a [postcontrol t of p, postcontrol £ of q] and a [precontrol t + 1 of p, precontrol t + 1 of q] .. endfor if cycle p: cycle % предполагаем, что пути p и g либо оба цикличны, % либо оба нецикличны else: a [point infinity of p, point infinity of <j] fi enddef; /gK Четырнадцатого февраля 1979 года автор купил коробку шоколадных конфет и JL поместил ее на лист миллиметровки (предварительно употребив по назначению содержимое). Полученные таким способом экспериментальные данные легли в основу определения следующей фигуры, имеющей форму сердечка: heart = (100,162) .. (140, l7S){right} .. (195, !2b){down} .. (100,0){curl0} .. {up}(5,125) .. {right} {60, US) .. (100,162). Интересно провести интерполяцию между путем heart и другими путями при помощи следующей программы: for п = 0 upto 10: draw interpath(n/10,p, heart)\ endfor. Например, чтобы получить левую из приведенных ниже иллюстраций, мы взяли р = (100,0) -- (300,0) -- (200,0) -- (100,0) -- (0,0) -- (-100,0) -- (100,0). Заметьте, что операция interpath, вообще говоря, не сохраняет гладкость в ключевых точках. Иллюстрацию справа мы получили, продублировав в пути heart точку (100,0) (превратив его, таким образом, в путь длины 7) и взяв р = (100,200) -- (200,200) -- (200,100) -- (200,0) -- (0,0) -- (0,100) -- (0,200) -- (100,200).
Глава Ц: Пути 147 f/])iH того, чтобы определить, в каком направлении происходит движение по пути р в момент времени £, в системе METAFONT базового формата достаточно указать 'direction t of р'. Это — просто сокращенная форма записи последовательности '(postcontrol t of р) — (precontrol t of p)\ Иногда путь может делать очень резкий поворот и не иметь строго определенного направления; в этом случае функция 'direction' возвращает нечто среднее между двумя возможными предельными значениями. Например, приведенный выше путь heart в момент времени 3 делает поворот на некоторый угол; 'direction 3 of heart' оказывается равным (—93.29172,0), но, в то же время 'direction 3— epsilon of heart' равно (-46.64589, -31.63852), a 'direction 3 + epsilon of heart' равно (-46.64589, 31.63852). fH наоборот, METAFONT может вам сказать, в какой момент времени движение по пути происходит в заданном направлении. Вам нужно запросить 'directiontime w of p\ где w — вектор направления, ар — путь. Чтобы лучше понять эту операцию, лучше всего рассмотреть ее на примерах. Поэтому в завершение диалога с компьютером давайте, как и в главе 8, применим METAFONT к файлу 'ехрг'. На этот раз в самом начале, когда METAFONT скажет нам 'gimme an ехрг', мы напечатаем в ответ hide(P3 = (0,0){right}..{up}(1,1)) рЗ и в нашем распоряжении будет новый путь. Вот теперь начинается интересное: Вы печатаете И получаете directiontime right of рЗ 0 directiontime up of p3 1 directiontime down of p3 -1 directiontime (1,1) of p3 0.5 directiontime left of reverse p3 1 direction directiontime (1,2) of p3 of p3 (0.23126,0.46251) directiontime right of subpath(epsilon,l) of p3 0 directiontime right of subpath(2epsilon,l)of p3 -1 directiontime (1,1) of subpath(epsilon,1) of p3 0.49998 direction epsilon of p3 (0.55226,0) direction 2epsilon of p3 (0.55229,0.00003) directiontime dir 30 of p3 0.32925 angle direction 0.32925 of p3 29.99849 angle direction 0.32925+epsilon of p3 30.00081 directionpoint up of p3 (1,1) Заметьте, что если указанное направление не встречается, то операция 'directiontime' возвращает значение —1. В момент времени epsilon путь рЗ еще направлен вправо, но в момент 2epsilon он уже начинает поворачивать вверх. Операция 'directionpoint' аналогична операции 'directiontime', но вместо времени пребывания в точке она возвращает саму точку на пути. Вы печатаете И получаете directiontime up of fullcircle 0 directiontime left of fullcircle 2 directiontime right of fullcircle 6 directiontime (-1,1) of fullcircle 1 directiontime (epsilon,infinity) of fullcircle 8 directiontime right of unitsquare 0
148 Глава Ц: Пути directiontime up of unitsquare 1 directiontime (1,1) of unitsquare 1 directiontime (-1,1) of unitsquare 2 Если движение по пути в указанном направлении происходит неоднократно, то операция 'directiontime' выдает только первое значение времени. Путь unitsquare делает резкие повороты на углах; операция 'directiontime' предполагает, что в этих точках присутствуют одновременно все направления из промежутка между входящим и выходящим направлениями. /$N/$\ Можно построить аномальные пути, которые будут вести себя необычным об- JL JL разом. Например, путь р = (0,0) .. controls (1, l)and(0,1) .. (1,0) в момент времени 0.5 имеет "пик". В этот момент он как бы "замирает" и делает поворот на месте. (Если вы запросите направление 'direction 0.5 of р', то получите ноль, тогда как 'direction 0.5 — е of р' даст (0, 2е), a 'direction 0.5 + е of р' даст (0, —2с).) Операция 'directiontime' предполагает, что в "мертвых точках" присутствуют все возможные направления. Поэтому в данном случае 'directiontime right of р' выдаст значение 0.5, хотя можно показать, что путь р вправо никогда не поворачивает. Пути с пиками нестабильны в том смысле, что в результате трансформирования могут становиться "необычными", поскольку ошибки округления могут изменить их число обходов. В данном примере путь р имеет контрольные точки, соответствующие натяжениям относительно начальной и конечной точек со значениями, равными всего-навсего 0.28. Поскольку система METAFONT требует, чтобы значения натяжения были равны как минимум 0.75, то, если бы мы не задали контрольные точки непосредственно, этот аномальный путь никогда бы не возник. /$K/^s^ Упражнение 14.15 JL JL Напишите макросы с именами posttension и pretension, которые бы определяли эффективное натяжение в контрольных точках пути, соответствующих целым моментам времени t. Например, 'pretension 1 of (zo .. tension a and/? .. zi)' должно (примерно) равняться p. Проверьте ваш макрос, вычислив posttension 0 of ((0,0){right} ... {up}(l, 10)). f Теперь мы уже обсудили почти все, что система METAFONT может делать с путями, осталось рассмотреть только одну важную операцию — пересечение. Если даны два пути — р и д, то вы можете написать р intersectiontimes q и в результате получите пару моментов времени (t,u), такую, что point t of р « point и of q. Вот, например, что получается при использовании программы ехрг: Вы печатаете И получаете unitsquare intersectiontimes fullcircle (0.50002,0) unitsquare intersectiontimes fullcircle rotated 90 (0.50002,6) reverse unitsquare intersectiontimes fullcircle (0.50002,2) fullcircle intersectiontimes unitsquare (0,0.50002) halfcircle rotated 45 intersectiontimes unitsquare (1,3.5) halfcircle rotated 89 intersectiontimes unitsquare (0.02196,3.5) halfcircle rotated 90 intersectiontimes unitsquare (0,3.50002) halfcircle rotated 91 intersectiontimes unitsquare (-1,-1) halfcircle rotated 45 intersectiontimes fullcircle (0,1) fullcircle intersectiontimes (-0.5,0) (4,0) unitsquare intersectionpoint fullcircle (0.5,0) reverse unitsquare intersectionpoint fullcircle (0,0.5)
Глава Ц: Пути 149 Заметим, что когда пути не пересекаются, результат есть (—1,-1). В последних двух примерах иллюстрируется действие оператора 'intersectionpoint', который дает точку пересечения. Обе операции 'intersectiontimes' и 'intersectionpoint' имеют третичный порядок предшествования, поэтому скобки в этих примерах не нужны. f> Упражнение 14.16 Студент Дж.Г.Квик захотел построить путь г, который бы выходил из начальной точки предварительно определенного пути р и двигался по нему вплоть до точки, где тот касается пути q (отличного от р), а затем двигался бы по пути q. Он написал следующее: path г; numeric t, u; (£, и) = р intersectiontimes q\ г = subpath (0, t) of p & subpath (u, infinity) of q\ Почему это не сработало? /gK/$K Если пути пересекаются более одного раза, то система METAFONT решает, какие JL jl значения времени (t,u) выдать в ответ на 'р intersectiontimes q1 несколько своеобразно. Допустим, путь р имеет длину т, а путь q — длину п. (Если какой-либо из путей имеет нулевую длину, то он предварительно заменяется неподвижным путем длины 1.) METAFONT последовательно проверяет подпути вида 'subpath (к, к ■+- 1) of р' на предмет пересечения с подпутями вида 'subpath (/, / + 1) of q' для к = 0, ..., m — 1 и / = 0, ...,п— 1, причем / пробегает все свои значения при каждом к. В результате общая проблема сводится к частному случаю путей длины 1, а значения времени {t,u), соответствующие первому найденному пересечению, прибавляются к (&,/). Но для самих путей длины 1 схема поиска несколько иная: вместо того чтобы выдавать "лексикографически наименьшую" из пар, соответствующих пересечению, METAFONT находит такую пару (t,tt), у которой минимально "бинарно перемешанное" представление (.£ 11x1*2^2 • • • Ь, где (.^1*2 • • • Ь и (.iiiг*2 •. • )г — представления чисел t и и в двоичной системе счисления. <&><$)* Упражнение 14.17 JL JL (Математическая головоломка.) Путь р = (0,0) .. controls (2,2) and (0,1) .. (1,0) образует петлю, поэтому существуют такие значения времени t < и, для которых point t of р и point и of р. Придумайте простой способ нахождения (t,tt) при помощи программы для METAFONT, в которой не использовалась бы операция 'subpath'. /$\ В завершение главы давайте применим то, что мы узнали о путях, к конкретному JL примеру из жизни. На обложке журнала Journal of Algorithms, который выходит с 1980 года в издательстве Academic Press, красуется следующий логотип, разработанный Дж.К.Кнут в соответствии со стилем оформления обложки: При помощи программы для METAFONT, генерирующей этот логотип, редакторы журнала имеют возможность использовать его в своей переписке, помечая им бланки писем. Вот один из способов нарисовать этот символ, ничего при этом не стирая: 1 beginchar("A",29mm#,25mm#,0); thick# := 2гага#; thin* := 5/4гага#; 2 define. whole_blacker_ pixels (thick, thin); 3 forsuffixes $ = a, 6, c: transform $; 4 forsuffixes e = /,r: path $e,$;e; numeric t$[]e; endfor endfor
150 Глава Ц: Пути 5 penpos^thick,0); penpos2{thick,90); penpos3(thick, 180); penposA(thick,270); 6 penposb (tfucfc, 0); penpos6 (^Л»сА;, 90); penpos7 (tfitcfc, 180); penposs (thick, 270); 7 x2 = s4 = x6 = xs = .b[x5,x7] = .5tu; я1г = iu; я3г = 0; x5 - x7 = y6 - y8; 8 2/i = Уз = 2/5 = У7 = .5[t/6,2/s] = .5ft; 2/2r = ft; 2/4r = 0; y6r = -75ft; 9 forsuffixes e = /, r: a.e = 6'e = c'e = superellipse(zie,Z2eiz3e, z4e, .75); Ю a'e = b.e = c.e = superellipse[z^e,ZQej z7eizge, .72); endfor li penposal(thin,0); penposab(whatever, —90); penposa9(£ftm, 180); 12 Жди - Xa9/ = 1/3(25/ - X7l)] Xa5 = .5w, yal = Уа9*, Уа5г = 4/7ft; 13 хаз1 = хац; xazr = ^air5 ^a4r = 1/6[яа3г,xn]; x0 = .5w; y0 = -52ft; 14 XaQi + Xa4/ = Жабг + Sa4r = #a7/ + £a3/ = #a7r + £a3r = Xag + Жа1 = ttf; 15 J/a3r = 2/o4r = Уабг = 2/a7r = -2[У21, Уо]] УаЫ = УаА1 = 2/аб/ = 2/а7/ = 2/аЗг - Л«Л; 16 га4/ = za4r + (tftin, 0) rotated(angle(za4r - *a5r) + 90) 17 + whatever * (za4r - га5г); га4/ - 2a5/ = whatever * (za4r - га5г); 18 z = a.r intersectionpoint (z0 -- (w,0)); yai - уаъ = length(z - z0); 19 6= identity shifted (0,2/o — 2/ai) rotatedaround(zo, 90 — angle(zo - (w,0))); 20 с = 6 reflectedabout (z2, z4); 2i for n = 1,3,4,5,6,7,9: forsuffixes e = /,, r: forsuffixes $ = 6, c: 22 -z$[n]e = £a[n]e transformed $; endfor endfor endfor 23 forsuffixes e = /, r: forsuffixes $ = a, 6, c: 24 2:$2e = $r intersectionpoint (z$le -- 2$3e); 25 z$8e = $r intersectionpoint (z$9e -- z$7e); 26 ^$ie = xpart($e intersectiontimes (z$u -- 2$3/)); 27 t$9e = xpart($e intersectiontimes (z$9j -- ^$7/)); 28 £$4e = xpart($,e intersectiontimes (z$^r --2$4/)); 29 £$6e = xpart($'e intersectiontimes (z$5r -- г$6/)); endfor endfor 30 penstroke subpath(£a96,ttee) of ae\ 3i penstroke subpath(£64e,£c4e) of 6'e; 32 penstroke subpath(£C6e, ta\e + 8) of c'e; 33 penstroke subpath(£a6e5 *69e) of a'e; 34 penstroke subpath(£bie,£cie) of b.e; 35 penstroke subpath(tcge, ta4e + 8) of c.e; 36 forsuffixes $ = a, 6, c: penlabels($l, $2, $3, $4, $5, $6, $7, $8, $9); 37 penstroke z$2e - - <z$3e - - 2$4e - - *$5e - - 2$6e - - z$7e - - 2$se; endfor 38 penlabels(range 0 thru 8); endchar; В строках 5-10 этой программы определяется главный деформированный эллипс этой фигуры. Внешний деформированный эллипс рисуется как три отдельных линии в строках 30-32, а внутренний рисуется в строках 33-35, также как три отдельных линии. Оставшаяся часть фигуры состоит из трех стрелок, точки которых помечены метками, начинающимися, соответственно, с букв a, b и с. В строках 11-18 определяются ключевые точки стрелки 'а'; затем, в строках 19-22, эти точки трансформируются в ключевые точки стрелок Ъ' и 'с'. В этих трансформациях мы, несколько забегая вперед, используем возможности, которые будут рассмотрены в главе 15. В строках 23-29 вычисляются тридцать шесть точек, в которых стрелки пересекаются с деформированными эллипсами. И наконец, при помощи команд penstroke в строках 36-37 рисуются сами стрелки.
Глава Ц: Пути 151 Маршрут показан точками, однодневные переходы обозначены числами, а буквы указывают на места, заслуживающие внимания, и достопримечательности. ... Мы подошли к Арройо де Сан Франциско — речке, возле которой растет красное дерево, о котором я рассказывал вчера. Я измерил его высоту при помощи графометра, и, по моим подсчетам, она составила приблизительно пятьдесят ярдов. — ФРЕЙ ПЕДРО ФОНТ, Дневник (1776) В букве О Джотто собрано все то, чему учили мастера изобразительного искусства. — ДЖОН РАСКИН, Пояс Аглаи (1865)
15 Трансформации
Глава 15: Трансформации 153 Точки, пути, перья и рисунки можно сдвигать, масштабировать, вращать и перекраивать множеством способов. Наша цель в этой главе — изучить все виды встроенных трансформаций системы METAFONT, поскольку они упрощают программы и делают их более многогранными. Давайте для начала перечислим простейшие трансформации, хотя они уже не раз появлялись в наших примерах: (х, у) shifted (о, Ь) — (х + а, у + 6); (х,у) scaled s = (sx,sy); (х,у) xscaled s = (sx,t/); (x, у) yscaled s = (x,st/); (x,y) slanted s = (x + sy,y)\ (x, y) rotated в — (x cos в — у sin 0, x sin в 4- у cos в); (x, у) zscaled (u, v) = (xu — yv, xv + yu). Одно из приятных свойств системы METAFONT состоит в том, что вам не нужно запоминать тригонометрические формулы — все эти синусы и косинусы; все, что вам нужно знать, — это то, что '(x,t/) rotated в1 значит: "вектор (х,у), повернутый вокруг точки (0,0) против часовой стрелки на угол 0". Компьютер сам выполнит все необходимые вычисления. Операция 'zscaling' может показаться вам необычной, но это всего лишь комбинация вращения на угол 'angle (u, v)' и масштабирования на величину 'length (u, v)\ В базовом формате METAFONT определены еще две простые трансформации, потребность в которых возникает довольно часто. Если необходимо произвести вращение вокруг точки zo, отличной от (0,0), то достаточно указать '(х,у) rotatedaround (zq,0)\ Если же нужно отразить точку (х,у) относительно прямой, проходящей через точки z\ и 22, то достаточно указать '(х, у) reflectedabout (21,22)'. Все эти операции являются частными случаями одной мощной операции, которая в общем виде записывается следующим образом: (x,t/) transformed t. Здесь t — переменная (или первичная формула) типа transform (преобразование); на месте t может стоять произвольная последовательность сдвигов, масштабирований, наклонов и т.д. В программах для METAFONT преобразования можно связывать между собой уравнениями, точно так же, как и объекты других типов. Так, например, вы можете указать transform t[]; £2 = h shifted (2,2) rotated 30; тогда выражение вида '(х,у) transformed £1 shifted (2,2) rotated 30' сократится до более простого и компактного выражения '(х,у) transformed ^2'- Существует особое преобразование identity, который обладает тем удивительным свойством, что (х,у) transformed identity = (х,у) для любых х и у. Может показаться, что преобразование identity бесполезно, так как ничего не делает. На самом деле оно является основой для построения других
154 Глава 15: Трансформации преобразований. Например, в строке 19 программы, приведенной в конце предыдущей главы, имеется утверждение Ь = identity shifted (0, уо - Уа\) rotatedaround(zo, theta); т.е. Ь определяется как сложное преобразование, который используется затем в строках 21 и 22 программы для построения левой нижней стрелки изображаемого символа посредством сдвига и вращения его верхней стрелки. /gK Переменная t типа transform (преобразование) представляет собой набор шести JL чисел {tx, ty, tхх, txy, tyx,tyy), точно так же, как переменная типа pair представляет собой пару чисел (х,у). Трансформация общего вида '(х,у) transformed V есть просто сокращенная форма записи для \tx ~Г X txx "г У txy) *у "Г X tyx ~Г У *уу)'ч Таким образом, например, '£Ху' входит в иксовую компоненту как коэффициент при у. Если t — совершенно неизвестное преобразование и вы укажете 'show t\ то компьютер выдаст » (xpart t,ypart t,xxpart t,xypart t,yxpart t,yypart t) точно так же, как в случае совершенно неизвестной переменной и типа pair он печатает '» (xpart u,ypart и)'. Вы можете оперировать отдельными компонентами преобразований, обращаясь к ним как к 'xpart V, 'ypart t\ 'xxpart t\ и т.д. /$\ Чтобы лучше в этом разобраться, проведем еще одну серию экспериментов с JL файлом ехрг (см. главу 8); на этот раз мы поиграем с преобразованиями: Вы печатаете И получаете identity (0,0,1,0,0,1) identity shifted (a,b) (a,b,1,0,0,1) identity scaled s (0,0,s,0,0,s) identity xscaled s (0,0,s,0,0,1) identity yscaled s (0,0,1,0,0,s) identity slanted s (0,0,l,s,0,l) identity rotated 90 (0,0,0,-1,1,0) identity rotated 30 (0,0,0.86603,-0.5,0.5,0.86603) identity rotatedaround ((2,3),90) (5,1,0,-1,1,0) (x,y) rotatedaround ((2,3),90) (-y+5,x+l) (x,y) reflectedabout ((0,0),(0,1)) (~x,y) (x,y) reflectedabout ((0,0),(1,1)) (y,x) (x,y) reflectedabout ((5,0),(0,10)) (-0.8y-0.6x+8,0.6y-0.8x+4) f> Упражнение 15.1 Угадайте результат трансформации '(х,у) reflectedabout ((0,0), (1,0))'. f* Упражнение 15.2 Какая трансформация переводит (х,у) в (—х, — у)? f> Упражнение 15.3 Верно ли, что (—(х,у)) transformed t = —((х,у) transformed i)l
Глава 15: Трансформации 155 /$N Для того чтобы иметь в распоряжении несколько переменных-преобразований, X перед тем, как перейти к новым экспериментам, необходимо ввести яри помощи команды 'hide' несколько команд и объявлений: Вы печатаете И получаете hide(transform t[]) tl (xpart tl,ypart tl.xxpart...) hide(tl«identity zscaled(l,2)) tl (0,0,1,-2,2,1) hide(t2»tl shifted (1,2)) t2 (1,2,1,-2,2,1) t2 xscaled s (s,2,s,-2s,2,1) unknown t2 false transform t2 true tl=t2 false tl<t2 true inverse t2 (-1,0,0.2,0.4,-0.4,0.2) inverse t2 transformed t2 (0,0,0.99998,0,0,0.99998) hide(t3 transformed t2=identity) t3 (-1,0,0.2,0.4,-0.4,0.2) Функция inverse отыскивает преобразование с действием, обратным действию другого преобразования. Из приведенного выше уравнения, в котором определяется £з, видно, как можно найти обратное преобразование, не прибегая к inverse. Выражения-преобразования, как и числовые выражения или выражения-пары, могут быть либо "известными", либо "неизвестными" в любой, произвольно выбранной, точке программы. (Если не известна ни одна из компонент преобразования, то считается неизвестным все преобразование.) Вы всегда можете использовать конструкции вида (известное) transformed (известное) (неизвестное) transformed (известное) (известное) transformed (неизвестное) Но конструкции вида '(неизвестное) transformed (неизвестное)' METAFONT не воспринимает. Данное правило можно и ослабить, но в таком виде его проще запомнить. /$\ ►Упражнение 15.4 JL Если zi и Z2 — неизвестные пары, то вы не можете указать czi shifted 22'. Как же правильно проделать эту операцию? ^\ ►Упражнение 15.5 Допустим, dbend — это переменная-рисунок, содержащая обычный знак опасного поворота, как в примере с "инвертированием цветов" из главы 13. Что нужно с ней сделать, чтобы получить "левосторонний" знак опасного поворота, которым помечен этот абзац? fB следующих трех строчках иллюстрируется тот факт, что преобразование полностью определяется заданием образов трех точек: Вы печатаете И получаете hide((0,0)transformed t4=(l,2)) t4 (l,2,xxpart t4,xypart t4,...) hide((1,0)transformed t4=(4,5)) t4 (l,2,3,xypart t4,3,yypart t4) hide((1,4)transformed t4=(0,0)) t4 (1,2,3,-1,3,-1.25) Точки, определяющие преобразование, не должны все лежать на одной прямой. ■£\ Давайте построим с помощью трансформаций небольшой орнамент следующего вида: G©QG©0GQQQQQ©£3£3£3£3£3 , в основе которого будет лежать фигура, состоящая из четырех элементов вида *о\ $ *
156 Глава 15: Трансформации Следующая программа заслуживает тщательного изучения: 1 beginchar(n4",llp*# 11р*# 0); 2 pickup pencircle scaled 3/4pt yscaled 1/3 rotated 30; 3 transform t\ 4 t = identity rotatedaround((.5iu, .5/i), —90); 5 X2 = .35u>; xz = бги; 6 t/2 = -1Л; top y3 = Ah; 7 path p; p = z2 {right} .. .{up} Z3', 8 top zi = point .5 of p transformed i; 9 draw Zi . .. p; 10 addto currentpicture also currentpicture transformed i; li addto currentpicture also currentpicture transformed (t transformed £); 12 labels(l,2,3); endchar; В строках 3 и 4 вычисляется преобразование, которое переносит элемент 'и' по часовой стрелке в положение ближайшего соседа. В строках 5-7 определяется правая половинка элемента 'и'. Строка 8 представляет наибольший интерес: в ней z\ определяется как точка на повернутом пути. В строке 9 рисуется элемент 'о7, который затем в строке 10 заменяется двумя, а в строке 11 эти два элемента заменяются четырьмя. Скобки в строке 11 можно и опустить, но трансформации преобразований выполняются гораздо быстрее, чем трансформации рисунков. /£n/£\ Система METAFONT трансформирует выражение-рисунок только в том случае, JL JL если txx, txy, tyx и tyy — целые числа и либо txy = tyx = 0, либо txx = tyy = 0, кроме того, значения tx и ty она округляет до ближайших целых. Иначе границы пикселей не будут переходить в границы пикселей. f> Упражнение 15.6 Объясните, как развернуть наш узор на 45° : ффффффффффф В базовом формате METAFONT имеется специальная переменная-преобразование currenttransform, которая обычно остается "за кулисами". Эта невидимая переменная используется всякий раз, когда выполняются команды fill или draw;
Глава 15: Трансформации 157 например, при выполнении команды 'fill р' на самом деле заливается внутренняя область не самого р, а пути вида р transformed currenttransform Мы не упоминали об этом ранее, поскольку currenttransform обычно равно identity. Однако в редких случаях, когда требуется получить какой-нибудь необычный эффект, для этого можно использовать нестандартные значения currenttransform. Например, можно превратить 'METflFQNT' в lMETAFONT\ просто указав currenttransform := identity slanted 1/4 и выполнив программы из файла logo.mf, описанного в главе 11; больше никаких изменений в эти программы вносить не нужно. Следует отметить, что при изменении currenttransform кончик пера, при помощи которого рисовался логотип 'METTRFONT1, не наклонялся — изменялись только "дорожки", по которым двигалось перо, то есть пути в командах draw. Таким образом, наклонное начертание не было получено из обычного при помощи простого наклона. /$\/£\ При разработке шрифтов для устройств с пикселями неквадратной формы в ба- JL JL зовом формате METAFONT значение переменной currenttransform устанавливается равным 'identity yscaled aspecLratio\ а команда pickup на ту же величину масштабирует по игрек-направлению кончики используемых перьев. В этом случае буквы наклонного логотипа ' METAFONT' должны изображаться при значении currenttransform, равном currenttransform := identity slanted 1/4 yscaled aspect-ratio. A A^ Упражнение 15.7 JL JL Если пиксели не квадратные, то наша программа для ' £3' не срабатывает. Исправьте ее так, чтобы величина aspect-ratio присутствовала в ней как свободный параметр. Перемены порождают перемены. Ничто не множится столь быстро. — ЧАРЛЬЗ ДИККЕНС, Мартин Чезлвит (1843) Некоторым никогда и в голову не придет, как можно что-то изменить. — МАРК ТВЕН, Личные воспоминания о Жанне д'Арк (1896)
16 Элементы каллиграфии
Глава 16: Элементы каллиграфии 159 В главе 4 мы представили вам перья и, прежде чем пролить еще хоть каплю чернил, просто обязаны рассказать, что же система METAFONT может с этими перьями делать. В этой главе мы изучим возможности изображения фигур при помощи перьев с "фиксированным" кончиком, то есть переменных и выражений типа реп, без использования в том или ином виде операции заливки контура. Когда указываете 'pickup (выражение-перо)', макросы базового формата METAFONT выполняют несколько операций. Во-первых, они генерируют представление пера с заданным кончиком и присваивают это значение переменной-перу currentpen. Затем они отдельно сохраняют информацию о верхнем, нижнем, левом и правом краях пера, чтобы впоследствии использовать ее в операциях top, bot, Ift и rt. Перо currentpen будет использоваться командами draw, drawdot и filldraw для изменения текущего рисунка. Вы можете также сказать 'pickup (числовое выражение)'. В этом случае заданное выражение обозначает кодовое число ранее выбранного пера, которое было сохранено командой 'savepen'. Например, файл logo.mf из главы 11 начинается с выбора пера, используемого для рисования букв логотипа 'METAFONT', после чего указывается 'logo.реп := savepen\ Далее в этом файле каждая программа для отдельного символа начинается с команды 'pickup logo.pen\ Эта операция выполняется быстро, так как компьютеру не нужно генерировать в памяти новое представление пера. f Предостережение: Каждый раз, когда выполняется команда savepen, система METAFONT генерирует в памяти новую целочисленную величину и припрятывает на будущее еще одно перо. Если делать это постоянно, то память очень быстро окажется засоренной представлениями перьев, которые, возможно, вам больше никогда не понадобятся. Чтобы система METAFONT могла начать работу "с чистого листа", в ней предусмотрена команда 'clear-pen.memory', которая стирает из памяти все ранее сохраненные перья. f4TO же такое (выражение-перо)? Хороший вопрос! До сих пор в этой книге мы почти всегда выбирали перо pencircle, за которым следовала какая-то последовательность трансформаций. Например, перо logo-pen, упоминавшееся в главе 11, имело вид: 'pencircle xscaled рх yscaled ру\ В главе 13 мы вскользь упомянули перо другого типа, указав pickup penrazor scaled 10. Тем самым мы дали команду выбрать бесконечно тонкое перо, которое включает в себя точки с координатами относительно его центра от (—.5,0) до (.5,0). Чуть позже в этой главе мы будем использовать перья следующего типа: pensquare xscaled 30 yscaled 3 rotated 30; Это перо ограничено прямоугольником размера 30 пикселей х 3 пикселя и наклонено к основной линии под углом 30°. fBbi можете определить перо с кончиком в виде произвольного выпуклого многоугольника. Для этого достаточно указать 'makepen р\ где р — циклический путь. Оказывается, в данном случае для METAFONT важны лишь ключевые точки пути р, а не его контрольные точки, и поэтому можно считать, что путь р имеет вид 1zq - - zi - - (и т.д.) - - cycle'. Если этот путь содержит более трех ключевых точек, то он непременно должен в каждой ключевой точке делать поворот влево (т.е. для любого к точка Zk+i должна лежать слева от прямой, проходящей через точки Zk-i и Zk). Кроме того, число оборотов этого пути должно быть равно 1 (т.е. он должен делать не более одного полного оборота против часовой стрелки). Команда penrazor, определенная в базовом формате METAFONT,
160 Глава 16: Элементы каллиграфии означает 'makepen ((—.5,0) -- (.5,0) -- cycle)\ a pensquare есть сокращение от 'makepen (unitsquare shifted —(.5, .5))'. А вот 'pencircle' не определяется через makepen. Это перо является одним из примитивов системы и представляет собой правильную окружность единичного диаметра, проходящую через точки (±.5,0) и (0,±.5). Действий, которые можно совершать с перьями, не так уж много, поэтому и полный перечень правил синтаксиса для выражений этого типа довольно краток. Но здесь вас ожидает сюрприз: (первичное перо) —> (переменная-перо) | ((выражение-перо) ) | nullpen (первичная заготовка пера) —> pencircle | makepen (первичный путь) (вторичное перо) —> (первичное перо) (вторичная заготовка пера) —> (первичная заготовка пера) | (вторичная заготовка пера) (преобразователь) | (вторичное перо) (преобразователь) (третичное перо) —У (вторичное перо) | (вторичная заготовка пера) (выражение-перо) —> (третичное перо) Константа nullpen — это всего лишь точка (0,0); она невидима, если только вы не используете ее в команде filldraw, которая в этом случае сводится к fill. (Для того чтобы уменьшить число потенциально опасных зависимостей между программами для различных символов, команда beginchar устанавливает начальное значение переменной currentpen равным nullpen.) Обещанный сюрприз — понятие "заготовки пера". Оно представляет собой путь или эллипс, который еще не был переведен во внутреннее представление системы METAFONT, соответствующее "готовому перу". Этот перевод представляет собой довольно сложный процесс, и METAFONT откладывает его до тех пор, пока не убедится, что больше никаких трансформаций пера не ожидается. Готовое перо формируется на третичном уровне, где синтаксис уже не допускает присутствия заготовок перьев. Для обычного пользователя разница между готовыми перьями их заготовками была бы вовсе неощутима, если ли бы не еще один удивительный факт: все перья системы METAFONT, даже те, которые получены из пера pencircle и производных от него, представляют собой выпуклые многоугольники! Таким образом, например, если вы зададите перо с кончиком в форме ромба вида makepen ((.5,0) -- (0, .5) -- (-.5,0) -- (0, -.5) --cycle). то оно ничем не будет отличаться от пера, полученного из готового нетрансформированного пера pencircle. А перья 'pencircle scaled 20' и 'pencircle xscaled 30 yscaled 20' — это многоугольники с числом сторон 32 и 40, соответственно:
Глава 16: Элементы каллиграфии 161 На этой иллюстрации вершины многоугольников помечены жирными точками; все они имеют "полуцелые" координаты — то есть каждая координата есть либо целое число, либо целое плюс 1/2. Всякий многоугольник, обязанный своим происхождением перу pencircle, симметричен относительно вращения на 180°. Кроме того, если заготовка пера имела форму круга или эллипса и не вращалась, то полученное из нее перо будет обладать вертикальной и горизонтальной симметрией. f Замена идеальных фигур многоугольниками объясняет, почему следует различать заготовки перьев и готовые перья. Например, введение дополнительных скобок в '(pencircle xscaled 30) yscaled 20', приведет к результату совершенно не похожему на эллиптический многоугольник, который мы только что рассмотрели. Эти скобки приведут к превращению 'pencircle xscaled 30' из заготовки пера в готовое перо, и соответствующий многоугольник будет иметь вид (12.5,-0.5)--(15,0)--(12.5,0.5) -- (-12.5,0.5) -- (-15,0) -- (-12.5, -0.5) --cycle, то есть, будет аппроксимацией эллипса размером 30 х 1. Последующее масштабирование 'yscaled 20' дает многоугольник вида f Почему же METAFONT предпочитает иметь дело не с правильными окружностями, а с аппроксимирующими их многоугольниками? Тоже хороший вопрос. Главная причина в том, что подходящим образом выбранные многоугольники дают лучшие результаты по сравнению с точными объектами, если иметь в виду последующую оцифровку. Допустим, требуется нарисовать прямую линию с тангенсом угла наклона равным 1/2 и толщиной в один пиксель, проходящую от точки с координатами (0, у) до точки с координатами (200, у + 100). Если сделать это при помощи идеально круглого пера единичного диаметра, прогнав его вдоль этой линии, то контур полученного следа будет пробегать точки от (0, у ± а) до (200, у 4- 100 ± а), где а = \/5/4 « 0.559. Оцифровав этот контур и произведя заливку ограничеваемой им области, мы обнаружим, что при одних значениях у (например, при у = 0.1) результатом будет повторяющийся массив пикселей вида ' **** " '', а при других (например, при у = 0.3) этот повторяющийся массив будет в полтора раза (на 50 процентов) насыщенней: '... Jp*^ '• Точно так же, при использовании идеально круглого пера, одни наклонные линии с тангенсом угла наклона равным 1 оказываются вдвое толще, чем другие. Но перо с кончиком в форме ромба, которым METAFONT заменяет круглое перо единичного диаметра, лишено этого недостатка; все нарисованные им прямые линии с одинаковым углом наклона после оцифровки будут иметь одинаковую толщину. Более того, если кривая нарисована ромбообразным пером, то на тех участках, где она проходит более или менее горизонтально (с тангенсом угла наклона, лежащим между -f 1 и —1), в каждый столбец всегда попадает ровно один пиксель, а там, где она проходит вертикально, ровно один пиксель попадает в каждую строку. Если же рисовать кривые круглыми перьями, то на их контурах местами образуются жирные "кляксы". Эллипсы и круги произвольных размеров можно с успехом заменять многоугольниками,
162 Глава 16: Элементы каллиграфии которые, будучи подправлены до идеальной формы кусочками пикселей, лучше поддаются оцифровке. METAFONT проделывает это согласно интересной теории, развитой Джоном Хобби (John D. Hobby) в его докторской диссертации (Станфордский университет, 1985). Контур пера, двигающегося вдоль заданной линии, гораздо проще вычислить, если перо не идеально круглое, а имеет форму многоугольника. Поэтому многоугольники побеждают в конкурентной борьбе и по качеству, и по скорости. Когда движение вдоль кривой происходит в направлении, лежащем между краевыми векторами пера- многоугольника Zk+i — Zk и Zk —Zk-v, контур кривой оказывается смещенным относительно ее центра на Zk- Если вам понадобится строго контролировать процесс рисования кривой, то для этой цели в METAFONT предусмотрена примитивная операция 'penoffset ги of р\ где w — вектор, ар — путь. Если ги = (0,0), то результатом будет точка (0,0); если w лежит строго между Zk+i — Zk и Zk — Zk-i, то результатом будет Zk\ а если w совпадает с zjt+i — Zk при некотором А;, то результатом будет либо Zk, либо Zk+i, смотря по тому, какую из этих точек системе METAFONT легче вычислить. ► Упражнение 16.1 Как с помощью операции 'penoffset' найти "верхнюю" точку (или точки) пера, то есть, точку (или точки) с наибольшей координатой у? Примитивная операция 'makepath р\ где р — перо (с кончиком в форме многоугольника) с вершинами zo, 21, ..., 2n-i, дает путь вида '«го .. controls zo and 21 .. zi .. (и т.д.) .. zn-i •• controls 2n_i and zo .. cycle', то есть один из путей, порождающих перо р. Благодаря этому мы получаем доступ ко всем граничным точкам пера. Любая из операций, рассмотренных в главе 15, трансформирует перо pencircle в некоторый эллипс, так как в системе METAFONT все трансформации сохраняют свойство эллипсообразности. Перед превращением эллипса в многоугольник его диаметр по каждому направлению в уменьшается на текущее значение fillin, умноженное на 2min(|sin0|, |cos#|). Это позволяет компенсировать разницу между толщиной диагональных линий и линий, проходящих горизонтально или вертикально, присущую некоторым устройствам вывода. (METAFONT использует величину fillin только при построении многоугольников из эллипсов, но пользователи, конечно же, могут использовать ее в собственных подпрограммах с использованием перьев.) Полученный в результате многоугольник никогда не будет абсолютно плоским*, как, например, penrazor, даже если вы укажете 'xscaled 0' и/или 'yscaled 0'; его центр всегда будет окружен, по меньшей мере, элементарным ромбом, соответствующим кругу единичного диаметра. ► Упражнение 16.2 Запустите METAFONT с файлом ехрг из главы 8 и посмотрите, что компьютер выдаст в ответ на 'pencircle' и 'pencircle scaled 1.1'. (Вы увидите, что в первом случае это будет ромбообразный кончик, а во втором — многоугольник, эквивалентный pensquare.) Продолжайте экспериментировать до тех пор, пока не отыщете "критическое значение" диаметра при котором происходит переход от одного многоугольника к другому. Перья системы METAFONT с кончиками в форме многоугольников как. нельзя лучше подходят для рисования прямых и кривых линий. Но этот приятный факт имеет одно неприятное следствие: они не всегда хорошо поддаются оцифровке в крайних точках, где линия начинается или заканчивается. Причина этого исследуется ниже в главе 24; вершины многоугольника, дающего аккуратные линии равномерной толщины, могут быть "неоднозначно определенными" точками, что вызывает трудности с округлением их координат при растрировании. Поэтому для рисования путей, состоящих из одной только точки, предусмотрена специальная программа drawdot. Иногда имеет ф$ * Имеется в виду, что многоугольник никогда не будет вырождаться в отрезок; плоским в математическом смысле он, конечно же, будет. (Прим. перев.)
Глава 16: Элементы каллиграфии 163 смысл применить команду drawdot к первой и последней точкам пути р после того, как вы указали 'draw р'; в результате, точки на концах, возможно, станут чуть жирнее и за счет этого будут выглядеть более согласованно. f Кроме того, в базовом формате METAFONT имеются две программы, которые позволяют "подчистить" концы линий двумя различным способами. Команда 'cutoff (z,6y удаляет в точке z половину следа пера currentpen, а именно все точки этого пера, лежащие в направлениях между (9 — 90)° и (9 -f 90)° от его центральной точки. А команда 'cutdraw р' есть сокращенная форма записи следующих трех команд: draw р; cutoff (point 0 of р, 180 + angle direction 0 of p); cutoff (point infinity ofp, angle direction infinity of p). В результате будет изображена кривая, концы которой обрезаны под прямым углом к начальному и конечному направлениям. Например, команда cutdraw za .. controls z\ and 22 .. ze рисует следующую кривую, для которой напрашивается сравнение с аналогичной необре- занной кривой, приведенной в конце главы 3: fBoT еще один пример использования операции cutoff, в котором концы буквы 'Т' из логотипа 'METAFONT' срезаны под углом 10° относительно перпендикуляра к направлению линии: pickup logo_pen; top 1ft zl=(0,h); top rt z2=(w,h); top z3=(.5w,h); z4=(.5w,0); draw zl—z2; cutoff(zl,170); cutoff(z2,-10); draw z3~z4; cutoff (z4,-80) . &)<£) В определение макроса cutoff (см. приложение В) входят некоторые недавно изу- JL JL ченные нами объекты. Поэтому будет полезно рассмотреть его здесь в несколько упрощенном виде: def cutoff (ехрг z, theta) = eutopic := nullpicture; addto eutopic doublepath z withpen currentpen; addto eutopic contour ((0, —1) -- (1, —1) -- (1,1) -- (0,1) -- cycle) scaled 1.42(1 + max(—pen Aft^penjrt, pen-top, —penJbot)) rotated theta shifted z\ cull cut-pic keeping (2,2) withweight —1; addto currentpicture also cut-pic enddef. Основная часть работы выполняется отдельно в переменной eutopic типа picture так, чтобы соседние линии при этом не затрагивались. Сначала значение cut-pic устанавливается равным целому оцифрованному следу пера (посредством применения команды doublepath к одной единственной точке). Затем к ней прибавляется прямоугольник, ограничивающий область обрезки; величины penJft, pen.rt, репЛор и pen.bot входят в
164 Глава 16: Элементы каллиграфии определение функций Ift, r£, top и bot, поэтому они в точности выражают размеры пера. Операция подравнивания cull дает пересечение пера с прямоугольником. Наконец, это пересечение вычитается из рисунка currentpicture. /$\/g\ В завершение главы мы рассмотрим на двух примерах то, какие интересные воз- JL JL можности открываются при комбинировании различных средств системы METR- FONT, предназначенных для управления перьями и рисования кривых. Вначале давайте внимательно присмотримся к следующим двум символам "тильда": Оба они нарисованы при помощи одной команды вида draw z\ .. controls^ andzz .. z\. Символ слева получен при помощи пера 'pencircle xscaled £pt yscaled .2pt rotated 50', а символ справа — при помощи такого же пера, но с заменой pencircle на pensquare. Контрольные точки Z2 и 23, определяющие начертание, были заданы уравнениями 2/2-2/1=У4~2/з = 3(2/4 - 2/i); Z2 — z\ = z\ — zz = whatever * dir 50. Вторая пара уравнений — это старый каллиграффический прием, который состоит в том, чтобы, начиная и заканчивая линию, вести перо в направлении, указываемом его ручкой. Первая пара уравнений — это прием математический, основанный на том, что полином Бернштейна £[0,3, — 2,1] принимает значения от 0 до 1, от 1 до 0, и от 0 до 1, в то время как t меняется от 0 до .25, от .25 до .75, и от .75 до 1. /$\/£\ Теперь попробуем нарисовать оригинальную засечку, используя те же самые два пера, но установив их под углом не 20°, а 50°. Вот два примера которые могут быть получены при помощи команд 'filldraw': filldraw z\ .. controls 22 .. 23 -- (flex(z3, .5(2:3, 24] + dishing, 24)) shifted (0, —epsilon) - - z\ .. controls z$ .. Zb - - cycle. Параметр dishing вызывает небольшой подъем на участке между гз и 24; весь flex сдвинут вниз на величину epsilon, чтобы избежать образования "необычного пути", который мог бы возникнуть из-за крошечных петелек в точках zz и z^. Но самое интересное в этом примере — использование на двух отрезках пути двойных контрольных точек — гг и Z5. (Напомним, что 'controls z^ означает то же, что и 'controls^ and Z2') Эти точки определяются уравнениями Х2 = xi\ Z2 = zz + whatever * dir 20; хъ = Хб; Zb = 24 4- whatever * dir —20;
Глава 16: Элементы каллиграфии 165 Таким образом, соответствующие линии идут вертикально в точках z\ и zq, вдоль угла наклона пера в точке 2з, и вдоль угла, дополнительного к углу наклона пера, в точке z\. Вероятно, перо в большей степени, чем любой другой инструмент, повлияло на начертание букв с засечками на концах ... Скорее всего, буквы [на колонне Траяна] были сначала нарисованы, а затем уже высечены. И хотя их общая структура приписывается перу, а окончательный вид — технике резца, свободой своих форм они, несомненно, обязаны кисти. — Л. К. ЭВЕТС, Латинская письменность (1938) Помните: чтобы овладеть искусством или ремеслом, необходимы время, терпение и глубокое изучение теории и практики. Как бы вы ни старались, экспериментируя с отточенным пером, мастерство не придет к вам за несколько часов работы, пусть даже очень напряженной. Запаситесь временем и терпением. Если у вас уйдет на это месяц, вы должны быть рады, что потребовался всего один месяц. — ЛЛОЙД РЕЙНОЛДС, Искусство каллиграфии (1969)
Группирование
Глава 17: Группирование На данный момент мы уже успели рассмотреть все визуально-графические аспекты системы METAFONT — точки, пути, перья и рисунки; но нам по-прежнему ничего не известно о ее организационно-административных аспектах — программах. Поэтому следующие несколько глав книги мы целиком посвятим тому, как эффективно составлять программы. Программа для METAFONT — это последовательность утверждений, разделенных точками с запятой, которая заканчивается словом 'end'. Точнее, понятие (программа) определяется в правилах синтаксиса через понятие (утверждение): (программа) —> (список утверждений) end (список утверждений) —> (пустое утверждение) | (утверждение) ; (список утверждений) Но что такое "утверждение"? Утверждения бывают разных видов. Утверждение типа "уравнение" устанавливает равенство двух выражений. "Присваивание" приписывает переменной значение определенного выражения. В "объявлении" утверждается, что данная переменная отныне будет иметь указанный тип. "Определение" задает макрос. "Заголовок" дает описательное имя следующему символу. "Команда" — это приказ системе METAFONT немедленно выполнить конкретную операцию. "Пустое утверждение" дает системе распоряжение не делать абсолютно ничего. И наконец, "сложносоставленное утверждение" — это список других утверждений, рассматриваемый как группа. (утверждение) —> (уравнение) | (присваивание) | (объявление) | (определение) | (заголовок) | (команда) | (пустое утверждение) | begingroup (список утверждений) (утверждение) endgroup В главе 10 мы приводили правила синтаксиса для утверждений типов (уравнение) и (присваивание); синтаксис для утверждений типа (объявление) был приведен в главе 7; утверждения типов (определение), (заголовок) и (команда) мы рассмотрим в последующих главах. А сейчас подробно остановимся на утверждениях последнего из вышеперечисленных типов, где лексемы begingroup и endgroup связывают в самостоятельную единицу другие утверждения. Они действуют точь-в-точь, как скобки в алгебраическом выражении, разбивающие его на структурные элементы. Основная цель группирования состоит в том, чтобы защитить значения переменных в одной части программы от изменений, которым они подвергаются в других ее частях. Внутри группы символьной лексеме можно придать новый смысл, не меняя ее значения вне группы. (Напомним, что METAFONT работает с тремя основными видами лексем, как говорилось в главе 6; смысл числовой лексемы или лексемы-цепочки изменить невозможно, но смысл символьной лексемы может меняться произвольным образом.) Есть два способа защиты значений переменных в группе. Первый называется (команда save), а второй — (команда interim): (команда save) —> save (список символьных лексем) (список символьных лексем) —> (символьная лексема) | (список символьных лексем) , (символьная лексема) (команда interim) —> interim (внутренняя величина) : = (правая сторона) В команде save символьные лексемы теряют свои текущие значения, но эти старые значения бережно сохраняются и восстанавливаются по окончании текущей группы.
168 Глава 17: Группирование Все лексемы становятся неопределенными, как будто прежде они нигде не встречались. Например, команда save х, у фактически перекрывает доступ ко всем прежде известным переменным типа х\ и 2/5г5 переменная х\ теперь может участвовать в новом уравнении, где она может действовать совершенно независимо от того, какое значение она имела за пределами группы. Вы можете дать следующую глупую команду: save save. В результате, лексема 'save' превращается из (вызова) в (ярлык), и поэтому вы не сможете использовать save как команду вплоть до окончания группы. /gK Действие команды interim более ограничено по сравнению с save, поскольку JL она применяется только к (внутренним величинам). (Напомним, внутренние величины — это особые переменные, такие как tracingequations, которые принимают только числовые значения; с полным списком всех стандартных внутренних величин вы можете ознакомиться в главе 25, хотя этим списком они не исчерпываются, поскольку вы можете сами определять новые внутренние величины.) METAFONT рассматривает команду interim как обычное присваивание, с той лишь разницей, что по окончании группы переменной присваивается ее прежнее значение. fEom значение какой-либо величины сохраняется в пределах одной группы неоднократно, то первое из сохраненных значений имеет приоритет над остальными. Например, величины autorounding и я в конструкции begingroup interim autorounding := 0; save x\ interim autorounding := 1; save x; endgroup по окончании группы примут те значения, которые они имели до утверждения 'interim autorounding := 0'. (Кстати, эти значения не всегда будут совпадать с теми значениями, которые они имели до начала группы.) /gK Лексемы и внутренние величины с окончанием группы приобретают прежний JL смысл только в том случае, если они были сохранены командами save или interim. Все прочие изменения их смысла и/или значения остаются в силе и за пределами группы. /gK Команда beginchar, определенная в базовом формате METAFONT, включает лек- JL сему begingroup, а команда endchar — лексему endgroup. Таким образом, утверждения с использованием команды interim в программе для одного символа никак не влияют на программы для других символов. fEcnn (команда save) применяется вне какой-либо группы, то она просто обессмысливает все указанные в ней символьные лексемы; их прежний смысл не сохраняется, поскольку восстановлению не подлежит. А (команда interim) вне группы действует как обычное присваивание. /g\ Если вы присвоите внутренней величине tracingrestores положительное значение, JL то METAFONT будет делать отметку в файле-стенограмме всякий раз, когда будет восстанавливаться значение символьной лексемы или внутренней величины. Это может пригодиться при исправлении текста программы, смысл которой вам непонятен.
Глава 17: Группирование 169 Группы можно использовать внутри алгебраических выражений. Это еще одна важная причина для формирования групп. За счет этого METAFONT может выполнять сколь угодно сложные манипуляции по ходу других вычислений. Таким образом, многократно увеличиваются потенциальные возможности макросов (которые мы рассмотрим в следующей главе). Выражение-группа представляется в общем виде как begingroup (список утверждений) (выражение) endgroup и подчиняется правилам синтаксиса для выражений первичного уровня. Смысл выражения-группы можно выразить так: "Выполнить все, что указано в списке утверждений, найти значение выражения, а затем восстановить значения всех величин, которые сохранялись в пределах этой группы". f Выражения-группы предусмотрены правилами синтаксиса для выражений всех типов, но в предыдущих главах мы о них не упоминали, поскольку там эти подробности были излишни. Так, в правилах синтаксиса для формул типа (первичное числовое) на самом деле имеется дополнительная альтернатива begingroup (список утверждений) (числовое выражение) endgroup. Это касается и формул типов (первичная пара), (первичный рисунок) и т.д. Полностью правила синтаксиса для формул всех типов приведены в главе 25. /gK ►Упражнение 17.1 JL Каково значение выражения begingroup х:»х+1; х endgroup + begingroup х:=2х; х endgroup если начальное значение х равно а? Каким будет значение этого выражения, если переставить местами входящие в него группы? Проверьте свой ответ при помощи программы ехрг, рассматривавшейся в главе 8. f> У пражнение 17.2 В приложении В величина whatever определяется как аббревиатура выражения- группы вида 'begingroup save ?; ? endgroup'. Почему это срабатывает? <£><£)* Упражнение 17.3 JL JL Каково значение выражения 'begingroup save ?; (?, ?) endgroup' ? <$V$>* Упражнение 17.4 JL JL Согласно упражнению 10.2, присваивание 'хз := whatever1 заставит числовую переменную хз вести себя как новую, но при этом никак не повлияет на другие переменные, например хг- Придумайте, как проделать то же самое в случае индексированной последовательности переменных типа picture. Кучность (группирование) попаданий большинства начинающих стрелков довольно сложно определить; для некоторых же эта затея и вовсе безнадежна. А. Дж. ФУЛТОН, Заметки по пулевой стрельбе (1913) Рок-группы предпочитают тусоваться не в Нью Йорке, а в Сан-Франциско. — ЭЛЛЕН УИЛЛИС, But Now I'm Gonna Move (1971)
Определения, или макросы
Глава 18: Определения, или макросы 171 При написании программ для METAFONT вы зачастую можете сэкономить время, обозначив одной лексемой часто использующуюся последовательность других лексем. Например, в приложении В '—' определяется как аббревиатура последовательности лексем '.. tension infinity ..', и это определение является частью базового формата системы METAFONT. Программы, в которых используются подобные определения (definitions), проще не только писать, но и читать. Однако в приложении В содержатся далеко не все определения, которые могут понадобиться при составлении программ. В этой главе мы объясним, как стоить собственные определения. В простейшем случае вы просто указываете def (символьная лексема) = (подставляемый текст) enddef и впредь эта символьная лексема всегда будет разлагаться в последовательность лексем подставляемого текста. Например, в приложении В указано def~ = ..tension infinity., enddef. Подставляемый текст может представлять собой произвольную последовательность лексем, которая не содержит лексемы 'enddef или же включает целые подопреде- ления вида 'def ... enddef в соответствии с определенными правилами, которые мы рассмотрим позже. Определения становятся более интересными, если в них входят параметры, которые при разложении определения заменяются аргументами. Например, в приложении В также указано def rotatedaround(expr z,theta) = shifted -z rotated theta shifted z enddef; это значит, что такое выражение, как lzi rotatedaround (2:2,30)', будет разлагаться в последовательность Kz\ shifted -z-i rotated 30 shifted 22'. Параметры 'z' и 'theta' в этом определении могут быть любыми символьными лексемами; между ними и теми 'z' и 'theta', которые встречаются за пределами определения, нет никакой связи. (К примеру, 'z' здесь обозначает не пару '(х,у)', а просто какую-то лексему.) Определение может включать в качестве параметров даже "примитивные" лексемы. Например, с таким же успехом, можно указать: def rotatedaround(expr;,+) = shifted-; rotated+shifted; enddef; (Конечно, нет смысла пускаться на подобные трюки, если только вы не стараетесь умышленно запутать свое определение.) При использовании операции 'rotatedaround' вначале вычисляются значения аргументов, которые должны занять место величин z и theta. Найденные значения помещаются в так называемые "капсулы", поэтому они будут вести себя, как первичные формулы. Например, команда lz\ rotatedaround (22 + 23,30)' разлагается не в последовательность "z\ shifted —2:2 + 2:3 rotatec^ 30 shifted 22 + 2:3' shifted 2:2 + 2:3', смысл которой совершенно иной, а в последовательность lz\ shifted —a rotated 30 shifted а', где а — безымянная внутренняя переменная, содержащая значение 2:2+2:3. f Значение, помещенное в капсулу, нельзя изменить. Поэтому параметры типа ехрг не могут стоять в присваиваниях слева от оператора ':='.
172 Глава 18: Определения, или макросы f Хорошо, когда макросы работают исправно. Но сложно устроенные макросы иногда приподносят сюрпризы своим создателям. Для того чтобы вы могли разобраться в причинах их необычного поведения, в системе METAFONT имеются специальные средства "отслеживания". Они позволяют следить за тем, как компьютер понимает производимые им действия. Если вы укажете Hracingmacros := Г, то в файл-стенограмму сеанса работы будут записываться все встречающиеся макросы с их последовательным разложением и записью значений аргументов в том порядке, в котором они были вычислены. Например, при выполнении команды 'rotatedaround(up,30)\ в файле-стенограмме могут появиться такие строчки: rotatedaround(EXPRO)(EXPRl)->shifted-(EXPRO)rotated(EXPRl)sh ifted(EXPRO) (EXPR0)<-(0,1) (EXPR1X-30 fBoT еще один пример из приложения В. Из него мы видим, насколько полезными бывают выражения-группы при определении макросов: def reflectedabout (ехрг р, q) = transformed begingroup save T; transform T; p transformed T = p\ q transformed T = q; xxpart T = —yypart T\ xypart T = yxpart T; T endgroup enddef; Значение новой переменной-преобразования Т вычисляется внутри другого выражения, и разложение макроса 'reflectedabout(р,^)' фактически имеет вид 'transformed Т\ Некоторые макросы, например 'rotatedaround', предназначены для использования в общих целях. Однако при разработке конкретных шрифтов бывает удобно определить макросы специального назначения, которые значительно упрощают работу. Рассмотрим с этой точки зрения логотип 'METAFONT'. Программа для буквы 'Е' в главе 11 начиналась со строчки begincharO'E",14u#+2s#,ht#,0); pickup logo.pen; и программы для букв 'М' и 'Т' и т.д. все начинались почти так же. Поэтому где-то в начале файла logo.mf можно поместить следующее определение: def beginlogochax(expr code, unit_width) = beginchar(code,unit_width*u#+2s#,ht#,0); pickup logo_pen enddef; Тогда программу для 'E' можно будет начать, просто указав beginlogocharC'E",14); Подобным образом можно упростить программы для всех семи букв логотипа. Отметим в данном примере тот факт, что макросы можно использовать внутри других макросов (поскольку 'beginchar, и 'pickup' сами по себе являются макросами, которые определены в приложении В). Определяя макрос, вы, по сути, расширяете язык METAFONT. Отметим также, что параметры типа ехрг могут быть выражениями любого типа; например, "Ем — цепочка, а первый параметр макроса 'rotatedaround' представляет собой выражение-пару.
Глава 18: Определения, или макросы 173 В главе 11 мы не приводили программ для букв 'R' и 'О'. Оказывается, эти программы можно значительно упростить, если использовать в них вспомогательную подпрограмму ' super_half '. Например, как выглядит программа, при помощи которой была получена буква 'О': beginlogochar("0,l,15); xl=x4=.5w; top yl=h+o; bot y4=-o; x2=w-x3=1.5u+s; y2=y3=barheight; super_half(2,1,3); super_half(2,4,3); labels(1,2,3,4); endchar; Программа super.half должна рисовать половину деформированного эллипса, проходящего через три точки с указанными в скобках индексами. Мы могли бы определить super „half как макрос, зависящий от трех параметров типа ехрг, ссылаясь на первую точку как, скажем, на 'z[i]', но есть способ получше. Параметры макросов можно классифицировать как суффиксы, указав suffix вместо ехрг. В этом случае допустимым аргументом будет любой (суффикс), то есть произвольная последовательность индексов и ярлыков, дополняющая имя переменной, как объяснялось в главе 7. Вот как будет выглядеть программа super_half при использовании этой идеи: def super„half(suffix i,j,k) = draw z.i{0,y.j-y.i} ... (.8[x.j,x.i],.8[y.i,y.j]){z.j-z.i} ... z.j{x.k-x.i,0} ... (.8[x.j,x.k],.8[y.k,y.j]){z.k-z.j> ... z.k{0,у.k-y.j} enddef; ► Упражнение 18.1 Будет ли работать программа для буквы 'О', если два вызова в ней подпрограммы super_half будут иметь вид 'super_half (3,1,2)' и 'super_half (3,4,2)'? ► Упражнение 18.2 Составьте программу для буквы 'R' логотипа METAFONT, ширина которой точно такая же, как и у буквы 'О'. f Кроме параметров типа ехрг и suffix, в системе METAFONT допускаются также параметры третьего типа, который называется text. В этом случае допустимый аргумент представляет собой текст, то есть произвольную последовательность лексем, смысл которой пока не имеет значения. Текстовый аргумент просто копируется в то место, где стоит соответсвующий параметр. Это позволяет строить макросы, применимые к спискам объектов. Так, в приложении В макрос 'define_pixels' определяется как: def define_pixels(text t) = forsuffixes a=t: a := a# * hppp; endfor enddef; Это значит, что команда (define_pixels(em,cap)' будет разлагаться в forsuffixes a=em,cap: а := а# * hppp; endfor что, в свою очередь, разлагается на лексемы 'em := еш# * hppp; cap := сар# * hppp;', как мы увидим в главе 19.
174 Глава 18: Определения, или макросы f Давайте теперь рассмотрим подпрограмму для рисования засечек, так как она — типичный представитель макросов специального назначения, с которыми нам наверняка придется иметь дело при разработке мета-шрифта. Засечки поражают многообразием форм, так что придется выбирать среди бесчисленного множества вариантов. Мы рассмотрим два совершенно разных подхода к их рисованию. Один предполагает заливку контура, а другой — использование пера с фиксированным кончиком. Чтобы не слишком усложнять примеры, в обоих случаях нам придется пожертвовать элементами изящества, присутствия которых мы добиваемся при полноценной разработке шрифта. fHaui первый пример — это программа, которая строит шесть точек z$a, z$b, • • •, z%f вокруг заданной тройки точек z%{, z%, z$r из команды lpenpos\ определяющей положение пера; здесь $ — суффикс, играющий роль параметра в макросе serif. Кроме него, параметрами этого макроса являются: breadth — расстояние между параллельными линиями, проходящими из z%\ в z$a, и из z$r в z$f; theta — угол наклона этих двух линий; left-jut — расстояние от z$i до 2$ь; и right-jut — расстояние от z%T до z$e. (Параметры с окончанием ljuV определяют величину "выступа" засечки влево и вправо.) Кроме того, определен макрос serif-edge, который строит путь, показанный на рисунке. В программах фигурируют три переменные, которые могут быть использованы для рисования любых засечек: slab — расстояние по вертикали от г$ь и z$e до z$c и z%d\ bracket — расстояние по вертикали от z$a и z$f до z%\ и z%T; и serif-darkness — дробь, которая определяет, какая часть треугольных областей (z$a, z$t, z$b) и (г$/, z$r, z$e) подлежит заливке. def serif (suffix $)(ехрг breadth, theta, left-jut, right-jut) = penpos$ (breadth /abs sind theta, 0); z$a — z$i = z$f — z$r = (bracket /abs sind theta) * dir theta; У$с = 2/$d5 У$ь = У$е = У$; У$ь ~ У$с = if theta < 0 : - fi slab) х$ь = я$с = z$j - left-jut; x$d = x$e = x$r + right-jut; labels($a, $b, $c, $d, $e, $/) enddef; def serif-edge suffix $ = (serif-bracket ($a, $/, $6) - - z$c - - z$d - - reverse serif-bracket ($/, $r, $e)) enddef; def serif-bracket (suffix i,j,k) = (z.i{z.j — z.i) ... serif-darkness [z.j, .b[z.i, z.k] ]{z.k — z.i) ... z.k{z.k — z.j}) enddef; f^ Упражнение 18.3 При каких условиях путь serif-edge пройдет через точки z$i и z$r? f* Упражнение 18.4 Что следует сделать вначале: применить определенный выше макрос serif или задать точки z$i, z% и z$r? f Следующие две буквы приводятся как образец того, как можно использовать эти подпрограммы для рисования засечек. В программах предполагается, что шрифт имеет несколько дополнительных специальных параметров: и — единичная ширина символа, ht — высота символа, thin и thick — две толщины линий, и jut — величина выступа засечек на "нормальной" букве, такой, как 'Н\
Глава 18: Определения, или макросы 175 beginchar("A", 13u#,/i*# 0); zi =(.5и>,1.05Л); xai — w - хъг = Щ J/4/ = Уъг = slab] numeric £/ie£a[]; theta* = angle(zi — 24/); thetas = angle(zi — zsr)\ serif (4, thin, theta*, .6jut, jut); serif (5, tfiicfc, thetas,jut, .6jut); zq = Zir + whatever * dir theta\ = Z5/ + whatever * dir t/iefos; fill 2i -- serif .edge A -- zo k, zq-- serif-edge ъ -- 21 & cycle; penpos2(whatever, f/ieiu4); penpos3(whatever, t/ietas); 2/2r = узг = .5[у4,уо]; 2/2/ = уз/ = 2/2г - *Лгп; Z2 = whatever[zi, ztr]', zz — whatever [21,25/]; penstroke 22e - - 2зе; penlabels(0,1, 2, 3,4, 5); endchar; beginchar ("I", 6u#, Л*#, 0); x\ = X2 = 5u?; t/i = /i -2/2; 2/2 = sfa6; seri/(l, £/itcfc, —90, l.ljut, l.ljut); serif (2, £/iicA;,90, l.ljuf, l.ljtti); fill serif.edge 2 --reverse serif-edge ± -• penlabels(l,2); endchar; % верхняя точка % нижняя точка % наклон левой линии % наклон правой линии % левые засечки % левые засечки % внутренняя верхняя точка % левая линия % правая линия % высота перемычки % толщина перемычки ■ cycle; % перемычка % верхние засечки % нижние засечки % линия При создании этой иллюстрации были заданы такие значения: thin = .5р£, thick и = .6р£, ht = Ipt, slab = .25р£, jut = .9р£, bracket = р£, и serif-darkness = 1/3. l.lpt, /g\ ► Упражнение 18.5 JL Можно ли в программе для буквы "I" заменить уравнения, в которых определяются 2/i и 2/2 уравнениями 'yic = h} и '2/2с =0'? f ►Упражнение 18.6 Напишите программу для буквы "Н", сочетающейся с этими буквами. <£)<$) Второй подход к рисованию засечек можно объяснить на примере, приведенном в конце главы 16. В этом случае мы предполагаем, что перо broad-pen определяется
176 Глава 18: Определения, или макросы как 'pensquare xscaled px yscaled py rotated phi\ где px > py, a phi — малый угол. Более толстые линии получаются применением этого пера к более обширным областям; одним из параметров этой подпрограммы является величина хх, равная расстоянию от z$i до z$r. Переменная-пара dishing контролирует кривизну линии на участке между z$c и z$d. Верхняя и нижняя засечки похожи, но все же отличаются настолько, что легче написать для каждого случая отдельный макрос. def botserif (suffix $)(ехрг хх, theta, left-jut, right-jut) — penpos$(xx,0); z$a — z$i — z$f — z$r = (fcracfce*/abssind theta) * dir theta; y$c = topy$l; y$d = y$r; x$c = x$, - left-jut; xu = x$r + right-jut; z$b = z$i + whatever * dir theta = z$c ■+■ whatever * dir phi; zSe — z$r + whatever * dir theta = z$d + whatever * dir —phi; labels($a, $6, $c, $d, $e, $/) enddef; def botserif-edge suffix $ = (z$a .. controls z$b .. z$c -- (flex(z$c, .b[z$c, zu] + dishing, z$d)) shifted (0, -epsilon) -- z%d • • controls z$e .. z$f) enddef; beginchar ("A", 13u#, ht#, 0); pickup broad-pen; z\ — (.bw, top h); Ift хц = w — rt x$r = 1.2u; y^i = уы = 0; numeric theta[]; theta4 = angle^i — 24/); theta5 = angle(zi — zsr); numeric xxx; px * sm&{thetab — phi) + xxx * sind theta^ = px * cosdphi + xx; botserif (4,0, theta^, .Sjut, .Sjut); botserif (5, xxx, theta$, .6jut, .Sjut); z0 = z4r + whatever * dir theta^ = z^i 4- whatever * dir theta^; filldraw z\ -- botserif .edge ± -- zo $z zo -- botserif-edge ъ -- zi & cycle; fop 2/2 = fop 2/3 = .45&o£y0; <z2 = whatever^, Z4r]; 23 = tu/ia£ever[2:i,Z5/]; draw Z2 --23; penlabels(0,1,2,3,4,5); endchar; beginchar ("Iм, 6u#, /i£ #, 0); pickup broadjpen; x\ = x2 = .5w; тд = Л; y2 = 0; top serif {\, xx, —90, 1.1/ufc, l.l/ttf); botserif {2, xx, 90, l.l/u£, l.l/u£); filldraw botserif-edge 2 -- reverse topserif.edge x --cycle; penlabels(l,2); endchar; В этой иллюстрации px = .Spt, py = .2pt, phi = 20, xx = .Zpt, и = .6p£, /it = 7pt, jut = .9pt, bracket = p£, и dishing = (.23pt, 0) rotated 20. /^s A^ Упражнение 18.7 Напишите недостающие макросы topserif и top serif .edge.
Глава 18: Определения, или макросы 177 <^!Х^* Упражнение 18.9 >► Упражнение 18.8 (Для математиков.) Объясните уравнение с ххх в программе для буквы "А". Напишите программу для буквы "Н", сочетающейся с этими буквами. /gK Внимательно приглядевшись к тексту подпрограммы serif-edge в этих примерах, JL вы увидите, что в ней не хватает некоторых скобок: в определении макроса мы писали 'def serif „edge suffix $' вместо 'def serif-edge (suffix $)', а при его использовании мы писали l serif-edge $ вместо l serif-edge (Ь)1. Дело в том, что METAFONT позволяет записывать последний параметр макроса без разделителей; об этом прежде ничего не говорилось, и из предыдущих примеров этого понять невозможно. Теперь настало время перейти от рассмотрения частных случаев к изучению полного набора правил синтаксиса, которым подчиняются определения макросов. Вот эти правила: (определение) —> ("шапка" определения)(есть)(подставляемый текст) enddef (есть) —У = | : = ("шапка" определения) —> def (символьная лексема)("шапка" параметров) | ("шапка" переменной-макроса) | ("шапка" бинарной операции) ("шапка" параметров) —> (выделенные параметры)(невыделенные параметры) (выделенные параметры) —> (пустой параметр) | (выделенные параметры) ((тип параметров) (лексемы-параметры)) (тип параметров) —► ехрг | suffix | text (лексемы-параметры) —> (символьная лексема) | (лексемы-параметры) , (символьная лексема) (невыделенные параметры) —> (пустой параметр) | primary (символьная лексема) | secondary (символьная лексема) | tertiary (символьная лексема) | ехрг (символьная лексема) | ехрг (символьная лексема) of (символьная лексема) | suffix (символьная лексема) | text (символьная лексема) (Что такое ("шапка" переменной-макроса) и ("шапка" бинарной операции), мы объясним в главе 20.) Вначале мы даем имя определяемому макросу, затем перечисляем выделенные параметры (т.е. параметры в круглых скобках), количество которых может быть нуль и более, затем перечисляем невыделенные параметры, количество которых также может быть нуль и более. Затем через знак '=' мы записываем подставляемый текст, и в конце ставим enddef. Вместо знака '=' можно писать ':=', оба они означают одно и то же. f Выделенные параметры бывают типов ехрг, suffix и text; два и более параметров одного типа можно перечислять вместе, разделяя запятыми. Например, '(ехрг а,6)' эквивалентно '(ехрг а)(ехрг 6)'. Невыделенные параметры, согласно синтаксису, могут принимать один из восьми возможных видов. /4\ (Подставляемый текст) просто фиксируется для дальнейшего использования как JL некоторая последовательность символов и никак не интерпретируется в тот момент, когда METAFONT считывает определение. Но некоторые лексемы все же понимаются в особом смысле. ■ Лексемы def, vardef, primarydef, secondarydef и tertiarydef расцениваются как определения внутри определения.
178 Глава 18: Определения, или макросы ■ Лексема enddef обозначает конец подставляемого текста, если только ей не предшествует какая-либо лексема из предыдущего пункта. ■ Для того чтобы (символьная лексема) стала именем параметра, ей достаточно появиться в ("шапке" параметров) или ("шапке" бинарной операции). В подставляемом тексте каждая такая лексема заменяется специальной внутренней "лексемой- параметром". Впоследствии, встретив эту лексему, METAFONT всегда будет подставлять на его место соответствующее значение. ш Команда quote запрещает особую интерпретацию следующей за ней лексемы, то есть превращает ее в обычную последовательность символов. Сама лексема 'quote' выбрасывается из подставляемого текста (если только команда quote не применялась сама к себе). f* Упражнение 18.10 Для того чтобы проверить, хорошо ли вы разобрались в этих правилах, отыщите подставляемый текст в следующем таинственном определении: def fоо(text t) expr e of p := def t = e enddef; quote def quote t = p enddef f Считывая (определение), система METAFONT не выполняет разложение макросов. Практически в любое другое время, натолкнувшись на лексему, обозначающую макрос, она заменяет ее соответствующим текстом, вычислив предварительно значения всех аргументов. После такой замены подставленный текст считывается так, как если бы он присутствовал в программе с самого начала. fKaK же система METAFONT распознает аргументы макросов? Дело в том, что по шапке параметров, она определяет, какие аргументы ей могут встретиться. Давайте сначала рассмотрим выделенные аргументы. ■ Выделенный аргумент типа ехрг должен иметь вид '((выражение))'; значение выражения вычисляется и помещается в специальную "капсулу" — лексему, которой этот параметр заменяется затем всюду в подставляемом тексте. ■ Выделенный аргумент типа suffix должен иметь вид '((суффикс))'; если в суффиксе присутствуют индексы, они вычисляются и заменяются числовыми лексемами. В результате получается список из нуля или более лексем, и этим списком аргумент заменяется везде в подставляемом тексте. ■ Выделенный аргумент типа text должен иметь вид '((текст))', где (текст) означает произвольную последовательность лексем, сбалансированную относительно разделителей. Эта последовательность лексем занимает место параметра всюду в подставляемом тексте. ■ Если количество однотипных выделенных параметров больше двух, то вы можете не заключать каждый аргумент в круглые скобки, а записывать их через запятую. Например, для трех выделенных аргументов, все четыре формы записи '(a)(6)(c)', '(а,6)(с)', '(а)(6,с)' и '(а,6,с)' эквивалентны. Однако такое сокращение нельзя использовать в случае текстовых аргументов, которые должны оканчиваться символом ')', так как они могут содержать запятые. В главе 8 указывалось, что вместо круглых скобок можно использовать другие лексемы-разделители. Вообще говоря, если запятая стоит после выделенного аргумента типа ехрг или suffix, то она эквивалентна двум лексемам ') (', соответствующим тем разделителям, внутри которых находится эта запятая. /^\/$\^ Упражнение 18.11 JL JL Пусть задано определение 'def f (ехрг а) (text b,c)=.. .enddef, а затем указано 'delimiters {{ }}'. Перечислите аргументы макроса 'f{{x, (,}}((}}))'.
Глава 18: Определения, или макросы 179 f Правила для невыделенных аргументов почти такие же. Невыделенный аргумент типа primary, secondary, tertiary или expr — это максимальная синтаксически верная (первичная формула), (вторичная формула), (третичная формула) или (выражение), следующая/следующее непосредственно за выделенными аргументами. Невыделенный параметр 'expr х of у' задает два аргумента, как видно из его представления в виде '(выражение) of (первичная формула), где формулы в угловых скобках имеют максимальную длину и синтаксически верны. В каждом из этих случаев перед выражением может стоять символ '=' или ':='. Невыделенный аргумент типа suffix — это (суффикс) максимальной длины, стоящий сразу после выделенных аргументов; в этом случае допускается также вариант '((суффикс))', но не '=(суффикс)' или ':=(суффикс)'. Невыделенный аргумент типа text просто переносится в конец текущей записи; точнее, он переносится к первой лексеме ';' или 'endgroup' или 'end', которая не входит в одну группу с этим аргументом. fB приложении В содержится множество определений макросов, иллюстрирующих эти правила. В частности, определены макросы def fill expr с = addto currentpicture contour с enddef; def erase text t = cullit; t withweight — 1; cullit enddef; Здесь они приведены в несколько упрощенном виде, но основная идея сохранена. Команда 'erase fill р' превращает 'fill р' в текстовый аргумент макроса erase, после чего 'р', в свою очередь, превращается в аргумент типа expr макроса fill. <£><£>* Упражнение 18.12 JL JL Макрос 'pickup', определенный в приложении В, имеет "шапку" вида 'def pickup secondary q\ Почему его аргументом выбрано не выражение, а вторичная формула? &)<£)* Упражнение 18.13 JL Jl Объясните, почему приведенный ниже макрос '/tide' позволяет спрятать любую последовательность записей внутри выражения. def /iide(text t) = gobble begingroup t\ endgroup enddef; def gobble primary g = enddef; ОПРЕДЕЛЕНИЕ, ед. [definitio, латынь] 1. Короткое описание чего-либо его свойствами. — САМЮЭЛЬ ДЖОНСОН, Словарь английского языка (1755) ОПРЕДЕЛЕНИЕ, сущ. [лат. definitio. См. определять.] 1. Краткое описание чего-либо его свойствами; например, определение остроумия или круга. НОА ВЕБСТЕР, Американский словарь английского языка (1828)
19 Условия и циклы
Глава 19: Условия и циклы 181 Если бы нам никогда не приходилось принимать решения, то жить и программировать было бы намного легче. Однако зачастую нам приходится выбирать среди множества альтернатив. В системе METAFONT программы могут развиваться по тому или иному сценарию, в зависимости от обстоятельств. Достаточно сказать нечто вроде if not decisions: life := programming := easier (much) elseif choice = a: program.a else: programJ) fi и данная конструкция сведется к программе ''programJ)' тогда и только тогда, когда decisions = true и choice ф а. Кроме того, обычный порядок интерпретирования программы слева направо можно изменять, задавая "циклы", в которых компьютеру дается указание считывать определенные лексемы снова и снова с незначительными изменениями до тех пор, пока не будет выполнено некое условие. Мы уже не раз видели этот механизм в действии; в этой главе будет рассмотрен весь спектр возможностей. Условия (conditions) и циклы* (loops) системы METAFONT отличаются от условий и циклов в большинстве других языков программирования, так как текст условия или итерируемого фрагмента не обязательно должен укладываться в рамки синтаксиса. Вы можете, например, написать такую странную фразу: р = (if Ъ: 0,0)..(1,5 else: u,v fi) в которой условный текст '0,0) .. (1,5* сам по себе ничего не значит, но приобретает определенный смысл в контексте. В этом отношении условия и циклы ведут себя как макросы. Они задают правила преобразования лексем, которые, говоря образно, происходят "во рту" системы METAFONT до того, как эти лексемы будут переварены в "желудке" машины. Первая из рассмотренных условных конструкций содержит три альтернативы, которые представлены в виде if (булево выражениех): (текстх) elseif (булево выражениег): (текстг) else: (текстз) fi Во втором примере альтернатив всего две. Перед словом 'else:' может стоять сколько угодно высказываний (clause), начинающихся с 'elseif. Из всех условных текстов "выживает" только один — первый, условие которого будет выполнено; условие 'else:' выполнено всегда. Вы можете обойтись вовсе без 'else:', поместив непосредственно перед замыкающим 'fi' высказывание вида 'else: (пустой текст)'. К примеру, в подпрограмме mode_setup базового формата METAFONT имеется условная команда if unknown mag: mag := 1; fi которая устанавливает значение переменной mag равным 1, если прежде ей не было присвоено какое-либо значение; в этом случае альтернатива лишь одна. ► Упражнение 19.1 Будет ли ошибкой вставить ';' после 'fi' в только что приведенном примере? * Не путать с циклическими путями! — Прим. перев.
182 Глава 19: Условия и циклы f Описанные нами неформальные правила, конечно же, можно выразить в более формальном виде, как правила синтаксиса: (условие) —> if (булево выражение) : (условный текст) (альтернативы)f i (альтернативы) —> (пустая лексема) | else : (условный текст) | elseif (булево выражение) : (условный текст)(альтернативы) Каждая условная конструкция начинается с лексемы 'if и заканчивается лексемой 'fi\ Условный текст — это произвольная последовательность лексем, сбалансированная относительно 'if и 'fi'; кроме того, лексемы 'elseif и 'else' могут присутствовать в условном тексте только в том случае, если они окружены лексемами 'if и 'fi'. За каждым оператором 'if и 'elseif должно следовать (булево выражение), то есть выражение, принимающее значения true (истина) и false (ложь). Булевы выражения названы в честь Джорджа Буля (George Boole), основоположника алгебраического подхода к логике. В главе 7 отмечалось, что переменные могут иметь тип boolean, а в главе 8 приводились примеры булевых выражений. Настало время изучить их системно, чтобы узнать о них все то, что мы знаем о числовых выражениях, выражениях-парах, выражениях-рисунках, выражениях-путях, выражениях- преобразованиях и выражениях-перьях. Вот соответствующие правила синтаксиса: (первичное булево) —> (булева переменная) | true | false | ( (булево выражение) ) | begingroup (список утверждений) (булево выражение) endgroup | known (первичная формула) | unknown (первичная формула) | (тип) (первичная формула) | cycle (первичная формула) | odd (первичное числовое) | not (первичное булево) (вторичное булево) —> (первичное булево) | (вторичное булево) and (первичное булево) (третичное булево) —> (вторичное булево) | (третичное булево) or (вторичное булево) (булево выражение) —> (третичное булево) | (числовое выражение) (отношение) (третичное числовое) | (выражение-пара) (отношение) (третичная пара) | (выражение-преобразование) (отношение) (третичное преобразование) | (булево выражение) (отношение) (третичное булево) | (выражение-цепочка) (отношение) (третичная цепочка) (отношение) —> < | <= | > | >= | = | о Большинство этих операций уже описывалось в главе 8, так что теперь достаточно рассмотреть только наиболее тонкие моменты. Любую первичную формулу можно проверить на предмет принадлежности к тому или иному типу, а также проверить, известно ли в данный момент ее значение. При этом считается, что (первичная заготовка пера) имеет тип реп. Проверка 'cycle р' даст положительный результат в том и только том случае, когда путь р — циклический. Функция 'odd' вначале округляет значение аргумента до целого числа, а затем проверяет, является ли это число нечетным. Функция 'not' заменяет значение true на false, и наоборот. Функция 'and' возвращает значение true только тогда, когда оба ее аргумента имеют значение true; функция 'or' возвращает значение true, если значение true
Глава 19: Условия и циклы 183 имеет хотя бы один из аргументов. Отношения между парами, преобразованиями и цепочками устанавливаются сравнением их покомпонентно в порядке слева направо. (Преобразования представляют собой шестерки чисел, см. главу 15.) /$К ►Упражнение 19.2 JL Как вы думаете, верно ли, что false > true? f ►Упражнение 19.3 Может ли выражение '(odd n) and not (odd п)' оказаться истинным? f> Упражнение 19.4 Может ли выражение ((cycle р) and not (known р)' оказаться истинным? f ►Упражнение 19.5 Определите макрос 'even' так, чтобы 'even (п)' было истинным тогда и только тогда, когда число round(n) — четное целое. [Указание: существует элегантное решение.] /gK/gK Булево выражение, начинающееся с лексемы, обозначающей (тип), не должно JL JL стоять в самом начале утверждения, поскольку тогда METAFONT решит, что имеет дело не с (выражением), а с (объявлением). Например, если Ъ — булева переменная, то уравнение 'path р = 6' следует переписать как '6 = path р' или как '(path р) = Ь\ /$s/$\ Булево выражение вида 1х = у\ выражающее отношение равенства, выглядит JL JL точь-в-точь как уравнение. METAFONT рассматривает знак '=' как (отношение) всегда, за исключением тех случаев, когда выражение слева от него стоит в самом начале (утверждения) или (правой части). Если вам нужно превратить уравнение в отношение, достаточно вставить скобки, например, таким образом: '(х = у) = 6' или '6 = (х = у)\ /$\/$\ Лексема 'cycle' не рассматривается как начало (первичной булевой формулы), JL JL если она стоит после (связки путей). (См. главу 14.) /gK/gK Булево выражение 'path ((0,0))' ложно, несмотря на то, что последовательность JL JL '((0,0))' удовлетворяет сразу нескольким правилам синтаксиса для формул типа (первичный путь), приведенным в главе 14, — как ((выражение-путь)), как ((третичный путь)) и как ((третичная пара)). Выражение-пара считается выражением типа path только в тех случаях, когда такая интерпретация диктуется обстоятельствами. <^><^>^ Упражнение 19.6 YY Вычислите 'length ((3,4))', 'length ((3,4){0,0})' и 'length reverse (3,4)'. Итак, это все, что касается условий. А что же циклы? О них будет проще рассказать, приведя сначала правила синтаксиса: (цикл) —> ("шапка" цикла):(текст цикла) endf or ("шапка" цикла) —> for (символьная лексема) (есть) (список цикла) | for (символьная лексема)(есть)(прогрессия) | for suffixes (символьная лексема) (есть) (список суффиксов) | forever (есть) —> = | : = (список цикла) —> (выражение) | (пустое выражение) | (список цикла) , (выражение) | (список цикла) , (пустое выражение) (список суффиксов) —> (суффикс) | (список суффиксов) , (суффикс) (прогрессия) —> (начальное значение) step (шаг) until (предел) (начальное значение) —> (числовое выражение) (шаг) —> (числовое выражение) (предел) —> (числовое выражение) (условие выхода из цикла) —> exit if (булево выражение) ;
184 Глава 19: Условия и циклы Как и в определениях макросов, лексемы '=' и ':=' здесь взаимозаменяемы. Из правил синтаксиса следует, что циклы бывают четырех видов; их можно представить в общем виде как for х = 61,62,63: text(a:) endfor for x = v\ step 1/2 until 1/3: text(ar) endfor forsuffixes s = сг!,сг2,сгз: text(s) endfor forever: text endfor В первом случае цикл раскладывается в последовательность 'текстах) тексте) текст(бз)', где величины е могут быть выражениями произвольного типа, необязательно "известными", значения которых вычисляются и помещаются в капсулы, прежде чем они будут подставлены вместо х. Величины е могут быть и пустыми, в этом случае 'текст(б)' просто опускается. Второй случай более сложный, поэтому на нем мы подробно остановимся чуть ниже; в простейших случаях, например, '1 step 2 until 7', такой цикл эквивалентен небольшому списку, например, '1,3,5,7'. В третьем случае цикл раскладывается в последовательность 'текстах) текст(сг1) тексте)', где каждое а обозначает произвольный (возможно, пустой) суффикс, который будет подставляться вместо s после того, как входящие в него индексы будут вычислены и заменены символьными лексемами. В последнем случае цикл раскладывается в последовательность 'текст текст текст ...', и так до бесконечности. Из этого (и трех предыдущих видов) цикла можно "выскочить"; для этого в тексте должно присутствовать (условие выхода из цикла), о котором мы поговорим чуть позже. Отметим, что если текст в цикле состоит из одного утверждения, которое должна быть повторено несколько раз подряд, то точку с запятой нужно ставить перед лексемой endfor, а не после нее. В системе METAFONT циклы не вставляют точки с запятой автоматически, поскольку они изначально задуманы не только для многократного воспроизведения целых утверждений, но и для использования внутри выражений. В базовом формате METAFONT 'upto' определяется, как краткая форма записи последовательности 'step 1 until', a 'dawnto' — последовательности 'step -1 until'. Поэтому, например, вместо 'for х = 1,2,3,4,5,6,7,8,9:' вы можете указать просто ' for х = 1 upto 9:'. /$К Когда вы указываете 'for х = v\ step 1/2 until v$\ система METAFONT находит JL вычисляет три заданных выражения, значения которых должны быть известны. Затем она считавает текст цикла. Если vi > 0 и v\ > 1/3, или v% < О и v\ < 1/3, то выполнение цикла прекращается. В противном же случае выполняется текстах), после чего v\ заменяется на i/i +1/2, и весь процесс повторяется с новым значением v±. f> Упражнение 19.7 Внимательно прочтите предыдущий абзац и скажите, с какими значениями х будет выполняться цикл, если вы укажете (а) ' for х = 1 step 2 until 0'. (b) ' for х = 1 step —2 until 0'. (с) ' for х = 1 step 0 until 0 '. (d) ' for x = 0 step .1 until 1 \ f Следует отметить, что (текст цикла) во многом похож на (подставляемый текст) макроса. Он представляет собой произвольную последовательность лексем, сбалансированную относительно разделителей for/forsuffixes/forever и endfor, не подвергшихся действию команды quote. Прежде чем попытаться выполнить текст цикла или разложить входящие в него макросы, METAFONT быстро считывает и запоминает его. Затем переменная (символьная лексема) заменяется всюду в тексте цикла специальными
Глава 19: Условия и циклы 185 внутренними лексемами-параметрами, которые означают: "В этом месте должен стоять аргумент". В случае, когда цикл контролируется лексемой for, аргумент будет иметь тип ехрг, а в случае forsuffixes — тип suffix. Из этого правила, в частности, следует, что переменная символьная лексема никак не связана с одноименными переменными из других частей программы. /$\ ►Упражнение 19.8 JL Какие значения выведет на экран следующая программа? n=0; for n=l: m=n; endfor show m,n; end. f Программа flex, описанная в главе 14, дает интересный пример того, как цикл может использоваться внутри макроса, который сам находится внутри выражения: pair 2l[], dz.\ numeric n_; % зарезервированные переменные def flex (text t) = % t — список пар hide(n. :=0; for z = t: z. [incr n_] := z\ endfor dz-:=z-[n-]-z.[l}) z- [1] for k = 2 upto n_ — 1: ... z_ [fc]{dz_} endfor ... z_ [n_] enddef; Первый цикл временно записывает заданные пары в массив и одновременно подсчитывает их количество; эти операции "спрятаны" при помощи макроса hide. Затем при помощи второго цикла строится путь flex. (В приложении В принято соглашение, что пользователь в своих программах не должен использовать символьные лексемы, оканчивающиеся на '_'; благодаря этому зачастую отпадает необходимость сохранять лексемы при помощи команды 'save'.) f Натолкнувшись на конструкцию вида 'exitif (булево выражение)', METAFONT вычисляет значение указанного булева выражения. Если выражение истинно, то выполнение (последнего по вложению) цикла сразу же прекращается. В противном случае ничего особенного не происходит. f ►Упражнение 19.9 Определите макрос 'exitunless' так, чтобы команда вида exit unless (булево выражение)' вызывала выход из текущего цикла, если указанное булево выражение ложно. <£><£)* Упражнение 19.10 JL JL Напишите программу, которая бы присваивала значение, равное fc-тому простому числу, 1 < к < 30, переменной р[к]. Так, р[1] должно быть равно 2, р[2] = 3 и т.д. /$s/^\^ Упражнение 19.11 JL JL Запустив METAFONT с файлом 'expr.mf', рассматривавшийся в главе 8, вы попадаете в бесконечный цикл типа 'forever', выйти из которого вы можете, набрав, например, '0 end'. Но при этом вы выйдете и из METAFONT. Как выйти из цикла, не прекращая сеанса работы? (Ваша цель — заставить METAFONT выдать приглашение к работе '*', не выдав предварительно никаких сообщений об ошибках.) Если? Защитник ты проклятой потаскухи Мне смеешь говорить, что было б "если"? Предатель ты, и смерти ты повинен. УИЛЬЯМ ШЕКСПИР, Ричард Третий (1593) Не поминайте всуе. — Евангелие от Матфея 6:7 (с. 70 A.D.)
20 И снова о макросах
Глава 20: И снова о макросах В главе 18 мы привели лишь основные сведения об определениях макросов (macros), но этого недостаточно, чтобы иметь о них полное представление. Пришло время рассказать о них всю правду. fBce абзацы в этой главе помечены знаком "опасного поворота", поскольку изложенный здесь материал лучше всего будет усваиваться теми, кто уже достаточно поднаторел в работе с METAFONT. Мы обсудим следующие темы: ■ Определения, начинающиеся с 'vardef'; такие определения вводят во множество переменных программы переменные-макросы, пополняя, таким образом, класс унарных операций системы METAFONT. ■ Определения, начинающиеся с 'primarydef, 'secondarydef или 'tertiarydef'; такие определения пополняют класс бинарных операций системы METAFONT. ■ Другие примитивы МЕТА FONT, которые, подобно макросам, разлагаются в последовательности лексем; в частности, 'input' и 'scantokens'. ■ Правила, определяющие, когда лексемы подлежат разложению, а в когда — нет. f Сначала давайте рассмотрим ("шапку" переменной-макроса), определение которой мы отложили на будущее в главе 18. Обычный макрос, который мы рассматривали в той главе, начинался с "шапки" вида def (символьная лексема) ("шапка" параметров) за которой следовал знак '=' и т.д. Вместо этого, вы можете начать определение, указав vardef (объявляемая переменная)("шапка" параметров) В этом случае (объявляемая переменная) может состоять из нескольких лексем, и вы, по сути, определяете переменную, "значение" которой представляет собой макрос. Допустим, вы решили указать pair a.p\ pen a.q; path a.r\ vardef a.s = ... enddef; тогда переменные a.p, a.q и a.r будут иметь, соответственно, типы pair, pen и path, a переменная a.s будет разлагаться в последовательность лексем. (Язык SIMULA67 дает наглядный пример преимуществ такого подхода, когда процедуры включаются в структуру переменных данных; в системе МЕТА FONT то же самое происходит с макросами.) f Определение вида 'def t = ...' превращает лексему t в "вызов"; то есть ее уже нельзя будет использовать в качестве суффикса. А вот определение вида 'vardef t = ...' оставляет лексему t в ранге "ярлыка", поскольку разложение будет производиться только в том случае, когда t — первая лексема в имени переменной. Именно по этой причине в приложении В некоторые макросы определяются не через 'def, а через 'vardef. Так, например, макрос vardef dir primary d = right rotated d enddef не запрещает пользователю называть переменные такими именами, как 'p5dir\ fC точки зрения синтаксиса, переменная является первичной формулой. Если бы подставляемый текст переменной-макроса существенно отличался от первичной формулы, то это привело бы к неоправданному усложнению внутреннего устройства МETA- FONT. Поэтому в начало и конец подставляемого текста переменной-макроса автоматически вставляются лексемь!-разделители 'begingroup' и 'endgroup'. Если сразу после рассмотренных выше объявлений и определений вы скажете 'showvariable a\ то машина ответит следующее: a.p=pair a.q=unknown pen a.r=unknown path a.s=macro:->begingroup...endgroup
188 Глава 20: И снова о макросах Макрос 'incr', определенный в приложении В, увеличивает на 1 значение своего аргумента, и выдает в качестве результата это новое увеличенное значение. Вставляемые лексемы 'begingroup' и 'endgroup' приходятся тут как нельзя кстати: vardef incr suffix $ = $:=$ + 1; $ enddef. Заметьте, что тип аргумента здесь не ехрг, a suffix, поскольку, во-первых, любое имя переменной есть частный случай (суффикса), а, во-вторых, параметр типа ехрг не может стоять слева от знака ':='. Кстати, согласно правилам для невыделенных параметров- суффиксов, которые мы приводили в главе 18, применяя макрос 'incr' к переменной v, можно писать либо 'incr v\ либо 'incr(v)'. Существует еще одна разновидность определений типа vardef. Они характерны тем, что при использовании определяемой в них переменной ее имя может иметь дополнительный суффикс; этот суффикс рассматривается как аргумент макроса. В этом случае вы пишите: vardef (объявляемая переменная)Ф# ("шапка" параметров) После этого вы можете использовать сочетание в# в подставляемом тексте (где оно будет вести себя, как любой другой параметр типа suffix). Например, в приложении В говорится: vardef z©# = (хв#, уФ#) enddef; это волшебное определение делает переменную '23/ эквивалентной паре '(хзг,узг)' и т.п. На самом деле, как мы теперь знаем, 4z3r' раскладывается в последовательность из одиннадцати лексем begingroup (хЗг, уЗг) endgroup /gK/gK^ Упражнение 20.1 JL JL Верно ли, что после определения 'vardef а€# suffix Ъ = ... enddef аргумент- суффикс b всегда будет пустым? /gK/gK В базовом формате METAFONT имеется макрос solve, позволяющий при помощи JL JL процедуры бинарного поиска находить численные решения нелинейных уравнений, которые обычными методами решить крайне сложно. Для того, чтобы использовать макрос solve, вы вначале определяете макрос /, такой, что /(х) принимает значения true и false; затем вы указываете solve /(*rue_x, false_х) где true-x и falsejx — значения, для которых f(true-x) = true и f(false-x) = false. В результате вы получите значение х, лежащее на границе между областями истинности и ложности, т.е. х будет принадлежать отрезку заданной длины, на котором / принимает оба возможных значения; длина этого отрезка определяется величиной tolerance. vardef sofoeв#(ехрг true-x,false-x) = tx. := trtxe_x; /x_ := falsejx\ forever: x_ := .5[te_,/x_]; exitif abs(te. — fx.) < tolerance; if Ф#(х_) : te_else : /x-fi:=x_; endfor; x_ enddef; В частности, процедура solve позволяет решить следующую интересную задачу, поставленную Ричардом Соуфоллом (Richard Southall): для заданного набора из четырех точек zi, Z2, 23 и z\ таких, что х\ < Х2 < хз < %4 и t/i < j/2 = Уз > 2/4, найти такую точку г, лежащую между гг и 2з, чтобы при обходе пути Zi {Z2 - 2l} . • Z . . {ZA - Z3} Za ff
Глава 20: И снова о макросах 189 система METfiFONT выбирала направление движения в точке z равным right. Если мы попробуем взять z = 22, то METAFONT сделает в точке z поворот в направлении с положительной у-компонентой (вверх), а если мы возьмем z = 23, то METAFONT выберет поворот в направлении с отрицательной у-компонентой (вниз). Где между этими точками находится такое "хорошее" значение 2, при котором кривая не будет подниматься выше линии у = у2- Где же находится эта точка 2? В главе 14 приводились уравнения, из которых, в принципе, можно найти точку z. Но это очень сложные тригонометрические уравнения. Как же приятно узнать, что, несмотря на всю сложность задачи, существует относительно простой способ, позволяющий отыскать точку z\ vardef upward (ехрг х) = ypart direction 1 of (21(22 — 21} .. (х,уг) • • {za — 23)24) > 0 enddef; 2 = (solve upward(х2,хг),у2)- ► Упражнение 20.2 Возможна нестандартная ситуация, когда значение upward (х) равно false для всех х в пределах хг < х < хз, поэтому, вызов процедуры solve происходит при неверных предположениях. Какой результат получим в этом случае? ► Упражнение 20.3 Найдите у/10 при помощи процедуры solve и сравните полученный ответ с кубическим корнем, найденным обычным путем. Правила синтаксиса для (объявляемой переменной) из главы 7 позволяют использовать в имени переменной, которую мы объявляем, как ярлыки, так и совместные индексы. Таким образом, вы можете указать vardef a[]b[] = ... enddef; Что же это значит? Это значит, что все такие переменные, как а1Ъ2, представляют собой макросы с общим подставляемым текстом. Каждая переменная-макрос имеет два неявных параметра-суффикса, '#Ф' и 'в', которые можно использовать в подставляемом тексте для того, чтобы определить, какие индексы используются на самом деле. Параметр 'в' — это последняя лексема в имени переменной (в данном случае — '2'); параметр '#Ф' — это все, что предшествует последней лексеме (в данном случае 'alb'). Эти обозначения легко запомнить, поскольку, если '€' — это конец переменной, то '#в' — это все, что стоит до него, а '€#' — все, что стоит после. ► Упражнение 20.4 В какую последовательность лексем будет разлагаться 'p5dir', если ранее было дано определение 'vardef p[]dir=(#edx,#©dy) enddef? ► Упражнение 20.5 Как можно выделить из подставляемого текста переменной-макроса vardef а [] Ъ [] первый индекс? (Вы должны получить не 'alb', а '1'.) <£><£>► Упражнение 20.6 JL JL Скажите системе METAFONT: 'showvariable incr,z', и объясните ее ответ.
190 Глава 20: И снова о макросах Определение переменной-макроса отменяет все определения макросов и объявления типов для переменных, имена которых совпадают с началом имени определяемой переменной. Например, если мы дадим определение 'vardef а', то исчезнут все такие переменные, как 'а.р' и 'а1Ь2', а определение 'vardef a.s' уничтожит такие переменные, как a.s.p, и т.п. Более того, после определения 'vardef а' вы уже не сможете указать 'pair а.р' или 'vardef а[]', поскольку такие переменные будут недоступны. Правила синтаксиса для (определений) в главе 18 остались неполными, поскольку там мы опустили правила для ("шапки" переменной-макроса) и ("шапки" бинарной операции). Вот эти недостающие правила: ("шапка" переменной-макроса) —¥ vardef (объявляемая переменная)("шапка" параметров) | vardef (объявляемая переменная) в# ("шапка" параметров) ("шапка" бинарной операции) —> (определение уровня) (параметр) (символьная лексема) (параметр) (определение уровня) —> primarydef | secondarydef | tertiarydef (параметр) —> (символьная лексема) Новыми здесь являются определения уровня primarydef, secondarydef и tertiarydef, которые позволяют вам самостоятельно пополнять "репертуар" системы METAFONT новыми бинарными операциями. Например, оператор 'dotprod' определяется в приложении В следующим образом: primarydef w dotprod z = (xpart w * xpart z + ypart w * ypart z) enddef. Таким образом, синтаксис METAFONT для выражений пополняется новым правилом (вторичное числовое) —У (вторичная пара) dotprod (первичная пара), которое вводит дополнительный вид формул типа (вторичное числовое). Может показаться, что имена определений уровня primarydef, secondarydef и tertiarydef "уменьшены на единицу", поскольку они определяют операторы с приоритетом предшествования на уровень выше, чем можно было бы предположить. Так, primarydef определяет бинарный оператор, который формирует вторичную формулу из вторичной и первичной формул; такие операторы имеют тот же уровень, что и операторы V и 'rotated'. При помощи secondarydef определяется бинарный оператор, который формирует третичную формулу из третичной и вторичной формул; такие операторы имеют тот же уровень, что и операторы '+' и 'or'. Посредством tertiarydef определяется бинарный оператор, который формирует выражение из выражения и третичной формулы; уровень предшествования таких операторов такой же, как у операторов '<' и '&'. Макрос 'intersectionpoint' вводится в базовом форматном файле посредством определения типа secondarydef, поскольку он, как и макрос 'intersectiontimes', применяется на уровне 'вторичная формула —> третичная формула'. secondarydef р intersectionpoint q = begingroup save x_, t/_; (x_, у J) = p intersectiontimes q\ if x_ < 0: err message ("The paths don't intersect"); (0,0) else: .5 [point x_ of p, point t/_ of q] fi endgroup enddef; Заметьте, что разделители begingroup и endgroup здесь необходимы; они не вставляются автоматически, как это было бы в случае определения типа vardef. ► Упражнение 20.7 Определите макрооперацию 'transum', которая бы давала сумму двух преобразований. (То есть, если Ьз = ti transum £2, то z transformed £3 = z transformed t\ + z transformed £2 для любой пары z.) фф
Глава 20: И снова о макросах 191 Ну вот мы и рассмотрели все типы (определений). Самое время остановиться и взглянуть на картину в целом. В результате "пережевывания" системой МETA- FONT входной файл превращается в длинную последовательность лексем, как объяснялось в главе 6, и процесс "пищеварения" системы направлен на переработку именно этой последовательности. Когда система METAFONT собирается "переварить" символьную лексему, она проверяет ее текущее значение и в определенных случаях, прежде чем произвести соответствующее вычисление, раскладывает в последовательность других лексем. Эта процедура разложения применяется к макросам, лексемам if и for, а также к некоторым другим специальным примитивам, которые мы рассмотрим ниже. Разложение продолжается до тех пор, пока не будет встречена неразложимая лексема; после этого процесс "пищеварения" системы может быть продолжен. Однако иногда разложение не выполняется; например, "переварив" лексему def, система METAFONT прекращает все разложения до тех пор, пока ей не встретится соответствующая лексема enddef. С полным перечнем всех случаев, когда лексемы не раскладываются, мы познакомим вас чуть позже. Давайте теперь рассмотрим все лексемы подлежащие разложению, если только оно не запрещено. ■ Макросы. Как уже объяснялось, при разложении макроса METAFONT вначале считывает и вычисляет аргументы (если таковые имеются). (При вычислении аргументов типа ехрг и suffix процесс разложения продолжается, но внутри аргументов типа text он подавляется.) Затем макрос и его аргументы заменяются подставляемым текстом. ■ Условия. При разложении лексемы if система METAFONT считывает следующее за ней булево выражение и находит его значение. Затем, если необходимо, делает переходит к следующему булеву выражению. И это продолжается до тех пор, пока ей не встретится лексема 'fi' или условие, значение которого истинно, после чего система продолжит чтение следующей лексемы. Если происходит разложение одной из лексем 'elseif, 'else' или 'fi', то это значит, что только что закончился условный текст, поэтому METAFONT сразу переходит к замыкающему 'fi', и разложение будет пусто. ■ Циклы. При разложении лексемам 'for', 'forsuffixes' и 'forever' система METAFONT вначале считывает условия цикла, которые стоят до двоеточия, затем считывает (не раскладывая) текст цикла вплоть до лексемы 'endfor'. Наконец, она начинает раз за разом перечитывать текст, уже с разложением. Дойдя в разложении до лексемы 'exitif', METAFONT находит значение следующего за ней булева выражения и отбрасывает точку с запятой; если это выражение оказывается истинным, то текущий цикл прерывается. ■ scantokens (первичная цепочка). При разложении лексемы 'scantokens' METAFONT находит значение следующей за ней первичной формулы, которая должна иметь тип string. Затем эта цепочка переводится в последовательность лексем согласно правилам, описанным в главе 6, как если бы считывался файл, содержащий всего одну строку текста. ■ input (имя файла). При разложении лексемы 'input' само разложение будет пустым, но система METAFONT приготовится читать из указанного файла до того, как она считает еще какие-либо лексемы из текущего источника. При этом на (имя файла) налагаются специальные ограничения, описанные на следующей странице. ■ endinput. При разложении лексемы 'endinput' само разложение пусто. Но в следующий раз, когда система METAFONT дойдет до конца строки в файле, вводимом при помощи команды input, она прекратит чтение из этого файла. ■ expandafter. При разложении лексемы 'expandafter' система METAFONT сначала считывает еще одну лексему, назовем ее £, не раскладывая. Затем METAFONT считывает лексему, следующую за t (и, возможно, еще какие-то лексемы, если эта лексема содержит аргумент), заменяя ее соответствующим разложением. Наконец, METAFONT вставляет t обратно, помещая ее перед этим разложением. ■ V Разложение лексемы 'V всегда нулевое, т.е. пустая лексема.
192 Глава 20: И снова о макросах /gK/gK Правила синтаксиса для (имени файла) в системе METAFONT не являются стан- JL JL дартными для всех реализаций, поскольку в разных операционных системах используются различные соглашения. Вам нужно спросить у администраторов вашей местной системы, каким образом представляются имена файлов. Ситуация усложняется еще и тем, что процесс перевода в лексемы системой METAFONT необратим; например, *хОГ и 'xl.O' дадут одинаковые последовательности лексем. Поэтому METAFONT даже не пытается переводить имя файла в лексемы; операция input должна встречаться только в текстовом файле и не может содержаться в списке лексем, таком как подставляемый текст макроса! (Это ограничение можно обойти, указав scant о kens "input f о о" или в общем случае scantokens ("input " k. fname), где /name — это переменная-цепочка, содержащая имя файла, который вы хотите ввести.) Несмотря на то что для имен файлов нет стандартных правил синтаксиса, во всех реализациях системы METAFONT в качестве имени файла может использоваться последовательность из шести или менее обычных букв и/или цифр. Во многих системах заглавные буквы и соответствующие им строчные буквы различаются. /gK/gK И вот, наконец, обещанный полный перечень случаев, в которых разложимые лек- JL JL семы не раскладываются. В некоторых из этих ситуаций фигурируют примитивы, которые мы пока еще не рассматривали, но до которых обязательно доберемся. Разложение подавляется в следующих случаях: ■ При стирании лексем во время исправления ошибки (см. главу 5). ■ Если METAFONT перескакивает через лексемы, игнорируя условный текст. ■ Когда METAFONT считывает определение макроса. ■ Когда METAFONT считывает текст цикла или символьную лексему, следующую непосредственно за for или forsuffixes. ■ Когда METAFONT считывает text'oBbift аргумент макроса. ■ Когда METAFONT считывает начальную символьную лексему (объявляемой переменной) в объявлении типа. ■ Когда METAFONT считывает символьные лексемы, которые должны быть определены посредством команды delimiters, inner, let, newinternal или outer. ■ Когда METAFONT считывает символьные лексемы, которые должны быть выведены на экран посредством команд showtoken или showvariable. ■ Когда METAFONT считывает лексему, которая стоит после команды expandafter или everyjob, или после знака '=', следующего за let. Процесс разложения не подавляется при считывании суффикса, следующего за начальной лексемой в (объявляемой переменной), и даже при считывании ("шапки" переменной-макроса) .
Глава 20: И снова о макросах 193 Двое наместников, препятствовавших его продвижению, — Фонтеус Капито в Германии и Клавдиус Макрос в Африке — были свергнуты. — СВЕТОНИЙ, Sergius Sulpicius Galba (с. 125 A.D.) Введение макрокоманд в язык-источник позволяет разработчику добиться той же легкости программирования, какую обеспечило бы расширение множества доступных элементарных операций. Однако, естественно, при этом не может быть и речи об экономии объема памяти и машинного времени, которую мы бы получили при реальном расширении набора встроенных операций. — О. ДОППИНГ, Компьютерная обработка данных (1970)
21 Случайные числа
Глава 21: Случайные числа 195 Забавно поиграть с системой METAFONT, сочиняя для нее программы с элементами случайности. Вы можете генерировать фигуры самого непредсказуемого вида, а также вносить бессистемные возмущения, чтобы нарушить строгую симметрию, которая обычно присуща математическим конструкциям. Музыканты, которые используют компьютеры при сочинении своих произведений, давно заметили, что музыка становится более "живой", если ее ритм сделать чуточку неправильным и сбивчивым. И наоборот, что может быть скучнее, чем идеально размеренные, монотонные удары пульса: 1-2-3-4? С тем же явлением мы можем столкнуться и в печатном деле. Система METAFONT позволяет вносить в начертания контролируемую неопределенность двумя способами: (1) функция 'uniformdeviate V выдает случайное число it, которое равномерно распределено на промежутке от 0 до t\ (2) команда 'normaldeviate' выдает случайное число х, которое имеет так называемое нормальное распределение с нулевым средним и единичной дисперсией. /$\ Точнее, если t > 0 и и = uniformdeviate £, то мы имеем 0 < и < t, и для каждого JL дробного числа р, такого, что 0 < р < 1, число и будет лежать в интервале 0 < и < pt с вероятностью, примерно равной р. Если же t < 0, то результаты будут такими же, но с точностью до замены знака; при этом 0 > u> t. Наконец, если t = 0, то мы всегда имеем и = 0; это единственный случай, когда возможно равенство u = t. f Число ж, которое выдает оператор 'normaldeviate', будет положительным в половине случаев, а в другой половине случаев — отрицательным. Эта величина имеет "колоколообразное" распределение в том смысле, что каждое конкретное значение х появляется с вероятностью, примерно пропорциональной е~х ^2; график этой функции по форме напоминает колокол. Вероятность того, что |х| < 1 составляет около 68%; того, что |х| < 2 — около 95%; того, что |х| < 3 — около 99.7%. И уж совсем смело можно держать пари, что |х| < 4. Вместо того чтобы объяснять особенности этого вероятностного поведения при помощи одних лишь математических формул, мы можем наглядно представить его результаты, дав системе METAFONT возможность нарисовать для нас несколько "засеянных у часков". Рассмотрим следующую программу, которая рисует квадрат размера 10 pt х 10 pt и еще 100 маленьких точек внутри него: beginchar (incr corfe, 10р£#, 10р£#, 0); pickup pencircle scaled .3p£; draw unitsquare scaled w; pickup pencircle scaled lpt; for к = 1 upto 100: drawdot (uniformdeviate tu, uniformdeviate iu); endfor endchar. Повторив свой эксперимент десять раз, мы получим вот такие результаты: А если мы заменим 'uniformdeviate w1 на '.5tu 4- w/6 * normaldeviate', то получим шщшшшшштшш Наконец, если мы укажем 'drawdot(uniformdeviate tu, .5w -f гу/б * normaldeviate)7, то результаты будут представлять собой комбинацию двух предыдущих случаев: {HIS Е£ &2 &U fiS SI &S 62 Еёй SU ФЩ W1! W] ЙЯ ff?5 p*fl 1^1 ^з г*я g№
196 Глава 21: Случайные числа ► Упражнение 21.1 Рассмотрим фрагмент программы: 'if uniformdeviate 1 < 1/3: case.a else: case.b fi'. Правда ли, что условный текст case.b будет выполняться примерно в три раза чаще, чем case-a? ► Упражнение 21.2 Оператор 'uniformdeviate' обычно выдает нецелое число. Объясните, как генерировать случайные целые числа от 1 до п так, чтобы каждое значение появлялось с одинаковой частотой. ► Упражнение 21.3 Что выражает формула '(uniformdeviate l)[zi,Z2]'? ► Упражнение 21.4 Предскажите результат работы следующей программы: beginchar(incr code,100pt#,10pt#,0); for n:=0 upto 99: fill unitsquare xscaled lpt yscaled uniformdeviate h shifted (n*pt,0); endfor endchar. f* Упражнение 21.5 А что же рисует эта загадочная программа? beginchar(incr code,24pt#,10pt#,0); numeric count[]; pickup pencircle scaled lpt; for n:=l upto 100: x: =.5w+w/6*normaldeviate; y:=floor(x/pt); if unknown count[y]: count[y]:=-l; fi drawdot(x,pt*incr count[y]); endfor endchar. f Давайте попробуем немного "оживить" логотип МЕТА FONT, попросив госпожу Удачу приложить свою легкую руку, и слегка изменить положение всех ключевых точек. Для начала определим переменную-возмущение noise: vardef noise = normaldeviate * craziness enddef; параметр craziness будет контролировать величину случайного отклонения. Теперь мы можем написать следующую программу для буквы 'N' нашего логотипа: beginlogochar ("N", 15); х\ = leftstemloc + noise; Х2 = leftstemloc + noise] w — X4 = leftstemloc + noise; w — хь — leftstemloc + noise; bot уi = noise — o; top y2 = h + о + noise; Уз = 2/4 + удар + noise; bot 2/4 = noise — o; top 2/5 = h -f о + noise; z3 = whatever [za, z$]; draw 2i --22--23; draw z± - - z$; labels(l, 2, 3,4, 5); endchar. Приведенная здесь иллюстрация нарисована со значением craziness = 0, поэтому и возмущение было нулевым.
Глава 21: Случайные числа 197 Проделав три эксперимента с буквой 'N' размера 9pt при значении параметра craziness = Apt, мы получили следующие результаты. А вот что произойдет, если подобные манипуляции произвести со всеми буквами логотипа METAFONT, постепенно уменьшая значение craziness от Abpt до нуля с шагом .05р£: MELTRPCNT METAFONT METATONT METAFONT METAFONT METAFONT METAFONT METAFONT METAFONT METAFONT fB каждом сеансе работы программа, в которой фигурируют случайные числа, будет давать новый результат, так как при генерировании случайных чисел система METAFONT использует текущие значения даты и времени. Как правило, такое непредсказуемое поведение — как-раз то, что вам нужно, но оно может оказаться нежелательным, если в одном из сеансов вы вдруг увидите красивую фигуру и захотите получить ее снова. Или, например, при одном из сеансов может обнаружиться ошибка в программе, а вы не сможете отыскать ее причину, так как она, возможно, не будет повторяться. В таких случаях достаточно указать. randomseed .= (числовое выражение) и запомнить значение этого числового выражения. (Это значение будет автоматически записано в файл-стенограмму.) Используя одно и то же значение, полученное применением команды randomseed, в любых двух сеансах вы получите одну и ту же последовательность значений, выдаваемых операторами 'uniformdeviate' и 'normaldeviatc', поскольку в этом случае случайные числа системы METAFONT будут "псевдослучайными" Один мой знакомый музыкант забавлял себя тем, что, перестроив свой рояль совершенно произвольно, как в голову взбредет, исполнял на нем "Патетическую Сонату" Бетховена. Для меня было невероятным наслаждением слышать, как старое произведение вновь оживает. Я слышал эту сонату не раз в течение двадцати лет, и никогда не думал, что ее можно как-то усовершенствовать. — АВГУСТ СТРИНДБЕРГ, Случай в художественном творчестве (1894) [Образование] должно вести нас от случайности и произвольности к четкости мышления и к разумному порядку. — МАЙЕС ВАН ДЕР РО, Речь при вступлении в должность (1938)
22 Цепочки
Глава 22: Цепочки 199 Несмотря на то что система METAFONT не является текстовым редактором, в программах для METAFONT можно выполнять элементарные операции со словами и другими небольшими цепочками символов. Цепочки (strings) можно использовать как комментарии, поясняющие действия программы. Например, в файле io.mf, приведенном в главе 5, цепочка "The letter 0м используется в качестве заголовка, который будет появляться на пробных оттисках. Кроме того, в этом файле имеется цепочка "0", определяющая позицию символа в выходном шрифте. В главе 6 указывалось, что (лексема-цепочка) — это произвольная последовательность символов, заключенная в двойные кавычки (•'), при условии, что она не содержит сам этот символ. На тот случай, если вам понадобится вставить в выражение-цепочку символ двойной кавычки, в базовом формате METAFONT он определен как особая цепочка длины 1, которая называется ditto. Поэтому "символ '" & ditto к "' может содержаться в выражении-цепочке," несмотря на то, что (лексема-цепочка) его содержать не может. Выражение-цепочка само по себе может использоваться как утверждение, точно так же, как уравнение, объявление или команда; для этого непосредственно за ним должен следовать знак *; \ Такое утверждение называется заголовком (title). Если в тот момент, когда METAFONT встречает заголовок, установлено значение tracingtitles > 0, то система выведет этот заголовок на экран; если же в этот момент значение proofing > 0, то METAFONT скопирует заголовок в выходной файл, чтобы программа-постпроцессор (например, программа GFtoDVI, описанная в приложении Н) могла затем помещать его на пробных оттисках. fB приложении Н объясняется, как задавать цепочки, которые будут использоваться в качестве меток ключевых точек на пробных оттисках. /§\/$К Все операции с цепочками, за исключением сцепления ('&')? выполняются на пер- JL JL вичном уровне. Вот полные правила синтаксиса для выражений-цепочек: (первичная цепочка) —> (лексема-цепочка) | (переменная-цепочка) | ((выражение-цепочка)) | begingroup (список утверждений) (выражение-цепочка) endgroup | jobname | readstring | str (суффикс) | char (первичное числовое) | decimal (первичное числовое) | substring (первичная пара) of (первичная цепочка) (вторичная цепочка) —> (первичная цепочка) (третичная цепочка) —У (вторичная цепочка) (выражение цепочка) —> (третичная цепочка) | (выражение-цепочка) к (третичная цепочка) Новыми для нас являются лексемы jobname, readstring, str, char, decimal и substring; сейчас мы рассмотрим их все по порядку. Цепочка jobname — это название задания. Если в первой строке вашего диалога с системой METAFONT (т.е. строке, начинающейся с '**', или командной строке) вы указываете файл, который должен быть считан, то его имя и будет названием задания. В противном случае, как объяснялось в главе 5, заданию присваивается название mfput.
200 Глава 22: Цепочки Встретив лексему 'readstring', METAFONT останавливает работу и ждет, чтобы пользователь ввел строку с клавиатуры. Значение цепочки readstring будет равным содержимому этой строки, за исключением пробелов на конце. (Перед этим, наверное, стоит использовать команду message, чтобы намекнуть пользователю, что именно он должен напечатать, как это делается, например, в файле expr.mf (см. главу 8), где выражения вводятся посредством команды readstring. Способность readstring останавливать работу компьютера используется в макросе stop из базового формата METAFONT (см. приложение В); при этом сама эта цепочка роли не играет.) Произвольный (суффикс) преобразуется в цепочку посредством команды str, которая использует тот же метод, при помощи которого система METAFONT выводит на экран аргументы типа suffix, комментируя свои действия. Отрицательные индексы заключаются в квадратные скобки; пробелы и точки вставляются между лексемами, символы которых принадлежат одному классу (см. таблицу в главе 6). Например, если п = 1, то 'str z[n]a' есть мх1а", а 'str хпо' есть "х.п.а". Команда 'char п' дает в результате цепочку длины 1, представляющую собой символ с ASCII-кодом, равным п. (Кодировка ASCII описана в приложении С.) Значение п сначала округляется до ближайшего целого; затем, если необходимо, к нему прибавляется или из него вычитается число, кратное 256, до тех пор, пока значение п не попадет в интервал О < п < 256. Таким образом, величина char п определена во всех возможных случаях. Десятичное представление известной числовой величины можно получить в виде цепочки посредством команды 'decimal х\ Если величина х отрицательна, то первым символом в цепочке будет '-'. Если число х — дробное, то в его представление будет включена точка, отделяющая целую часть от дробной; цифр в дробной части будет столько, сколько необходимо для того, чтобы охарактеризовать данное число. (Принятые на этот счет соглашения были проиллюстрированы на примерах в главе 8.) Правила для подцепочек похожи на правила для подпутей, описанные в главе 14. METAFONT рассматривает цепочку так, как если бы ее символы были вписаны в квадратики миллиметровки с координатами от х = О до х = п, где п — длина цепочки. В простом случае команда 'substring(a,6)' обращается к символам, стоящим между х = а и х = 6. Правила для общего случая немного сложнее. Если Ъ < а, то результатом будет цепочка 'substring(а, 6)', записанная в обратном порядке. В остальных случаях а и Ь заменяются числами max(0, min(n, rounda)) и тах(0, min(n, round Ь)), соответственно; это приводит нас к описанному выше простому случаю 0 < a < 6 < п, когда длина цепочки равна Ъ — о. Цепочки можно переводить в числа, хотя этот факт и не упоминался в правилах синтаксиса для (первичного числа) величин, которые приводились в главе 8. Эти примитивные операции имеют вид ASCII (первичная цепочка) | oct (первичная цепочка) | hex (первичная цепочка) Команда ASCII возвращает ASCII-код первого символа цепочки, команда 'oct' вычисляет целое число, которое соответствует данной цепочке в восьмеричной системе счисления, а команда 'hex' вычисляет целое число, которое соответствует данной цепочке в шестнадда- теричной системе счисления. Например, ASCII п100м = 49; oct "100" = 64; hex "100" = 256. Нужно отметить несколько особых условий: (1) ASCII ""= —1; в остальных случаях команда ASCII дает целое число между 0 и 255. (2) Символы в цепочке, которая является аргументом команды 'oct', должны быть цифрами от 0 до 7. (3) Символы в цепочке, которая является аргументом команды 'hex', должны быть цифрами от 0 до 9 и от А до п п ф$
Глава 22: Цепочки 201 F, или от а до f. (4) Результатом применения команд 'oct' и 'hex' должно быть число, меньшее 4096. Поэтому максимальные допустимые значения — 'oct и7777"' и 'hex "FFF"\ <4»><^>^ Упражнение 22.1 JL JL При каких условиях (a) ASCII char n = га? (b) char ASCII s = s? AA^ Упражнение 22.2 JL JL Почему существуют примитивные операции перевода цепочек в восьмеричные и шестнадцатеричные числа, но не существует операции перевода в десятичные? <£><£>► Упражнение 22.3 JL JL Напишите макрос octal, который бы переводил неотрицательное число в цепочку, соответствующую его представлению в восьмеричной системе счисления. /£s/$\ Прямо или опосредовано общаться с пользователем позволяет (команда message). JL JL Общие правила ее синтаксиса таковы: (команда message) —> (оператор вывода сообщения) (выражение-цепочка) (оператор вывода сообщения) —> message | errmessage | errhelp Если вы укажете 'message s\ то символы цепочки 5 будут выведены на экран в начале новой строки; то же самое произойдет, если вы укажете 'errmessage s\ с той лишь разницей, что в начале строки будет стоять "! ", после цепочки — ".", а затем — строки контекста, как в обычном сообщении об ошибке. Если после сообщения об ошибке пользователь попросит помощи, то на экран будет выведена последняя встреченная цепочка, следующая непосредственно за оператором errhelp (если только она не была пустой). /$\/$К Система METfiFONT не позволяет составлять массивы из макросов т[г\\ однако с JL JL помощью команды scantokens вы можете составить массив из цепочек, которые ведут себя подобно макросам. Эта идея использована в конструкции mode.def, описанной в приложении В. Эта теория позволяет выполнить много других полезных практических рассчетов, например найти длину цепочки. УИЛЬЯМ ЭЛИНГХЭМ, Воплощенная геометрия (1695) Мое дрожащее перо, быть может, выводит то, о чем молчат старинные преданья. Прости меня, коль так. Но как иначе мне распутать цепочку эту? — ДЖЕЙМС ТОМСОН, Сонный за'мок (1748)
23 Вывод на экран
Глава 23: Вывод на экран 203 Как сделать, чтобы рисунки появлялись на экране монитора? В базовом формате METAFONT предусмотрена специальная команда 'showit', которая выводит на экран текущий рисунок — currentpicture. Кроме того, вы можете попросить отображать символы, указав 'screenchars'; тогда METAFQNT будет автоматически выполнять команду showit после каждой команды endchar. Вы можете также следить за процессом рисования, указав 'screentokens'; в этом случае команда showit будет выполняться всякий раз после выполнения команд draw и fill. Описанные выше возможности базового формата METAFONT реализуются как макросы, использующие примитивные команды низкого уровня; определения этих макросов приведены в приложении В. На самом низком уровне METAFONT выполняет такие команды, как 'display currentpicture inwindow 1'; кроме того, имеется команда открытия окна — openwindow, которая определяет соответствие между внутренними координатами METAFONT и координатами экрана. Правила синтаксиса таковы: (команда вывода на экран) —> display (переменная-рисунок) inwindow (окно) (окно) —У (числовое выражение) (команда открытия окна) —У openwindow (окно) (параметры окна) (параметры окна) —У (положение на экране) at (выражение-пара) (положение на экране) —У from (экранные коорд-ты) to (экранные коорд-ты) (экранные координаты) —У (выражение-пара) Здесь (окно) — это целое число между 0 и 15 включительно; оно представляет одно из шестнадцати "окон" или "иллюминаторов", через которые METAFONT показывает внешнему миру свои рисунки. Для того чтобы (окно) можно было указывать в команде display, его нужно предварительно "открыть" при помощи команды openwindow. Окна системы METAFONT не следует путать с так называемыми окнами, используемыми большинством современных операционных систем. Если у вас установлена такая система, вы, вероятно, обнаружите, что, когда METAFONT выводит на экран рисунки, все они появляются в одном окне операционной системы, а все операции ввода-вывода отображаются в другом окне. И в то же самое время вы можете выполнять еще какую-нибудь задачу в третьем окне. Окна системы METAFONT не обладают такими замечательными возможностями; они представляют собой всего лишь внутренние подокна того большого окна, в котором отображаются рисунки. Команда 'openwindow k from (ro,co) to (ri,ci) at (х,у)' сопоставляет пикселям встроенной координатной системы METAFONT прямоугольную область на экране (или в том окне операционной системы, в котором отображаются рисунки). Все числа, фигурирующие в этой команде (а именно: А:, го, со, n, ci, х и у), округляются до ближайших целых, если они не являются таковыми. Кроме того, го заменяется величиной max(0, min(maxr, го)), а ri — величиной тах(го, тт(тахг, п)), где тахг — максимальное число строк на экране; похожим образом преобразуются числа со и сь Эти две пары чисел (г, с) представляют собой количество строк и столбцов на экране; за нулевую строку принимается крайняя верхняя строка, за нулевой столбец — крайний левый. (Эти соглашения, принятые для системы координат экрана, отличают ее от нормальной декартовой системы координат, повсеместно используемой в системе METAFONT. Однако по некоторым соображениям она больше подходит для описания точек экрана.) Верхний левый угол прямоугольника помещается в точку (х, у) растра системы METAFONT; то есть, точка (х, у) приравнивается к верхнему левому углу прямоугольной области экрана, образованному пересечением столбца со и строки г о экранных пикселей. Само окно содержит п — г о строк и ci — со столбцов. Следовательно, пиксель, лежащий на пересечении столбца с\ и строки п окну не принадлежит, а представляет собой экранный пиксель, диагонально сопряженный правому нижнему углу окна.
204 Глава 23: Вывод на экран f/gK* Упражнение 23.1 JL Каковы координаты границ такого окна в системе координат системы METAFONT? fEciiH METAFONT работает в операционной системе, которая не поддерживает вывод на экран с побитовым отображением, то команды display и openwindow не вызывают никаких действий. У вас не будет возможности просматривать изображения символов в интерактивном режиме; чтобы просмотреть пробные оттиски, вам придется их распечатывать. (Однако при этом METAFONT может работать чуть быстрее.) f/g\ Согласно правилам синтаксиса команда display должна применяться к (пере- JL менной-рисунку), но не к (выражению-рисунку). Поэтому вы не можете указать 'display nullpicture'. В базовом формате METAFONT имеется специальная переменная — blankpicture, представляющая собой абсолютно пустой рисунок. Так что вы можете не только ничего не выводить на экран, но и выводить на экран "ничто", когда вам вздумается. f/g\ Открывать окно можно сколько угодно раз. При каждом открытии окно будет JL занимать новое положение на экране. При открытии окна на экране очищается соответствующая прямоугольную область, как будто вы выводите на экран пустой рисунок. /gN/^N Эффект от наложения окон может быть каким угодно, поскольку METAFONT не JL JL всегда перерисовывает заново пиксели, не изменявшиеся в промежутке между выводами на экран. f/£\ Изменения рисунка не меняют результаты его выводов на экран до тех пор, пока JL вы прямо не дадите команду выполнить новый вывод. Поэтому, может статься, что рисунок, который вы видите на экране, в памяти METAFONT уже не существует. f/£\ В базовом формате METAFONT имеется макрос 'openit, который открывает окно JL currentwindow. Значение переменной currentwindow всегда равно нулю, если только вы сами его не измените. Макрос showit выводит рисунок currentptcture в окне currentwindow, а также самостоятельно вызывает команду openit, но только при первом вызове команды showit Это значит, что экран обычно будет оставаться нетронутым до тех пор, пока вы не попытаетесь на него что-нибудь вывести. f/gK В приложении Е описан метод работы по более сложной схеме, когда при помощи JL шести окон вы можете проследить изменения, происходящие с метасимволом, при установке шести различных наборов параметров шрифта. Автор этой книги использовал эту шестиоконную систему при разработке шрифтов семейства Computer Modern; вот типичный пример того, что появляется на экране при доработке буквы 'а'.
Глава 23: Вывод на экран 205 /^/$\* Упражнение 23.2 YY В макросе openit, приведенном в приложении В, точка (—50,300) указывается как верхний левый угол окна, используемого для показа всех рисунков. Из-за этого нижняя часть большого символа может оказаться "срезанной", если, скажем, максимальное количество строк на вашем экране равно 360. Как изменить макрос openit, чтобы символы отображались на экране "поднятыми" на 20 строк вверх, по сравнению с их отображением при стандартных установках? <£><£>► Упражнение 23.3 JL JL Напишите программу new_window, которая бы размещала на экране окна 1, 2, ..., 15. Если пользователь указывает 'nev.vindow $(u, v)', где $ — произвольный суффикс, a u,v — пары координат двух противоположных углов прямоугольника, макрос должен разместить этот прямоугольник в следующей свободной прямоугольной области экрана и открыть его как окно с номером window$. Размещать окна он должен слева направо и сверху вниз, в предположении, что экран имеет вид бесконечно большого прямоугольника шириной screen-cols. Редактирование будет производиться в интерактивном режиме, с использованием возможностей вывода на экран и ввода с клавиатуры. — РИЧАРД Л. ВЕНЕЦКИ, в American Documentation (1968) В будущем мне, возможно, придется вновь писать для трубы. — ИГОРЬ СТРАВИНСКИЙ, в Harper's (1970)
24 Дискретность и дискретизация
Глава 24-' Дискретность и дискретизация 207 Массивы пикселей практически неотличимы от сплошных линий, если размеры пикселей достаточно малы. Ведь в конце концов человеческий глаз состоит из дискретного набора рецепторов, а видимый свет — из волн конечной длины. Если бы гипотетический принтер luxo (см. главу 11) с разрешением 2000 пикселей на дюйм действительно существовал, то его печать была бы очень высокого качества; все мельчайшие неровности сглаживались бы за счет физических свойств чернил, полностью скрывая от глаза тот факт, что начертания символов подверглись оцифровке. Однако работать с устройствами с низким разрешением всегда выходит дешевле, и нам хотелось бы, чтобы вывод системы METAFONT выглядел как можно лучше на устройствах, которые мы можем себе позволить. В этой главе мы обсудим принципы "дискренного приближения", то есть рассмотрим элегантные математические приемы, использование которых в системе METAFONT позволяет получать вполне удовлетворительные результаты даже при довольно грубом разрешении. Весь технический материал в этой главе отмечен знаком опасного поворота, поскольку тщательно проводимое округление усложняет программы; неискушенному пользователю незачем беспокоиться о таких тонкостях. С другой стороны, матерый METAFONT-щик подходит к округлению очень аккуратно даже при разработке шрифтов с высоким разрешением, так как тонкие поправки, которые мы сейчас обсудим, зачастую способны значительно улучшить начертание. С самого начала следует уяснить, что надеяться на что-то сверхъестественное было бы ошибкой. Нельзя ожидать, что буквы, полученные механически без вмешательства человека, смогут конкурировать с алфавитами, старательно разработанными специально для конкретного устройства. Все, что мы можем, — рассматривать букву и изменять ее пиксели до тех пор, пока результат не улучшиться. Поэтому, мы должны добиваться не совершенства, а только приемлемости. Давайте создавать такие меташрифты, чтобы хотелось изменить не более нескольких пикселей в каждом символе, скажем, полдюжины, независимо от разрешения. При низком разрешении шесть пикселей — это, конечно, относительно большая цифра, но и при высоком разрешении шесть тщательно продуманных изменений пикселей способны привести к значительным улучшениям. Суть в том, что, если мы довели разработку до этой стадии, то человек, работающий с хорошей программой редактирования побитового отображения, сможет менее чем за час оптимизировать наш шрифт. Эта цель вполне достижима, если с умом подходить к округлению. fEoiH внутренние величины autorounding и/или smoothing имеют положительные значения, то система METAFONT автоматически старается подстраивать кривые таким образом, чтобы они хорошо растрировались. (В базовом формате METAFONT по умолчанию устанавливаются значения autorounding := 2 и smoothing := 1, поэтому, как правило, эти возможности системы будут использоваться, если только вы сами их не отключите.) Однако все примеры в данной главе будут генерироваться при autorounding := smoothing := 0 (если не оговорено противное), так как это удержит автоматический механизм системы METAFONT от вмешательства в наши эксперименты. Мы обсудим все "за" и "против" автоматического округления после подробного изучения проблемы округления в целом. f Первое, что мы должны понять в процедуре округления — это то, как система METAFONT оцифровывает путь. Путь длины п можно рассматривать как траекторию z(t), описываемую при изменении t от 0 до п. В этих терминах соответствующий оцифрованный путь проще всего описывает формула 'round z(t)\ где 0 < t < п; каждое z(t) округляется до ближайшей точки с целыми координатами. Например, если путь проходит через точку (3.1,5.7), то соответствующий оцифрованный путь пройдет через точку (3,6).
208 Глава 24-' Дискретность и дискретизация При некоторых значениях t оцифрованная траектория совершает дискретные скачки, когда значение round z(t) перескакивает с одной точки на другую; эти две точки отстоят друг от друга на один пиксель, и мы можем представлять себе, что в момент скачка оцифрованный путь пересекает горизонтальную или вертикальную границу между ними. fllpn заливке обычной области это правило оцифровки путей сводится к простому критерию, имеющему наглядную интерпретацию: пиксель принадлежит оцифрованной области тогда и только тогда, когда его центральная точка лежит внутри исходной неоцифрованной области. В качестве примера рассмотрим два варианта "ионической" буквы 'О' (см. главу 5), которые приведены здесь с разрешением 200 пикселей на дюйм, и с использованием характеристик режима lowres из приложения В: Жирные ломаные линии обозначают границы оцифрованных путей, а пиксели, лежащие внутри этих границ, — это те, центры которых попадают в затененные области. f Буква 'О' слева хорошо поддалась оцифровке. А вот с той, что справа, возникли проблемы, поскольку лежащие в ее основе линии были построены без учета растра. Различие между этими двумя буквами целиком обусловлено строкой 8 программы, приведенной в главе 5. curvesidebar — round l/18em; Это уравнение определяет положение крайней левой и крайней правой границ буквы 'О' до оцифровки, и в конечном счете приводит к симпатичному оцифрованному начертанию (рисунок слева). Опустив лексему 'round', мы получаем низкокачественное начертание (рисунок справа), которое получено при помощи той же программы, но со значением curvesidebar, равным в точности 1/18ет. Всего одна лексема, которая заменила точное вычисление приближенным, — и такая разница результатов! f Неправильное размещение кривой на растре, может вызвать настоящую цифровую катастрофу, даже если сама по себе кривая вовсе не плоха. Представьте, например, что мы сдвинули правую фигуру всего лишь на 0.05 и 0.10 пикселя вправо: Первый сдвиг на 0.05 пикселя вызовет появление крошечного пупырышка на правой границе фигуры; после второго небольшого сдвига пупырышек превратиться в бородавку, а левый край станет слишком плоским.
Глава 24: Дискретность и дискретизация 209 f Разработчик, получивший задание построить оцифрованную букву 'О' шириной 22 пикселя, в процессе разработки, конечно же, будет постоянно представлять себе расположение пикселей. Поэтому вполне естественно, что в программе, генерирующей букву 'О', мы должны позаботиться о фактическом положении пикселей, округлив величину curve-sidebar, как в данном примере. Таким образом, еще перед тем, как произвести оцифровку, мы чуточку искажаем кривую с бесконечным разрешением так, чтобы она лучше оцифровывалась. fllyTb z(t) будет лучше поддаваться оцифровке, если она не слишком сильно его изменяет. Таким образом, желательно, чтобы на важных участках путь z(t) фактически совпадал с путем roundz(t). Но какие участки считать "важными"? Опыт показывает, что критическими являются те точки пути, в которых движение происходит горизонтально или вертикально, то есть участки, где путь проходит параллелельно линиям растра. Для линии лучше всего выбирать такое расположение, чтобы на участках, где она становится параллельной линиям растра, она касалась или почти касалась этих линий; тогда она будет иметь правильную кривизну после оцифровки. Самое плохое — это когда кривая становится параллельной растру, находясь на равном расстоянии от соседних линий растра; тогда на ней появляется либо пупырышек, либо плоская клякса. /$\/$\ Диагональные участки пути, на которых кривая имеет угол наклона ±45°, — JL JL еще один потенциальный источник нежелательных прыщевидных образований и плоских клякс. Точно так же при высоких разрешениях иногда можно выявить наличие мелких "бликов" на тех участках, где кривая имеет угол наклона, равный ±1/2 или ±2/1. Вообще, в "группу риска" попадают углы наклона, имеющие рациональные значения тп/п, где тип — небольшие целые числа. Но диагональные участки по значению стоят все же на втором месте; вертикальные и горизонтальные участки вызывают гораздо более серьезные проблемы. fH3 этих соображений можно вывести простой общий принцип адаптации контуров фигуры к оцифровке: Если вы знаете, что контур будет иметь в некоторой точке вертикальную касательную, округлите координату х этой точки до целого числа, а координату у оставьте прежней. Если вы знаете, что контур будет иметь в некоторой точке горизонтальную касательную, округлите координату у этой точки до целого числа, а координату х оставьте прежней. /gK/gK Кстати, в наших примерах о точках, где контур буквы 'О' проходит горизонтально, JL JL позаботилась команда 'define_corrected_pixels', которая делает значение параметра перелета о почти целым, и команда beginchar, которая делает целым значение h. Если бы координаты у не были округлены в точках с горизонтальной касательной, то наши плохие примеры выглядели бы еще хуже. f Прежде чем продолжить изучение округления, мы должны обсудить одну техническую деталь, которая иногда оказывается важной. Ранее мы говорили, что оцифрованной области принадлежат те пиксели, центры которых лежат в неоцифрованной области; но это правило ничего не говорит о том, что происходит, когда центры пикселей попадают точно на границу неоцифрованной области. Точно так же, когда мы говорили, что путь round z(t) перескакивает с одной точки на другую, смежную точку, мы игнорировали тот факт, что такая кривая, как z(t) = (t,t), будучи оцифрованной перескакивает с точки (0,0) на точку (1,1), когда значение t переваливает за 1/2, хотя эти точки смежными не являются. Система METAFONT обходит обе эти проблемы интересным образом: она сдвигает все пути вправо на бесконечно малую величину 6 и вверх на еще более малую величину бе так, чтобы ни один из путей не касался центра какого-либо пикселя. Величины S и б представляют собой положительные числа, которые выбраны малыми настолько, чтобы их значениями можно было пренебречь. Например, путь z(t) = (t, t) превращается тогда в путь (t + 6, t + бе), который перескакивает сначала с (0,0) на (1,0), а уже затем — с (1,0) на (1,1), так как при t = 1/2 — 26е он мгновенно округляется до (1,0).
210 Глава 24: Дискретность и дискретизация Точки вида (га+1/2,п+1/2), где тип — целые числа, лежат в центрах пикселей. Эти точки называются "точками неоднозначности", так как при округлении такой точки до ближайшей к ней точки с целыми координатами нам приходится делать выбор, какую из четырех смежных точек считать ближайшей. Если мы представим себе, что контур кривой медленно смещается вправо, то его оцифрованный образ будет резко меняться при пересечении контуром точек неоднозначности. Путь и его оцифрованный образ разняться более всего, когда этот путь проходит вблизи точки неоднозначности. Поэтому точки неоднозначности — это точки нестабильности, и, чтобы оцифровка давала лучший результат, нужно, чтобы путь не слишком сильно приближался к таким точкам. Теперь давайте рассмотрим, что происходит, когда мы не заливаем контур, а рисуем пером при помощи команды draw. На первый взгляд может показаться, что простейшая из возможных команд draw выглядит примерно так: pickup pencircle; draw (0,0) .. (10,0); В самом деле, что может быть проще? Но при более пристальном рассмотрении оказывается, что это едва ли не самый худший из возможных случаев! Верхняя и нижняя границы линии, проводимой круглым пером диаметра 1 из точки (0,0) в точку (10,0), пробегают отрезки между точками (0, ±1/2) и (10, ±1/2); и обе эти границы буквально пропахивают множество точек неоднозначности. Системе METAFONT приходится решать, производить ли ей заливку строки пикселей с координатой у, лежащей в пределах 0 < у < 1, или же нижней строки, в которой — 1 < у < 0; ни одна из этих строк не центрирована на данной линии. Следуя приведенному выше правилу, METAFONT сдвигает путь немного вправо и совсем немного вверх; таким образом, фактически осуществляется заливка пикселей, лежащих в области с границей (0,0) -- (10,0) -- (10,1) -- (0,1) -- cycle. <£) ►Упражнение 24.1 Какие пиксели были бы залиты, если бы путь имел вид '(0,0) .. (10, — epsilon)1 ? Вообще, когда мы рисуем фиксированным пером при помощи команды draw, качество оцифровки зависит от того, куда попадут края линии, вычерчиваемой пером, а не от того, где будет пролегать траектория, описываемая центром пера. Так, например, если путь, который мы рисуем, имеет вертикальную касательную в точке zi, не нужно чтобы xi было целым числом, а нужно, чтобы целыми были Iftxi и rtxi. Если же путь имеет горизонтальную касательную в точке 22, то нужно, чтобы topyi и bot 3/2 были целыми числами. Все перья, полученные из pencircle, обладают тем свойством, что разности (Ift х) — (rt х) and (top у) — (bot у) представляют собой целые числа; следовательно, края будут попадать либо в удачное, либо в неудачное положение одновременно. Допустим, нам нужно, чтобы xi примерно равнялось а и чтобы относительно выбранного на данный момент пера точка с такой координатой х представляла собой удачное место для прохождения касательной в вертикальном направлении. Один из способов определить xi — это указать: Ift xi = ro\md(lft а). Это приводит к желаемому результату, поскольку делает Iftxi целым числом и в то же время xi « а. Точно так же, чтобы приспособить уг « Р для прохождения горизонтальной касательной, мы можем указать: top j/2 = round(top /?). Такие операции на практике приходится выполнять очень часто, поэтому в базовом формате METAFONT для них предусмотрены специальные обозначения. Вместо того чтобы идти в обход, используя уравнения для Ift х\ и top 3/2, мы можем прямо указать: xi = good.x q; г/2 = good.y /?.
Глава 24: Дискретность и дискретизация 211 f Давайте еще раз обратимся к буквам логотипа METAFONT для того, чтобы выполнить правильное округление. В главе 11 мы уже рассматривали файл logo.mf, который рисует эти семь символов, но внеся в него пиксельно-ориентированные поправки мы можем улучшить результаты. В первую очередь мы должны заменить команду define_pixels(s, и, хдар, удар, leftstemloc, barheight) чем-то получше. Внимательно присмотревшись к использованию специальных единиц длины, мы видим, что величины хдар и удар должны быть целыми числами; величина leftstemloc должна равняться good.x при значении пера, равном logo-pen; а значение величины barheight должно быть равным good.у. Поэтому достаточно указать define_pixels(s, и); define, whole-pixels (хдар, удар); define_good_x_pixels( leftstemloc); define_good_y .pixels (barheight); и эти команды, имеющиеся в базовом формате METAFONT, сделают все как надо. (Перед тем как воспользоваться двумя последними командами, следует выбрать перо logo-pen.) Этих немногих изменений и одной поправки в букве 'О' вполне достаточно, чтобы исправить все буквы логотипа, за исключением *Т\ f> Упражнение 24.2 В главе 18 была приведена программа для буквы 'О' логотипа METAFONT. Какие поправки нужно в нее внести, чтобы начертание лучше оцифровывалось? fC буквой 'Т' у нас возникает новая проблема, так как мы хотим, чтобы ее правая и левая половины были симметричны. Если ширина пера есть число нечетное, то желательно, чтобы ширина символа w была нечетным числом; тогда количество пикселей слева и справа от ножки буквы будет одинаковым. Если же ширина пера выражается четным числом, то желательно, чтобы и w было четным. Таким образом, шансы на то, что значение w, вычисленное командой beginchar, окажется подходящим — пятьдесят на пятьдесят. f ►Упражнение 24.3 Докажите, что значение w будет подходящим для ' Т' относительно пера logo.реп тогда и только тогда, когда .bw есть подходящее значение х для вертикальных касательных. fEora значение w — плохое, то желательно заменить его либо на w + 1, либо на w — 1, в зависимости от того, какое из этих чисел ближе к той аппаратно- независимой величине ширины, из которой округлением получено w. К примеру, если w округлено до 22, исходя из идеальной ширины 21.7, то желательно заменить ее значение на 21, а не на 23. В базовом формате METAFONT эту процедуру выполняет подпрограмма change.width. Поэтому вместо простенькой программы для буквы 'Т', полученной как ответ к упражнению 11.4, мы получаем следующее: beginlogochar("T", 13); if .bw О good.x .5w: change.width; fi Ift xi = — eps; X2 = W — Xi] хз = X4 = .bw, 2/1=2/2=2/35 topyi=h; botyA = -o; draw z\ --22; draw 23 -- z\\ labels(l,2,3,4); endchar. В главе 4 было сказано, что буква 'Т' самая простая из семи букв логотипа, но, оказывается именно она — самая сложная.
212 Глава 24- Дискретность и дискретизация f/gK У этой программы имеется одна особенность, оставшаяся необъясненной. Почему JL значение Iftxi было установлено равным —ер$, а не нулю? Для ответа на этот вопрос нужно хорошо представлять себе многоугольники, соответствующие перьям, о которым мы говорили в главе 16. Если центр пера имеет целые или полу целые координаты, то вероятность того, что стороны этих многоугольников будут пересекать точки неоднозначности, весьма велика. Чтобы избавиться от неопределенности, система METAFONT сдвигает пути вправо и вверх; таким образом, если точки неоднозначности имеются на левом и правом краях буквы 1Т\ то при таком сдвиге на левом краю несколько пикселей будет утеряно, а на правом, наоброт, появится несколько лишних. Константа eps, равная 0.00049, мала, но все же больше нуля настолько, что не замечать ее система METAFONT, конечно же, не может. Вычитание величины eps из х\ и сложение ее с хг позволяет избавиться от точек неоднозначности, сохраняя результат симметричным. Поскольку величина перелета 'о' — всегда целое число плюс eps, нет необходимости делать, что-либо подобное с точкой z\\ достаточно уравнения lbot у а = —о1. Точка 2з в центре буквы *М' раположена приемлемо, поскольку bot уз = у gap — о. Если бы значение bot уг было целым числом, то буква 'М' очень часто оказывалась бы несимметричной из-за точек неоднозначности на границе вблизи от «гз- f/gk^ Упражнение 24.4 JL Верно ли, что, если значение пера currentpen равно pencircle xscaled рх yscaled ру, то изображение, полученное при помощи команды 'draw (— epsilon, 0) .. (+epst/on,0)\ будет симметричным как относительно вертикальной, так и относительно горизонтальной оси. (В предположении, что autorounding = smoothing =0.) )► Упражнение 24.5 Многоугольник, соответствующий перу 'pencircle scaled 3', представляет собой восьмиугольник с вершинами в точках (±0.5, ±1.5) и (±1.5, ±0.5). Докажите, что, если нарисовать что-либо таким пером при помощи команды 'draw (х,у)', результат никак не может быть и вертикально, и горизонтально симметричным. Округление может быть полезным не только при размещении точек, в которых кривая имеет вертикальную или горизонтальную касательную. Рассмотрим, например, "знак точной величины", или "диез", который рисует следующая программа: и# := ||pt#\ define_pixels(n); beginchar (0,15t*#, ^p*# |§P*#); pickup pencircle scaled (Apt + blacker)] Ift xi = round и — eps\ хз = X\\ X2 = X4 = w — Xi] yi = У2 = good.y (.5[-d, h] + pt)-, Уз = 2/4 =h-d- t/i; draw zi -- zi\ draw zz -- za\ Ift Хб = round Згх; x7 = w — же; X8 = good.x .bw] Xs — Хб = X7 — £8; top у5 = topy-r = h + eps; hot ye = bot ye = — d — eps; draw Zb -- 26; draw 27 -- z%\ labels (range 1 thru 8); endchar.
Глава 24: Дискретность и дискретизация 213 Оцифровав этот символ в режиме lowres с разрешением 200 пикселей на дюйм, мы получим следующие результаты: Пример слева мы получили, опустив операции 'round' и ''good.x1 в уравнениях, определяющих Хб и Х8. Это значит, что точки Z& и zs попали в разные, возможно, неудачные положения на растре, поэтому две диагональные линии после оцифровки выглядят по- разному, несмотря на то, что получены они из практически идентичных неоцифрованных линий. Пример посередине получен посредством приведенной выше программы без каких- либо изменений. А вот в примере справа диагональные линии были нарисованы более хитро: команды 'draw zs -- z§\ draw zj -- z% были заменены на 2/15 = t/i; 2i5 = whatever [zb,z&\, узе = уз; 2зб = whatever[zb,zo\\ У27 = У2; 227 = whatever [z7,z8]', У48 = У4; 248 = whatever [z7,z&]; draw z5 -- {good.x(xi5 + 5),yi) -- (good.x(xi5 - .5),yi) -- (good.x(x36 + -5), уз) -- (good.x(xz% - .5),уз) --26; draw z7 -- (good.x(x27 + -5),уг) -- {good.x{x27 - -5),уг) -- (good.x(x48 + -5), j/4)-- (good.x(x4s - -5),y4) --28; Идея этой замены заключалась в том, чтобы проконтролировать удачность расположения точек в местах, где диагональные линии пересекают поперечные, и спрятать в каждой из поперечин по одному "зубчику". Проделав то же самое при утроенном разрешении, мы получим те же результаты, но разница между ними уже не будет столь очевидной: f Когда буквы рисуются посредством заливки контура, левый и правый контуры оцифровываются независимо друг от друга. Поэтому, как правило, желательно, чтобы для соответствующих контуров разности между одноименными величинами выражались целыми числами. Допустим, при изображении буквы 'п' мы используем команды penpos2(stem,i))] penpos^stem^) для задания ширины линий в основаниях ее ножек. Тогда Х2г — ^2/ = %4г — хы = stem. Если значение stem не является целым (скажем, stem = 2.7), то возможен случай, когда Х2/ = 2.1, хгг = 4.8, X4i = 9.6 и Х4г = 12.3. В этом случае округление разности Х2г — ^2/ даст 5 — 2 = 3, так что ширина левой ножки будет равна трем пикселям, а ширина правой ножки будет равна всего 12 — 10 = 2 пикселям. Эту проблему можно обойти, потребовав, чтобы в каждой из пар хг/, Х2г и X4j, Х4г хотя бы одно из чисел было целым; тогда обе ножки будут иметь одинаковую ширину, равную трем пикселям. Но другие величины, значение которых определяется величиной stem (например, ширина диагональных линий), будут вычисляться исходя из значения 2.7, а не 3, хотя стороннему наблюдателю будет казаться
214 Глава 24-' Дискретность и дискретизация последнее. Поэтому лучше всего сделать значение величины stem целым. Чтобы сделать это правильно, вообще говоря, нужно сказать: define. whole_blacker_pixels( stem). Эта команда вычисляет величину stem, исходя из значения stem*, по формуле: stem := max(l, round(stem# * hppp + blacker)). (Заметьте, что округление не может свести значение stem к нулю при низких разрешениях.) Даже если значение stem для буквы *п' в примере будет целым, нам, вероятно, захочется задать такое расположение, чтобы координаты хг/, я2г, хц и хаг были целыми числами, поскольку это сведет к минимуму искажение при оцифровке. Предположим, однако, что нам удобнее задавать положение пера в центре линии, а не на краю; т.е. если не принимать во внимание округление, в программе должно быть указано просто 'яг = ос\ Как же задать хг, чтобы од было целым? Можно, например, указать: Х2 = a; X2i := round од; хгг := round X2r\ Х2 := .5[од,Х2г]; но это слишком сложно. Более того, если от значений хг, Х2* и хгг зависят еще какие- то переменные, то у нас ничего не получится, поскольку в тот момент, когда переменные получат новые значения, все старые зависимости будут забыты. В случае с фиксированным пером, мы решили эту проблему, указав 'хг = good.x а'; но функция good.x знать ничего не знает о величине stem. Один из способов выйти из положения — это указать: Х2\ = round(a — .5 stem), или 'хгг = round(a + .5stem)', и все пройдет как надо. Но такой способ решения не вполне удовлетворителен, поскольку, во-первых, требует, чтобы было известно значение ширины, задаваемое в команде penpos2, а, во-вторых, применим только тогда, когда угол наклона в команде penpos равен 0. Если команду penpos нужно будет изменить, то придется менять и соответствующие уравнения, задающие округление. Существует другое решение, более общее и удобное для использования: Х2\ = round(x2i — (хг — а)). Почему оно срабатывает? Ведь аргумент оператора 'round' должен быть величиной известной, а величины Х2\ и хгг неизвестны. К счастью, благодаря команде penpos2 известна их разность хг/ — хг. Оператор округления делает значение хг примерно равным alpha, так как он устанавливает Х2\ примерно равным заданному значению хг/ минус разность между Х2 и а. ► Упражнение 24.6 Степень общности данного метода можно оценить, рассмотрев следующую более трудную проблему, с которой автор столкнулся при разработке буквы 'w\ Допустим, вы хотите, чтобы разность xi — хг была целой и чтобы хз « х\. Допустим далее, что известны величины хг, хз—xi и Х4+Х1, но xi неизвестно, и, следовательно, неизвестны величины хз и Х4- Согласно общей идее метода нужно задать уравнение вида 'xi — хг = round(xi — хг+/)', где величина xi — хг +/ должна быть известна, а / — это некая формула, значение которое приблизительно равно нулю. В таком случае разность хз — Х4 примерно равна нулю, и величина (хз — xi) — (х4 + xi) известна. Каким следует выбрать значение /? Многие шрифты, в том числе и тот, которым набран этот текст, обладают той особенностью, что в начертаниях их символов искривленные линии "раздуваются" так, что утолщенные части буквы 'о', на самом деле, оказываются чуть-чуть шире ножек буквы 'п\ Поэтому в подпрограммах для шрифта Computer Modern, описанных в приложении Е, участвуют два параметра — stem# и curved, позволяющие управлять толщиной фф
Глава 24-' Дискретность и дискретизация 215 линий. Например, для шрифта cmr9*, используемого в этом абзаце, эти параметры имеют значения stem* = 2/3pt* и curved = 7/9pt*. Обе эти величины должны быть целыми, поэтому макрос font_setup, определенный в приложении Е, включает в себя команду: define.whole-blacker-pixels(stem, curve). На бумаге это выглядит хорошо, но на деле при некоторых малых значениях разрешения могут возникнуть проблемы, так как после операции округления разница между величинами stem и curve может стать довольно ощутимой, даже если величины stem* и curve* имеют очень близкие значения. К примеру, разрешение может иметь такое значение, при котором для шрифта cmr9 значение stem будет равным всего лишь 2, а значение curve — равным 3. Но если кривые будут настолько темнее ножек, то будут выглядеть неопрятно. Поэтому в базовом формате METAFONT предусмотрена специальная подпрограмма 'lower_fix', и в приложении Е говорится lowres_fix( stem, curve) 1.2 после того, как величины stem и curve определены указанным выше образом. В этом конкретном случае процедура lowres_fix переопределит значение curve := stem, если окажется, что отношение curve/stem превосходит отношение curve*/stem* более чем в 1.2 раза. Поскольку в случае шрифта cmr9 мы имеем curve*/stem* = 7/6, это значит, что максимальное значение, которое может иметь отношение curve /stem после оцифровки, равно 1.4; если curve — 3, a stem = 2, то значение параметра curve будет уменьшено до 2. Вообще, команда вида lowres_fix(di, d.2,.. •, dn) г устанавливает dn := •••d2 := di, если отношение max(di, cfe,..., dn)/min(di,c?2, •.. ,dn) превышает величину г • max(di#, di*^..., dn*)/mm(di*, d2*)..., dn*). /$\/£\> Упражнение 24.7 JL JL Чтобы добиться качественной оцифровки начертания, иногда нужно обратить особое внимание на углы, образованные прямыми линиями. Цель данного упражнения — проиллюстрировать идеи, используемые для улучшения оцифровки углов, на примере символа * ►', программа для которого приводилась в главе 4. Если использовать эту программу без каких- либо изменений при низких разрешениях, то получаемые в результате треугольники могут оказаться неудачными. Так, например, треугольник, изображенный на рисунке справа, после оцифровки может превратиться в остроносую или асимметричную фигуру. Главную опасность представляет точка 3. Если уз — целое число, треугольник будет симметричным, относительно горизонтальной оси, но его правый угол будет иметь два пикселя в высоту и выглядеть слишком тупым. Поэтому мы должны выбрать значение уз целым плюс 1/2. Как будут выглядеть при таком уз и различных значениях х$ четыре крайних справа столбца оцифрованного угла? ► Упражнение 24.8 Предположим, что условиях предыдущего примера xi — целое число. При каком значении t/i верхний угол треугольника после оцифровки будет иметь вид ' Sj&«' ? ► Упражнение 24.9 В завершение предыдущего упражнения, модифицируйте программу из главы 4 так, чтобы верхний угол треугольника и верхняя часть его правого угла после оцифровки имели вид 'В&вЛ * На самом деле этот абзац набран кириллическим шрифтом lhr9. — Прим. перев.
216 Глава 24-' Дискретность и дискретизация f/gK До сих пор в этой главе везде предполагалось, что пиксели квадратные. Но JL иногда нужно подготовить вывод системы METAFONT для устройств с пикселями общего прямоугольного вида, а это влечет дополнительные сложности при округлении. В базовом формате METAFONT при рисовании или заливке пути, а также при выборе пера, преобразование currenttransform автоматически умножает все координаты у на величину aspecLratio, равную отношению сторон пикселя. Кроме того, функции top и bot автоматически делят на ту же величину aspect-ratio офсетное расстояние. Это значит, что при написании программ можно по-прежнему считать, что пиксели имеют квадратную форму, и использовать обычные функции: 'angle', 'direction', и т.п. Но удачными местами с точки зрения прохождения горизонтальных касательных будут уже не точки, имеющие целую координату у, а те, у которых она принимает целочисленное значение после умножения на величину aspect-ratio. /gK/gK В общем случае функция vround округляет свой аргумент до ближайшей у-коорди- JL JL наты, соответствующей границе пикселя. Так, если aspect-ratio = 1, то функция vround действует также, как 'round', округляя просто до ближайшего целого. Но если, скажем, aspect-ratio = 4/3, то vround округляет аргумент до ближайшего числа, кратного 3/4. В базовом формате системы METAFONT используется оператор vround вместо 'round' при вычислении поправки перелета, а также при вычислении величин h и d в команде beginchar. Функция good.y находит "хорошее" значение у с учетом величины aspect-ratio. >► Упражнение 24.10 Не заглядывая в приложение В, попытайтесь восстановить определения макросов vround и good.x. )► Упражнение 24.11 Что такое "точка неоднозначности" в случае, когда пиксели прямоугольные? После всех тех поправок, которые мы уже успели внести в логотип METAFONT, достаточно внести еще несколько для того, чтобы он правильно оцифровывался при произвольном отношении сторон пикселей. Значение удар должно округляться при помощи оператора 'vround', а не 'round'. Мы должны указать это, указав: define, whole- vertical_pixels( удар). Кроме того, мы должны указать: Ло# := о#; define_horizontal_corrected.pixels(/io); и в уравнениях, определяющих значение х\ в программах для 'Е' и 'F', заменить о на ho. Все остальное можно оставить в прежнем виде. /$\ Приложение В содержит макросы good.top, good.bot, good.lft и good.rt, аргументами JL которых являются пары. Например, если задано '23 = good.top (а, /3)', это значит, что 2з будет иметь значение, близкое к (а,/?), и когда гз будет трансформироваться при помощи преобразования currenttransform, верхняя точка пера currentpen, помещенного в трансформированную точку, попадет в удачное положение на растре. При использовании встроенной возможности автоокругления 'autorounding1 система METAFONT пытается самостоятельно регулировать расположение кривых с учетом растра, но это не всегда целесообразно. Она действует следующим образом. Если внутренняя величина autorounding имеет положительное значение, то координаты х тех точек, где заливаемые или рисуемые пути имеют вертикальные касательные, округляются до значений, соответствующих удачным положениям на растре. Точно так же округляются и координаты у тех точек, где пути имеют горизонтальные касательные. Остальная часть кривой искажается так, как если бы растр был слегка растянут или сжат. Если autorounding > 1, то изменений будет еще больше: пути будут чуть-чуть смещены в тех точках, где они имеют наклон, равный ±45°, что предотвратит появление в этих точках вторичных вздутий и плоских клякс.
Глава 24-' Дискретность и дискретизация 217 f Снова возвращаясь к "ионической" букве О, с рассмотрения которой начиналась эта глава, допустим, что величина curve-sidebar осталась неокругленной. Мы уже говорили, что при нулевом значении autorounding результат оставляет желать лучшего; при значениях autorounding = 1 и 2 мы получаем вот что: Линия стала намного шире по бокам по сравнению с исходной (которую, кстати, можно видеть на иллюстрации, приведенной чуть ниже). Несмотря на то что автоокругление дало вполне узнаваемый портрет буквы О, характер оригинала был утерян, особенно в случае autorounding = 2. Действительно, внутренний контур сместился к центру в верхнем левом и нижнем правом секторах, и это сделало оцифрованную внутреннюю границу идеально симметричной! /$\ Существует внутренняя величина granularity, обычно равная 1, которая регулирует степень детализации. Эта величина влияет на автоокругление, увеличивая масштаб растра. Если, например, granularity = 4, то после автоокругления координаты х и у будут не просто целыми, а кратными числу 4. Приведенные выше иллюстрации были получены при значениях granularity = 10 и mag = 10; это позволило сделать результаты автоокругления видимыми. Значение granularity должно быть целым числом. /$s/p\ Кроме встроенной возможности автоматического округления, регулируемой вели- JL JL чиной autorounding, существует еще и возможность "сглаживания" (smoothing), которая подключается, если установлено значение smoothing > 0. Суть идеи состоит в том, чтобы сгладить неровности на краях кривой, превратив их в строго последовательную вереницу переходов от точки к точке. Полное описание алгоритма сглаживания выходит за рамки данного пособия, но мы попытаемся объяснить идею на следующем примере. Пусть буквы R и D обозначают переходы на один пиксель вправо и вниз, соответственно. Если оцифрованный путь представляет собой последовательность переходов 'RDDRDRDDD\ то количество переходов вниз, приходящееся на один переход вправо, сначала уменьшается, а затем возрастает. В результате операции сглаживания эта последовательность превращается в 'RDDRDDRDD'. Если применить сглаживание к рассматривавшейся выше ионической букве О, с ней ничего не произойдет; однако, если мы применим эту процедуру к оригиналу, полученному при autorounding = 0, результат изменится: В иллюстрации справа процедура smoothing добавила три лишних пикселя; например, последовательность превратилась RDRDDDDRDD в RDDRDDDRDD.
218 Глава 24: Дискретность и дискретизация Если вы выполняете округление "вручную", то автоматическое округление и сглаживание, как правило, изменяет значения всего несколько пикселей. Поэтому в таких случаях имеет смысл отключить эти операции. Если вы определяете линии посредством задания их контуров, то автоокругление и сглаживание применяются независимо к правому и левому краям контура, поэтому могут принести столько же вреда, сколько пользы; в этом случае их также лучше отключить. Но если вы работаете с фиксированным пером, автоокругление, как правило, дает хорошие результаты и выполняет за вас большой объем рутинной работы. Если перо имеет круглую или почти круглую форму, то сглаживание также бывает полезным. Но если перо более "фигурное", то иногда просто обязано рисовать линии с негладкими краями, так что лучше установить smoothing = 0. Если вы "наклоните" шрифт, изменив величину currenttransform как описано в главе 15, то угол наклона горизонтальных линий останется прежним. Однако положение вертикальных линий изменится весьма существенно, вследствие чего разрабатываемый шрифт изменится до неузнаваемости. Это значит, что автоокругление полезно в случае наклонного шрифта, буквы которого нарисованы пером, как, например, буквы в логотипе 'METAFONT '. Но, как автор убедился на собственном опыте, буквы, нарисованные посредством заливки контура, получаются лучше при значении autorounding = 0. Так, при автоокруглении букв курсивных шрифтов семейства Computer Modern одни символы получались слишком темными, а другие — слишком светлыми. Действие автоокругления можно отследить в цифрах, присвоив внутренней переменной tracingspecs положительное значение; тогда все внутренние вычисления, которые система METAFONT производит при нахождении положений точек, в которых линии проходят вертикально, горизонтально или по диагонали, будут выводиться на экран и записываться в файл-стенограмму. (Прежде чем оцифровывать путь, система METAFONT разбивает каждый из составляющих его участков, которые представляют собой кривые Безье, на подучастки, где движение происходит в одном из восьми "октантов".) Например, если autorounding = 0 и tracingspecs = 1, а величина curve.sidebar не подвергалась округлению, то файл io. log будет содержать следующую информацию о внешней кривой в букве 'О': Path at line 15, before subdivision into octants: (1.53745,9.05345)..controls (1.53745,4.00511) and (5.75409,-0.00049) ..(10.85147,-0.00049)..controls (16.2217,-0.00049) and (20.46255,4.51297) ..(20.46255,9.94655)..controls (20.46255,14.99713) and (16.23842,19.00049) ..(11.13652,19.00049)..controls (5.77066,19.00049) and (1.53745,14.48491) ..cycle Cycle spec at line 15, after subdivision: (1.53745,9.05345) 7. beginning in octant 'SSE' ..controls (1.53745,6.58786) and (2.54324,4.371) ..(4.16621,2.74803) 7. segment 0 У, entering octant 'ESE' ..controls (5.8663,1.04794) and (8.24362,-0.00049) ..(10.85147,-0.00049) 7. segment 0 7. entering octant 'ENE' ... и так далее; там еще много-много чисел! Давайте разберемся, что же все это значит. Первый участок кривой между точками (1.53745,9.05345) и (10.85147, -0.00049) был разбит на две части в той точке, где тангенс угла наклона равен — 1. В первой из этих частей кривая направлена в основном на 'юг от юго-востока' (SSE), а во второй — на 'восток от юго-востока'. Остальные три участка разбиваются похожим образом (хотя здесь это не показано). При autorounding = 1 тот же эксперимент даст несколько иные числа:
Глава 24-' Дискретность и дискретизация 219 Cycle spec at line 15, after subdivision and autorounding: (2,9.05348) У, beginning in octant 'SSE' ..controls (2,6.50526) and (3.02194,4.22272) ..(4.6577,2.58696) 7. segment 0 7, entering octant 'ESE' ..controls (6.2624,0.98225) and (8.45786,0) ..(10.85873,0) '/, segment 0 7, entering octant 'ENE' Точка (1.53745,9.05345), в которой кривая имеет вертикальную касательную, была округлена до (2,9.05348); точка (10.85147, —.00049), в которой кривая имеет горизонтальную касательную, была округлена до (10.85873, 0); соответственно сместились и промежуточные контрольные точки. (Округление координат х и у выполнялось по отдельности.) Наконец, при autorounding = 2, METAFONT вносит дополнительные поправки так, чтобы точки, в которых кривая проходит под углом ±45°, располагались в удачных (по ее мнению) местах: Cycle spec at line 15, after subdivision and double autorounding: (2,9.05348) 7. beginning in octant 'SSE* ..controls (2,6.6761) and (3.07103,4.42897) ..(4.78537,2.71463) 7. segment 0 7. entering octant 'ESE' ..controls (6.46927,1.03073) and (8.62749,0) ..(10.85873,0) 7. segment 0 7, entering octant 'ENE* (Заметьте, что 4.78537 4- 2.71463 = 7.50000; когда наклон равен —1 в точке (х,у), кривая вблизи нее дальше всего отстоит от ближайшей точки неоднозначности, если х + у + .5 — целое число.) — ПЬЕР ЛЕ БЁ, Bete Prune (1601) — МЭТЬЮ КАРТЕР, Bell Centennial (1978)
Обзор выражений "V •
Глава 25: Обзор выражений 221 Мы убедились, что METAFONT может оперировать множеством различных алгебраических выражений. Теперь настало время собрать воедино все, что мы о них знаем. В этой и следующей главах будет точно и кратко описано все, что умеет делать система METAFONT. Здесь мы рассмотрим только примитивные операции системы и не будем затрагивать операции более высокого уровня, входящие в состав ее базового формата, которые составляют большую часть типичной программы. Поэтому новичкам лучше отложить чтение глав 25 и 26 до тех пор, пока не возникнет реальная необходимость знать, что происходит внутри компьютера на уровне рутинных процедур. Приложение В содержит краткое описание возможностей базового формата системы METAFONT, а также готовые образцы их применения, которых вполне достаточно, чтобы удовлетворить интерес большинства пользователей. Вся оставшаяся часть главы, начиная с этого абзаца, напечатана мелким шрифтом, поскольку то, что изложено ниже, по сложности соответствует материалу, который в других главах был помечен двойным знаком опасного поворота. Вместо того чтобы везде использовать этот знак, давайте условимся, что главы 25 и 26 опасны по определению. В главе 8 мы дали вам общее представление о выражениях и четырехступенчатой иерархии — "первичная формула, вторичная формула, третичная формула, выражение", которая лежит в основе их синтаксиса. Переменные в системе METAFONT могут иметь один из восьми типов: boolean, numeric, pair, path, pen, picture, string и transform. Выражения же имеют девять различных типов, хотя выражения девятого типа — "вырожденные" — особого интереса не представляет, так как могут иметь лишь одно значение. Вот как выглядит их синтаксис в целом: (первичная формула) —> (первичное булево) | (первичное числовое) | (первичная пара) | (первичный путь) | (первичное перо) | (первичная заготовка пера) | (первичный рисунок) | (первичная цепочка) | (первичное преобразование) | (первичное вырожденное) (вторичная формула) —> (вторичное булево) | (вторичное числовое) | (вторичная пара) | (вторичный путь) | (вторичное перо) | (вторичная заготовка пера) | (вторичный рисунок) | (вторичная цепочка) | (вторичное преобразование) | (вторичное вырожденное) (третичная формула) —► (третичное булево) | (третичное числовое) | (третичная пара) | (третичный путь) | (третичное перо) | (третичный рисунок) | (третичная цепочка) | (третичное преобразование) | (третичное вырожденное) (выражение) —> (булево выражение) | (числовое выражение) | (выражение-пара) | (выражение-путь) | (выражение-перо) | (выражение-рисунок) | (выражение-цепочка) | (выражение-преобразование) | (вырожденное выражение) Мы рассмотрим все типы выражений в алфавитном порядке; поэтому тем, кто сгорает от нетерпения узнать, что же такое "вырожденное выражение", мы советуем заглянуть сразу в конец главы.
222 Глава 25: Обзор выражений ■ Булевы выражения мы обсуждали в главе 19. Их полный синтаксис содержит еще одну операцию — 'charexists', о которой мы ранее не упоминали. (первичное булево) —> (булева переменная) | (булевый аргумент) | true | false | ((булево выражение)) | begingroup (список утверждений) (булево выражение) endgroup | known (первичная формула) | unknown (первичная формула) | (тип) (первичная формула) | cycle (первичная формула) | odd (первичное числовое) | charexists (первичное числовое) | not (первичное булево) (вторичное булево) —> (первичное булево) | (вторичное булево) and (первичное булево) (третичное булево) —> (вторичное булево) | (третичное булево) or (вторичное булево) (булево выражение) —> (третичное булево) | (числовое выражение) (отношение) (третичное числовое) | (выражение-пара) (отношение) (третичная пара) | (выражение-преобразование) (отношение) (третичное преобразование) | (булево выражение) (отношение) (третичное булево) | (выражение-цепочка) (отношение) (третичная цепочка) (отношение) —У < | <= | > | >= | = | о Выражение 'charexists х' истинно в том и только том случае, если ранее выполнялась команда shipout со значением charcode = х. (Значение х сначала округляется до целого числа и сводится к числу из промежутка 0 < х < 256, посредством прибавления или вычитания числа, кратного 256.) В этих правилах на месте лексем, напечатанных шрифтом печатной машинки, могут стоять любые лексемы, текущий смысл которых совпадает со смыслом указанных. К примеру, 'true' обозначает произвольную лексему, смысл которой совпадает с изначальным смыслом лексемы 'true', сама по себе лексема 'true' (смысл которой может изменяться по ходу программы) здесь роли не играет. Символы ' (' и ')' в этих правилах подразумевают произвольную пару соответствующих друг другу разделителей, определенных посредством команды delimiters. Под (булевой переменной) подразумевается переменная типа boolean, (числовая переменная) означает переменную типа numeric и так далее. Правила синтаксиса для (переменной) мы обсуждали в главе 7. Под (булевым аргументом) подразумевается аргумент макроса типа ехрг, который представляет собой булево выражение. Аргумент типа ехрг помещается в специальную "капсулу" — особую лексему, которая была подробно описана в главе 18. ■ Числовые выражения составляют ядро языка системы METAFONT, поэтому и правил синтаксиса для них больше, чем для любых других выражений: (элементарное число) —> (числовая переменная) | (числовой аргумент) | (первичная числовая лексема) | (внутренняя величина) | normaldeviate | ((числовое выражение) ) | begingroup (список утверждений) (числовое выражение) endgroup (первичная числовая лексема) —> (числовая лексема) / (числовая лексема) | (числовая лексема, за которой не следует '/(числовая лексема)') (первичное числовое) —> (элементарное число) | (элементарное число) [ (числовое выражение) , (числовое выражение) ]
Глава 25: Обзор выражений 223 | length (первичное числовое) | length (первичная пара) | length (первичный путь) | length (первичная цепочка) | ASCII (первичная цепочка) | oct (первичная цепочка) | hex (первичная цепочка) | (компонента пары) (первичная пара) | (компонента преобразования) (первичное преобразование) | angle (первичная пара) | turningnumber (первичный путь) | totalveight (первичный рисунок) | (числовой оператор) (первичное числовое) | directiontime (выражение-пара) of (первичный путь) (компонента пары) —> xpart | ypart (компонента преобразования) —> (компонента пары) | xxpart | xypart | yxpart | yypart (числовой оператор) —► sqrt | sind | cosd | mlog | техр | floor | uniformdeviate | (скалярный оператор умножения) (скалярный оператор умножения) —У (плюс или минус) | (первичная числовая лексема, за которой не следует +, - или числ. леке.) (вторичное числовое) —У (первичное числовое) | (вторичное числовое) (умножить или поделить) (первичное числовое) (умножить или поделить) —У * | / (третичное числовое) —У (вторичное числовое) | (третичное числовое) (плюс или минус) (вторичное числовое) | (третичное числовое) (пифагоровы плюс или минус) (вторичное числовое) (плюс или минус) —у + | - (пифагоровы плюс или минус) —► ++ | +-+ (числовое выражение) —У (третичное числовое) О каждой из этих операций мы уже говорили (где именно, указано в приложении I). Теперь самое время перечислить все внутренние величины, изначально имеющиеся в языке METAFONT: tracingtitles tracing equations tracing capsules tracing choices tracingspecs tracingpens tracing commands tracingrestores tracingmacros tracingedges tracingoutput tracingonline tracingstats pausing showstopping fontmaking proofing turningcheck warningcheck smoothing autorounding вывод заголовков на экран по мере их появления вывод значений переменных, когда они становится известными вывод значений капсул и переменных вывод контрольных точек, выбранных для путей вывод разбиений путей на октанты перед их оцифровкой вывод вершин перьев по мере их образования из заготовок вывод команд и операций перед их выполнением вывод восстанавливаемых символов и внутренних величин вывод макросов перед их разложением вывод оцифрованных границ по мере их вычисления вывод оцифрованных границ по мере их вывода вывод подробных комментариев на экран и в log-файл вывод в log-файл информации об использовании памяти вывод на экран строк программы перед их считыванием остановка работы после каждой команды show генерирование метрического файла шрифта генерирование пробных оттисков обращение путей, ориентированных по часовой стрелке, и вывод сообщений о необычных путях предупреждения о слишком больших значениях переменных сглаживание отдельных неровностей на оцифрованных кривых перемещение путей в "удачные" положения на растре
224 Глава 25: Обзор выражений granularity размер пикселей, используемый в autorounding fillin противодействие затемнению диагональных линий year текущий год (например, 1986) month текущий месяц (например, 3 = март) day текущий день месяца time число минут, прошедшее после полуночи до начала сеанса charcode номер следующего выводимого символа charext расширенный код следующего выводимого символа charwd ширина следующего выводимого символа (в пунктах) charht высота следующего выводимого символа (в пунктах) chardp глубина следующего выводимого символа (в пунктах) charic курсивная поправка для следующего символа (в пунктах) chardx сдвиг по х для данного устройства вывода (в пикселях) chardy сдвиг по у для данного устройства вывода (в пикселях) designsize приблизительный размер в пунктах генерируемого шрифта hppp число горизонтальных пикселей, приходящееся на один пункт vppp число вертикальных пикселей, приходящееся на один пункт xoffset горизонтальное смещение выводимых символов yoffset вертикальное смещение выводимых символов boundarychar символ правой границы в программах лигатур и кернинга Все эти величины имеют тип numeric. В момент запуска системы все они имеют нулевые значения, кроме величин year, month, day и time, которые инициализируются как раз в момент запуска. Исключение составляет величина boundarychar, которая изначально имеет значение — 1. Величина granularity, равная нулю, эквивалентна granularity = 1. При загрузке того или иного формата системы, например базового формата (plain METAFONT), некоторым из этих величин присваиваются ненулевые значения. ■ Ну вот мы и добрались до выражений типа pair, которые по своей значимости для программ METAFONT стоят на втором месте после числовых. (первичная пара) —► (переменная-пара) | (аргумент-пара) | ((числовое выражение) , (числовое выражение)) | ((выражение-пара)) | begingroup (список утверждений) (выражение-пара) endgroup | (элементарное число) [ (выражение-пара) , (выражение-пара) ] | (скалярный оператор умножения) (первичная пара) | point (числовое выражение) of (первичный путь) | precontrol (числовое выражение) of (первичный путь) | postcontrol (числовое выражение) of (первичный путь) | penof f set (выражение-пара) of (первичное перо) | penof f set (выражение-пара) of (первичная заготовка пера) (вторичная пара) —> (первичная пара) | (вторичная пара) (умножить или поделить) (первичное числовое) | (вторичное числовое) * (первичная пара) | (вторичная пара) (преобразователь) (преобразователь) —> rotated (первичное числовое) | scaled (первичное числовое) | shifted (первичная пара) | slanted (первичное числовое) | transformed (первичное преобразование) | xscaled (первичное числовое) | yscaled (первичное числовое) | zscaled (первичная пара)
Глава 25: Обзор выражений 225 (третичная пара) —> (вторичная пара) | (третичная пара) (плюс или минус) (вторичная пара) | (третичная пара) intersectiontimes (вторичный путь) (выражение-пара) —> (третичная пара) Пара — это частный случай пути (точнее, путь нулевой длины); в главе 19 мы уже объясняли, что система METAFONT изменяет тип pair на тип path только в крайнем случае, когда нет другой возможности остаться в рамках синтаксиса. ■ Продолжая наш обзор, рассмотрим синтаксис выражений типа path: (первичный путь) —> (первичная пара) | (переменная-путь) | (аргумент-путь) | ((выражение-путь)) | begingroup (список утверждений) (выражение-путь) endgroup | makepath (первичное перо) | makepath (первичная заготовка пера) | reverse (первичный путь) | subpath (выражение-пара) of (первичный путь) (вторичный путь) —> (вторичная пара) | (первичный путь) | (вторичный путь) (преобразователь) (третичный путь) —> (третичная пара) | (вторичный путь) (подвыражение-путь) —> (выражение-путь) | (подвыражение-путь) (связка путей) (третичный путь) (связка путей) —> (указатель напр-я) (элементарная связка) (указатель напр-я) (указатель направления) —> (пустой указатель) | { curl (числовое выражение) } | {(выражение-пара) } | { (числовое выражение) , (числовое выражение) } (элементарная связка) —> & i •• | .. (натяжение) .. | .. (контрольные точки) .. (натяжение) —> tension (величина натяжения) | tension (величина натяжения) and (величина натяжения) (величина натяжения) —► (первичное числовое) | at least (первичное числовое) (контрольные точки) —> controls (первичная пара) | controls (первичная пара) and (первичная пара) (выражение-путь) —> (выражение-пара) | (третичный путь) | (подвыражение-путь) (указатель направления) | (подвыражение-путь) (связка путей) cycle В главе 14 рассказано все о построении путей. ■ Перья и заготовки перьев сосуществуют согласно следующим правилам: (первичное перо) —> (переменная-перо) | (аргумент-перо) | пи11реп | ((выражение-перо)) | begingroup (список утверждений) (выражение-перо) endgroup (первичная заготовка пера) —> pencircle | makepen (первичный путь) (вторичное перо) —> (первичное перо) (вторичная заготовка пера) —> (первичная заготовка пера) | (вторичная заготовка пера) (преобразователь) | (вторичное перо) (преобразователь)
Глава 25: Обзор выражений (третичное перо) —У (вторичное перо) | (вторичная заготовка пера) (выражение-перо) —У (третичное перо) Полное руководство по их использованию вы найдете в главе 16. ■ Рисунки можно складывать и вычитать; кроме того, они могут быть "нулевыми": (первичный рисунок) —> (переменная-рисунок) | (аргумент-рисунок) | nullpicture | ((выражение-рисунок)) | begingroup (список утверждений) (выражение-рисунок) endgroup | (плюс или минус) (первичный рисунок) (вторичный рисунок) —У (первичный рисунок) | (вторичный рисунок) (преобразователь) (третичный рисунок) —> (вторичный рисунок) | (третичный рисунок) (плюс или минус) (вторичный рисунок) (выражение-рисунок) —> (третичный рисунок) В главе 13 подробно описаны все операции, выполняемые с рисунками. ■ Цепочки мы рассматривали совсем недавно в главе 22, но, для полноты изложения, мы еще раз приводим правила их синтаксиса: (первичная цепочка) —У (переменная-цепочка) | (аргумент-цепочка) | (лексема-цепочка) | jobname | readstring | ((выражение-цепочка)) | begingroup (список утверждений) (выражение-цепочка) endgroup | str (суффикс) | char (первичное числовое) | decimal (первичное числовое) | substring (первичная пара) of (первичная цепочка) (вторичная цепочка) —> (первичная цепочка) (третичная цепочка) —У (вторичная цепочка) (выражение-цепочка) —У (третичная цепочка) | (выражение-цепочка) к (третичная цепочка) Больше о цепочках нам сказать нечего. ■ В главе 15 мы говорили о преобразованиях, но не приводили для них правил синтаксиса. Вот как выглядят эти правила: (первичное преобразование) —У (переменная-преобразование) | (аргумент-преобразование) | ((выражение-преобразование)) | begingroup (список утверждений) (выражение-преобразование) endgroup (вторичное преобразование) —У (первичное преобразование) | (вторичное преобразование) (преобразователь) (третичное преобразование) —У (вторичное преобразование) (выражение-преобразование) —У (третичное преобразование) Заметьте, здесь не участвует величина identity; эта переменная определена в приложении В и не является примитивным элементом языка системы METAFONT.
Глава 25: Обзор выражений 227 ■ Наконец, мы подошли к новому типу выражений, о котором не упоминали в предыдущих главах, так как он крайне тривиален. (первиченое вырожденное) —> (вырожденный аргумент) | (сложносоставное) | ( (вырожденное выражение)) | begingroup (список утверждений) (вырожденное выражение) endgroup (вторичное вырожденное) —> (первичное вырожденное) (третичное вырожденное) —> (вторичное вырожденное) (вырожденное выражение) —> (третичное вырожденное) Понятие (сложносоставное) будет определено в главе 26. /^/рК* Упражнение 25.1 JL JL Постройте небольшие примеры выражений каждого из типов (булева, числового, ..., вырожденного). В своих конструкциях вы можете использовать только "лексемы- вызовы", но не "лексема-ярлыки" или капсулы; в частности, нельзя использовать переменные (иначе это задание будет слишком простым). Ваши выражения должны быть как можно более компактными в том смысле, что в них должно входить как можно меньше лексем; то, сколько символов вам понадобится, чтобы их набрать, значения не имеет. Все это вы не раз старательно повторяли и отлично помните. — ТОМАС MOP, A Dialogue Concernynge Heresyes (1529) Чуть пониже пятен цвета спелых томатов была изображена кучка белых пятен с вертикальными черными полосами, смысл которых оставался ему неясным до тех пор, пока подошедший посетитель не произнес с восхищением: "Как выразительно он умеет подать передний план!" ... Он слышал, что на континенте теперь все — экспрессионисты. Неужели эта мода пришла и сюда? — ДЖОН ГОЛСУОРСИ, То Let (1921)
26 Обзор языка системы
Глава 26: Обзор языка системы 229 В этой главе мы завершим "большое турне" по синтаксису системы METR- FONT, начатое в предыдущей главе. Таким образом, в распоряжение тех, кого интересуют детали, будет предоставлен полный справочник по системе. (Кроме этих двух глав, в него войдет еще и обзор из приложения В.) Система METfi FONT обладает еще несколькими возможностями, упоминать которые в предыдущих главах мы сочли нецелесообразным. Теперь настало время рассказать и о них. Если вы обнаружите несоответствие между тем, что было сказано ранее, и тем, что будет сказано ниже, то факты, изложенные в данной главе, следует рассматривать как более достоверные. Рассмотрим "процесс пищеварения" системы METAFONT — то, какими действиями система реагирует на лексемы, попавшие в ее "желудок". В главе 6 был описан процесс превращения входного файла в последовательность лексем, происходящий во "рту" системы METAFONT, а в главах 18-20 было описано, как разложимые лексемы преобразуются в неразложимые в "пищеводе" системы путем процесса, напоминающего отрыгивание. В частности, такой механизм разложения применяется при обработке условий и циклов, и мы не будем к нему возвращаться. По-настоящему бурная деятельность начинается, когда неразложимые лексемы, наконец, достигают "желудочно-кишечного тракта" METAFONT: здесь выражениям присваиваются значения, уравнения решаются, переменные объявляются, команды выполняются. В этой главе мы поговорим о примитивных операциях, которые практически генерируют рисунки и формируют выходные данные. Для начала давайте рассмотрим полные правила синтаксиса для таких объектов, как (программа) и (утверждение): (программа) —> (список утверждений) end | (список утверждений) dump (список утверждений) —> (пустое утверждение) | (утверждение) ; (список утверждений) (утверждение) —У (пустое утверждение) | (заголовок) | (уравнение) | (присваивание) | (объявление) | (определение) | (сложносоставное) | (команда) (заголовок) —> (выражение-цепочка) (сложносоставное) —> begingroup (список утверждений)(утверждение, но не заголовок) endgroup (команда) —> (команда save) | (команда interim) | (команда newinternal) | (команда randomseed) | (команда let) | (команда delimiters) | (команда защиты) | (команда everyjob) | (команда show) | (команда message) | (команда переключения режима) | (команда, применяемая к рисунку) | (команда вывода на экран) | (команда открытия окна) | (команда shipout) | (команда special) | (команда описания метрики) Несмотря на то что (пустое утверждение) не делает ровным счетом ничего, оно полезно тем, что позволяет вставлять лишнюю точку с запятой между утверждениями. Утвержде-
230 Глава 26: Обзор языка системы ние типа (заголовок) не делает почти ничего, но, как объяснялось в главе 22, содержит полезную информацию. Правила синтаксиса для (уравнений) и (присваиваний) приводились в главе 10, (объявления) были рассмотрены в главе 7; (определения) рассматривались в главах 18 и 20. В этой главе мы сконцентрируем внимание на различных типах команд^ в особенности на тех, которые ранее не рассматривались. (команда save) —у save (список символьных лексем) (список символьных лексем) —у (символьная лексема) | (список символьных лексем) , (символьная лексема) (команда interim) —> interim (внутренняя величина) : = (правая сторона) Как говорилось в главе 17, команды save и interim восстанавливают значения по окончании текущей группы. (команда newinternal) —> newinternal (список символьных лексем) Каждая из символьных лексем, указанных в команде newinternal, с этого момента будет вести себя, как (внутренняя величина), начальное значение которой равно нулю, так что их можно будет использовать в качестве аргумента команды interim. Они представляют собой ярлыки, но не внешние ярлыки (см. главу 7). Поскольку система METAFONT обеспечивает быстрый доступ к внутренним величинам, их можно использовать для повышения эффективности программ. (команда randomseed) —> randomseed : - (числовое выражение) Команда randomseed определяет величину "посева", которая определяет псевдо-случай- ные числа, генерируемые операторами 'uniformdeviate' и 'normaldeviate' (см. главу 21). Если вы сами не зададите эту величину, то по умолчанию ее значение принимается равным day -f time * epsilon. (команда let) —> let (символьная лексема) (есть) (символьная лексема) (есть) —> = | : = Команда let изменяет текущий смысл лексемы, стоящей в левой части, придавая ей текущий смысл лексемы, стоящей в правой части. Например, после такой команды, как 'let diamonds = forever', лексема diamonds будет определять некий цикл. Если до этого лексема слева была первой лексемой в именах каких-либо переменных, то все эти переменные уничтожаются. Если же лексема справа была первой лексемой в именах каких- либо переменных, то эти переменные остаются без изменений, а лексема слева становится неизвестной независимой переменной. (Назначение команды let состоит в не том, чтобы приравнивать переменные, а в том, чтобы переопределять смысл тех или иных примитивов или макросов.) Если лексема справа является одним из элементов пары разделителей, то дальнейшее поведение символа из левой части остается неопределенным. Например, если мы укажем 'let [[= (; let ]] =)', ни к чему хорошему это не приведет. (команда delimiters) —► delimiters (символьная лексема)(символьная лексема) Команда delimiters придает новый смысл двум указанным в ней символьными лексемами; с этого момента они будут соответствовать друг другу (и только друг другу). Например, в приложении В говорится 'delimiters ()'; без этой команды круглые скобки представляли бы собой не более чем обычные символьные лексемы. Роль лексем-разделителей может играть произвольная пара различных символьных лексем, и множество различных пар разделителей может использоваться одновременно. (команда protection) —У outer (список символьных лексем) | inner (список символьных лексем)
Глава 26: Обзор языка системы Команда outer закрепляет за указанными символьными лексемами статус "запрещенных", а команда и inner этот статус отменяет; смысл самих лексем при этом не меняется. Лексема, объявленная запрещенной, не должна присутствовать среди лексем, через которые система METAFONT перескакивает, работая на высокой скорости. Если система чувствует, что запрещенная лексема стоит не там, где следует, она приостанавливает работу программы и автоматически вставляет подходящий разделитель, поскольку такие лексемы должны обрабатываться в "спокойной" обстановке. (Неспокойная обстановка — это когда METAFONT перескакивает через лексемы по одной из следующих причин: ложно некое условие, считывается подставляемый текст макроса или текст цикла, просматривается текстовый аргумент макроса, отбрасываются ошибочные лексемы, обнаруженные в конце утверждения.) Если бы не эта защита, отсутствие правого разделителя могло привести к тому, что система METAFONT "проглотила" бы всю вашу программу, не заметив ошибки; запрещенные лексемы позволяют такие ошибки локализовать. Команда inner отменяет действие команды outer; действие команды outer отменяет также команда let и вообще всякая команда, которая меняет смысл символьной лексемы. Изначально все лексемы имеют статус "разрешенных". (команда everyjob) —> every job (символьная лексема) Команда 'everyjob 5' дает системе МЕТА FONT распоряжение считать лексему S в самом начале сеанса, непосредственно считыванием входного файла. (Использовать эту команду имеет смысл только в форматном файле, который загружается вручную или автоматически в начале сеанса работы, она аналогична команде \everyjob системы ТЕХ.) (команда show) —> show (список выражений) | showvariable (список символьных лексем) | showtoken (список символьных лексем) | shovdependencies | shovstats Простая команда show по очереди выводит значение каждого из указанных выражений. Пути, перья и рисунки выводятся только в log-файл, если только величине tracing online не присвоено положительное значение. Команда showvariable выдает структуру всех переменных, которые начинаются с заданного внешнего ярлыка, а также их значение и краткую форму записи; это позволяет увидеть, какие индексы и атрибуты переменных появлялись в ходе работы. Например, если вы используете соглашения, принятые в базовом формате METAFONT, то в ответ на команду 'showvariable х,у' система METAFONT покажет все команды, которые определялись с тех пор, как была дана последняя команда beginchar. Команда showtoken показывает, каков текущий смысл указанной лексемы, что позволяет выяснить является ли она примитивом, а также является ли она запрещенной. (Если команда showvariable применяется не к ярлыку, а к вызову, то система выдает ту же информацию, что и в случае команды showtoken.) Команда showdependencies показывает все неизвестные числовые переменные, которые на данный момент являются зависимыми, за исключением капсул, которые выводятся, только если задано положительное значение величины tracingcapsules. Наконец, команда showstats выдает информацию о текущем использовании ресурсов памяти системы METAFONT. Если задано положительное значение величины showstopping, то каждая из этих команд, выполнив свое действие, остановит работу программы, выдав перед этим '! ОК.'. Это позволяет вводить дополнительные команды типа show в интерактивном режиме в процессе отладки программы. (команда message) —> (оператор вывода сообщения) (выражение-цепочка) (оператор вывода сообщения) —> message | errmessage | errhelp Как объяснялось в главе 22, вы можете общаться с пользователем при помощи команд message, errmessage и errhelp. (команда переключения режима) —► batchmode | nonstopmode | scrollmode | errorstopmode
232 Глава 26: Обзор языка системы Четыре команды "переключения режима работы" регулируют степень вмешательства системы METAFONT в процесс исправления ошибок, точно так же, как и в системе TeX. Запуск системы происходит в режиме errorstopmode, и вы всегда можете восстановить этот режим, прервав работу METAFONT; включить режим scrollmode, nonstopmode или batchmode можно, набрав в ответ на сообщение об ошибке 'S', 'R' или 'Q' (см. главу 5). (команда, применяемая к рисунку) —> (команда addto) | (команда cull) (команда addto) —> addto (переменная-рисунок) also (выражение-рисунок) | addto (переменная-рисунок) contour (выражение-путь) (список with) | addto (переменная-рисунок) doublepath (выражение-рисунок) (список with) (список with) —> (пустой список) | (список with) (предложение с with) (предложение с with) —> vithpen (выражение-перо) | vithueight (числовое выражение) (команда cull) —> cull (переменная-рисунок) | (сохраняя или отбрасывая) (выражение-пара) | (команда cull) withweight (числовое выражение) (сохраняя или отбрасывая) —> keeping | dropping Команды addto и cull являются основными средствами, при помощи которых вносятся изменения в рисунки; эти команды мы подробно обсудили в главе 13. (команда вывода на экран) —> display (переменная-рисунок) inwindow (окно) (окно) —> (числовое выражение) (команда открытия окна) —> openwindow (окно) (параметры окна) (параметры окна) —У (положение на экране) at (выражение-пара) (положение на экране) —> from (экранные коорд-ты) to (экранные коорд-ты) (экранные координаты) —> (выражение-пара) Использование команд вывода на экран (display) и открытия окна (openwindow) мы обсуждали в главе 23. (команда shipout) —> shipout (выражение-рисунок) Вас, наверное, давно интересует, как METAFONT преобразует графическую информацию в шрифт. Наконец-то мы можем ответить: команда 'shipout v' помещает информацию о пикселях с положительным весом, определяемую выражением-рисунком и, в так называемый файл шрифта "общего" формата, или gf-файл, где она превращается в схему побитового отображения, ассоциированную с символом с номером charcode mod 256 -f charext * 256. При выводе в gf-файл пиксели рисунка v сдвигаются на величину (xoffset,yoffset). (Однако, если proofing < 0, то вывода не происходит. Значения величин xoffset, yoffset, charcode и charext в случае необходимости округляются до целых чисел.) Кроме того, команда shipout сохраняет значения величин charwd, charht, chardp, chanc, chardx и chardy\ эти значения ассоциируются с текущей величиной charcode, когда генерируется информация о размерностях шрифта. (Основные сведения о метрических файлах и файлах шрифтов общего формата содержатся в приложениях F и G.) (команда special) —У special (выражение-цепочка) | numspecial (числовое выражение) Если величина proofing имеет неотрицательное значение, команды special и numspecial отсылают в выходной gf-файл, соответственно, буквенную и числовую информацию. Например, метки на пробных оттисках определяются в макросах базового формата именно таким образом. Более подробные сведения о них содержатся в приложениях G и Н. Мы рассмотрели все виды команд, кроме одного. Эта оставшаяся нерассмотренной команда является еще более специальной, чем (команда special), поэтому мы обсудим ее в отдельном приложении. В приложении F мы полностью завершим описание синтаксиса,
Глава 26: Обзор языка системы 233 определив объект (команда описания метрики). Пока же достаточно знать, что команды этого типа определяют мелкие детали шрифта, такие, как кернинг и элементы, определяемые утверждениями с 'font.normaLspace' в программе для логотипа METAFONT (см. главу 11). Наконец, возвращаясь в начало этой главы к самому первому правилу синтаксиса, мы видим, что для полноты картины не хватает одного последнего штриха. При работе со специальной версией METAFONT, которая называется 'INIMF', вместо лексемы 'end' можно использовать лексему 'dump'. Тогда все определенные на данный момент макросы, а также текущие значения переменных и текущие смысловые значения символьных лексем будут записаны в отдельный файл, который можно будет потом загружать, как форматный. (Эта команда аналогична команде \dump системы TeX) О форматных файлах говорится в конце приложения В. /^/$\^ Упражнение 26.1 Запустив METAFONT, введите следующие строки и объясните ответ. \newinternal а; let Ъ=а; outer a,b,c; let c=b; delimiters a::; shovtoken a,b,c; end Мы тратим жизнь, копаясь в мелочах. Честному человеку, как правило, достаточно для счета своих десяти пальцев рук. В крайнем случае он может задействовать еще десять пальцев ног, а все остальное отбросить. Простота, простота, и еще раз простота! Говорю вам: на вещи нужно смотреть, видя в них аспекта два-три, а не сотню или тысячу ... Упрощайте, упрощайте. — ГЕНРИ ДЭВИД ТОРО, Уолден (1854) Бдительный компьютер с его грозной памятью все сказанное тобою принимает за чистую монету. Поэтому, чтобы не допускать ошибок, ты должен шаг за шагом анализировать, продумывать на предмет истинности все, что говорят ему кончики твоих пальцев. — ГЕРМАНН ЗАПФ, Десять заповедей фото-печати (1982)
27 Как исправлять ошибки
Глава 27: Как исправлять ошибки 235 Мы рассказали о системе METAFONT все, что о ней нужно знать, а если вы чего-то не поняли — пеняйте на себя. Если вы планируете работать вообще без ошибок, то можете не забивать себе голову чтением этой главы. В противном же случае вам пригодятся некоторые методы, при помощи которых METAFONT ищет ошибки в программах. Во время экспериментов, которые вы проводили по ходу чтения главы 5, вы познакомились с основными видами сообщений об ошибках, а также научились отвечать на жалобы системы различными способами. Приобретя некоторый опыт в работе с METAFONT, вы сможете исправлять большинство ошибок в интерактивном режиме, вставляя или удаляя те или иные элементы. Однако некоторые ошибки могут иметь гораздо более разрушительные последствия, чем другие; из-за одной ошибки могут потерять смысл вполне логичные конструкции. Кроме того, система METAFONT не всегда правильно определяет тип ошибки, поскольку нарушить правила можно огромным числом способов. METAFONT представляет собой компьютерную программу с довольно ограниченным интеллектом, вовсе не призванную угадывать замыслы человека. В самом деле, иногда ваше личное мнение может разойтись с мнением системы METAFONT; она может посчитать ошибкой то, что с вашей точки зрения логически абсолютно верно. В этой главе мы объясним, как узнать о мотивах, лежащих в основе действий METAFONT; надеемся, что это позволит вам избежать недоразумений в диалоге с системой. В идеале вы должны приступать к работе с системой, находясь в хорошем настроении, и всякое сообщение об ошибке должны воспринимать не как личное оскорбление, а как интересную головоломку, задавая себе вопрос: "Почему машина выдала это сообщение?". Система METAFONT умеет распознавать более сотни различных видов ошибок, и вы скорее всего никогда не столкнетесь со всеми из них, поскольку некоторые ошибки очень трудно допустить. Давайте вернемся к примеру с программой 'badio.mf из главы 5, поскольку из него можно почерпнуть еще кое-к? кую информацию. Если память у вас лучше, чем у автора, вы припомните, что первое сообщение об ошибке было таким: >> mode.setup ! Isolated expression. <to be read again> » 1.1 mode setup; У, an intentional error! ? В главе 5 мы проигнорировали эту ошибку и продолжили работу, но настоящий METAFONTumK на нашем месте обязательно подумал бы: "Черт побери! Я ведь хотел напечатать 'mode_setup', но забыл набрать знак нижнего подчеркивания. К счастью, ничего страшного не произошло; просто система METAFONT обнаружила странное выражение imode.setup'1 и проигнорировала его. Пожалуй, здесь стоит вставить правильную команду — 'mode-setup'". Хорошая мысль! Итак, вы наберете 'I mode_setup', не так ли? Увы, это неверно. Прежде чем считать точку с запятой и подготовиться считывать следующую запись, система METAFONT выдаст множество сообщений об ошибках; в таком случае важная подсказка содержится в следующих двух строчках: <to be read again>
236 Глава 27: Как исправлять ошибки которые говорят нам, что система ждет точку с запятой. Поэтому вы должны были бы сказать 'I; mode.setup'. Без этой точки с запятой в начале вы получите в ответ какую-то беспорядочную смесь: ! Extra tokens will be flushed. <to be read again> warningcheck mode_setup->warningcheck :=0;if.unknown.mode:mode=proof;f i... <insert> mode.setup <to be read again> 1.1 mode setup; У, an intentional error! ? He волнуйтесь, существует простой способ исправить положение. Сообщение-комментарий подсказывает нам: "Теперь, пожалуйста, вставьте точку с запятой в начале текста, который мне не следует стирать"; все, что вам нужно сделать, — это напечатать 'I;', и конечный результат будет точно таким же, как если бы вы сразу вставили точку с запятой перед командой mode.setup. Мораль такова: когда в процессе исправления ошибок вы вставляете новое утверждение, перед ним зачастую необходимо поставить точку с запятой. Но даже если вы забыли это сделать, система METAFONT дает вам еще один шанс. Продолжая обрабатывать файл badio.mf так, как предлагается в главе 5, мы снова получаем сообщение об ошибке: » 0.08682thinn+144 ! Undefined х coordinate has been replaced by 0. (Здесь обнаружена ошибочная лексема thinn.) Сообщение-комментарий к этой ошибке содержит довольно странный совет: (Chapter 27 of The METAFONTbook explains that you might want to type 'I ???' now.) (что в переводе означает: В главе 27 книги "Все про METAF0NT" объясняется, что сейчас имеет смысл набрать 'I ???'.) Глава 27? Так это же здесь! Что же произойдет, если мы сейчас наберем 'I ???'? В ответ мы получим следующее: х41=0.08682thinn+144 у4=-0.4924thinn+259.0005 х4г=-0.08682thinn+144 y4r=-0.9848thinn+259.0005 ! ОК. Теперь очевидно, что мы допустили опечатку в слове 'thin'. В базовом форматном файле лексема '???' определяется как макрос, которая показывает текущие зависимости между числовыми переменными и приостанавливает работу, выдав '0К\ Это очень полезный макрос, поскольку, в отличие от сообщения об ошибке, он позволяет проследить такую возможную ситуацию, когда переменная с ошибочно набранным именем превращается не в независимую, а в зависимую переменную. Еще одного примера внесения поправок в интерактивном режиме будет вполне достаточно, чтобы уяснить общую стратегию. Допустим, вы нечаянно напе-
Глава 27: Как исправлять ошибки 237 чатали вместо круглых скобок квадратные; в этом случае машина воскликнет: ! A primary expression can't begin with '['. <inserted text> 0 <to be read again> [ <*> show round[ 1 + sqrt43]; (По случайному совпадению, сообщение-комментарий к этой конкретной ошибке снова отсылает нас к главе 27.) Если система METAFONT, просмотрев последовательность лексем, не находит выражения, которое должно было бы присутствовать, она всегда пытается вставить '0', чтобы продолжить работу. В данном случае мы видим, что нуль нам не подходит; поэтому наберем 7, стерев тем самым следующие семь лексем, и тогда компьютер выдаст новое сообщение: <*> show round[1 + sqrt43] 7 Вставим правильную формулу, напечатав 'I (1 + sqrt43)', и программа успешно продолжит работу, как будто никакой ошибки не было. ► Упражнение 27.1 Почему мы удалили именно 7 лексем? f* Упражнение 27.2 Если бы пользователь ничего не вставлял и не удалял, а просто продолжил работу, то получил бы от системы METAFONT еще одно сообщение об ошибке: » О ! Extra tokens will be flushed. <to be read again> [ <to be read again> (7.55743) <to be read again> ] <*> show round[1 + sqrt43] i ? Объясните в чем дело. Что теперь нужно сделать? Полезно иметь в виду, что первая ошибка в программе может повлечь за собой целый ряд мнимых "ошибок", так как неверно заданные команды могут серьезно повлиять на то, как система METAFONT будет воспринимать последующий материал. Но, как вы сами убедитесь, в большинстве случаев достаточно одного сеанса, чтобы машина обнаружила все места во входном файле, где нарушены правила синтаксиса METAFONT. f Иногда ошибка оказывается настолько серьезной, что система METAFONT вынуждена преждевременно прекратить работу. К примеру, если вы работаете в режиме batchmode или nonstopmode и системе METAFONT в требуется считать входную
238 Глава 27: Как исправлять ошибки последовательность с клавиатуры, то она делает "экстренную остановку". То же самое происходит и в том случае, когда не может быть открыт нужный файл, или когда входная последовательность лексем содержит команду end. Вот некоторые из сообщений, которые вы можете получить непосредственно перед тем, как METAFONT отступит перед неизвестностью: Fatal base file error; I'm stymied. Это значит, что указанный вами форматный файл не может быть использован, так как написан для другой версии системы METAFONT. That makes 100 errors; please try again. Это значит, что система METAFONT обнаружила 100 ошибок подряд в текущем утверждении, так что, возможно, это — бесконечный цикл. I can't go on meeting you like this. Предыдущая ошибка каким-то образом разладила систему METAFONT. Исправьте ее и повторите запуск. This can't happen. Что-то не в порядке с самой системой METAFONT, которую вы используете. Рвите и мечите! /£\ Существует еще одно страшное сообщение, которое METAFONT выдает с большой JL неохотой. Но может статься, что вы получите такое сообщение: METAFONT capacity exceeded, sorry. Увы, это значит, что вы переоценили возможности METAFONT. В сообщении говориться, что какой-то из разделов памяти METAFONT переполнен; здесь же будет указан тип объектов, по вине которых произошло переполнение. Это могут быть: number of strings цепочки и имена символьных лексем и файлов pool size символы в этих цепочках main memory size пары, пути, перья, рисунки, списки лексем и т.п. hash size имена символьных лексем input stack size источники разового ввода number of internals внутренние величины rounding table size переходы между октантами в циклах parameter stack size параметры макросов buffer size символы в строках, считываемых из файлов text input levels вводимые файлы и вставки исправлений path size количество ключевых точек в одном пути move table size строки в рисунках, обрабатываемые одновременно pen polygon size количество смещений пера в одном октанте ligtable size накопляемые инструкции ligtable kern различные величины кернинга extensible встроенные символы headerbyte максимальный адрес headbyte f ontdimen максимальный адрес fontdimen Кроме того, в сообщении указывается текущий объем свободной памяти. fEciiH ресурсов системы METAFONT достаточно, чтобы выполнить задание, которое вы перед ней ставите, но вы все же хотите знать, насколько близко вы подошли к пределу ее возможностей, достаточно задать положительное значение величины tracingstats до конца этого задания. Тогда в конце log-файла будут приведены сведения о реально использованных ресурсах, соответствующих первым девяти пунктам приведенного
Глава 27: Как исправлять ошибки 239 выше списка (т.е. количество строк, ..., размер буфера обмена); они будут записаны в том же порядке, в котором они были перечислены. Более того, при помощи команды showstats вы можете в любой момент сеанса работы узнать о текущем использовании основной памяти и памяти, зарезервированной под цепочки. Информация об использовании основной памяти делится на две части: '490&5950' означает, что на данный момент под "крупные" объекты, такие как перья, капсулы и преобразования, занято 490 слов*, а под "мелкие" объекты, такие как лексемы или границы, занято 5950 слов. fA что же делать, когда ресурсов системы METAFONT недостаточно? Все вышеперечисленные разделы памяти системы, за исключением количества различных величин кернинга и встроенных символов, можно увеличить, если только для этого достаточно ресурсов компьютера. Объем памяти, необходимый для увеличения одного раздела, почти всегда можно освободить за счет уменьшения другого раздела, так что увеличивать общий объем памяти, выделенный системе METAFONT, не приходится. Если вы планируете решать при помощи METAFONT какую-то особо важную задачу, можете попросить друзей- программистов создать для вас специальную версию системы, ресурсы которой заданы вручную с учетом ваших потребностей. Но прежде чем решиться на такой отчаянный шаг, убедитесь, что вы правильно используете возможности METAFONT. Так, если вы пытаетесь задать гигантский рисунок со множеством переходов от черных пикселей к белым, то вам, пожалуй, следует пересмотреть свой подход, так как система METAFONT обязана помнить обо всех переменах пиксельных значений при переходах между смежными пикселями во всех рисунках, к которым имеется доступ в текущий момент. Не исключено, что постоянно запоминая различные перья, вы попусту расходуете ресурсы системы, о чем вас предупреждали в главе 16. Если вы встроили в систему огромную библиотеку макросов, то представьте, сколько разных подставляемых текстов должна запомнить система METAFONT. Поэтому, если ресурсы системы ограничены, загружайте в память только те макросы, которые действительно необходимы. f Некоторые неверно составленные программы могут переполнить любой конечный объем памяти. Например, если дать определение 'def recurse=(recurse)enddef', а затем использовать команду recurse, то немедленно обрушится шквал сообщений: ! METAFONT capacity exceeded, sorry [input stack size=30]. recurse->(recurse ) recurse->(recurse ) recurse->(recurse ) И как бы вы не увеличивали объем раздела памяти, выделенного под стек ввода (input stack size), та же ошибка будет появляться снова и снова. f Большинство реализаций системы METAFONT позволяет прерывать работу программы. Это дает возможность выяснить причины образования бесконечных циклов, если машина не прерывает работу сама из-за переполнения памяти. Вследствие такого прерывания METAFONT переключается в режим errorstopmode; поэтому вы сможете вставлять во входную последовательность команды. Вы можете прекратить сеанс, вывести информацию на экран при помощи команды show, изменить текущий набор переменных и т.д. В таких случаях вам, вероятно, понадобится использовать команду hide для того, чтобы "спрятать" команды, отвечающие за вывод комментариев. Например, чтобы не * Размер слова зависит от платформы операционной системы; как правило, одно слово — эю 4 байта. — Прим. перев.
240 Глава 27: Как исправлять ошибки запутаться в выражениях, значения которых система METAFONT определяет в данный момент, можете набрать I hide(showstopping:=1; alpha:=2; show x) С помощью прерываний вы также можете определить, на что система METAFONT тратит большую часть рабочего времени. Это может пригодиться в том случае, если вы используете неэффективный макрос; случайные прерывания позволят определить место в программе, которое METAFONT посещает чаще всего. На втором месте в списке наиболее неприятных сообщений об ошибках системы METAFONT стоят те из них, в которых говорится о наличии "необычных" путей. Иногда одного взгляда на полученный рисунок достаточно, чтобы понять, что вы действительно указали путь, который пересекает сам себя, наподобие цифры '8'. Однако возможно, что путь, который вам кажется вполне обычным, машина "забракует". В последнем случае придется взяться за расшифровку аббревиатур октантов, выдаваемых системой METAFONT; на первый взгляд они выглядят устрашающе, но привыкнув к ним, вы оцените, насколько они полезны. Давайте вернемся к программе из главы 14, рисующей дерево Эль Пало Альто, и рассмотрим тот ее фрагмент, в котором строится путь branch4: branch4= f1ех((0,509),(-14,492),(-32,481)) ftf1ех((-32,481),(-42,455),(-62,430)) &flex((-62,430),(-20,450),(42,448)) &flex((42,448),(38,465),(4,493),(0,509)) ftcycle; Если бы вместо 450 в третьем флексе стояло 452, то в этом месте система METAFONT остановилась бы и выдала следующее: > 0 SSW WSW 1 2 SSW 3 WSW 4 (VNV NNV) NNE ENE 5 ESE 6 (ENE) NNE NNW 7 WNW NNW 8 NNE 0 (NNW VNW WSV) ! Strange path (turning number is zero). <to be read again> i <for(4)> ...]shifted(150,50)scaled(w/300); ENDF0R p.4,1.94 endfor endchar; Последовательность лексем 'for(4)', которая неявно содержится в последней пятой строке, делает путь branch4 дефектным, так как в ней говорится, что индекс в цикле for равен 4. Почему путь branch4 считается "необычным"? Подсказку могут дать только коды октантов, такие как 'SSV. (Более простой пример, на который вам, возможно, захочется сейчас еще раз взглянуть, приводился в главе 13.) Возможно, у вас уже имеется и следующая диаграмма, полученная в режиме proofmode: #о У^Г —Ла 4*^
Глава 27: Как исправлять ошибки 241 Начавшись в момент времени 0 в точке (0,509), путь продолжается в направлении на юг от юго-запада (SSW), затем — на запад от юго-запада (WSW), вплоть до момента времени 2 (конец первого флекса). Затем он снова ведет в направлении SSW, и снова — в направлении WSVI (это второй флекс). Однако в момент времени 4 путь делает резкий поворот, пробегая по направлениям WNW и NNW, но оставаясь неподвижным (поскольку коды соответствующих октантов заключены в скобки). Ага! Вот здесь путь должен был повернуть против часовой стрелки, пройдя через октанты SSW, SSE и ESE, но система METAFONT решила сделать поворот по часовой стрелке, поскольку так было короче. Путь действительно делает небольшую петлю в момент времени 4 между концом второго и началом третьего флексов. Таким образом, его число обходов равно нулю, и путь является необычным по определению. /gK ►Упражнение 27.3 JL В какой точке пересекаются второй и третий флексы в данном примере? f Существует три основных способа избежать проблем с необычными путями. Первый состоит в том, чтобы избегать путей с такими резкими поворотами. Во- вторых, вы можете смещать пути на величину epsilon, как это было сделано в примере, приведенном в конце главы 16. (Для пущей безопасности смещать пути можно на величину eps.) В-третьих, вы можете взять за правило выполнять все повороты в направлении против часовой стрелки. В последнем случае вы можете установить turning check := 0; это значит, что METAFONT не будет проверять наличие необычных путей, но все будет в порядке, поскольку, если вы проходите все циклы в правильном направлении, то небольшие петли на путях не причинят никакого вреда. /$К/$\ Иногда система пытается обратить необычный путь, чтобы устранить его "необыч- JL JL ность". Тогда коды октантов приводятся в обратном порядке. Рано или поздно (чем раньше, тем лучше) вы научитесь работать с системой METAFONT так, чтобы, обрабатывая ваш входной файл, она ни разу не остановилась. Но результат по-прежнему может быть неудовлетворительным. Сам по себе тот факт, что система METAFONT не останавливается, не означает, что не нужно просматривать пробные оттиски. На этой стадии обычно легко догадаться, как исправить ошибки печатного изображения, подкорректировав входной файл. Явные ошибки, как правило, легко определяются визуально при помощи пробных оттисков, которые обсуждаются в приложении Н, особенно если вы не забыли пометить ключевые точки ваших конструкций. Однако полученное изображение может содержать необъяснимые на первый взгляд ошибки. Если вы не можете понять, в чем ошибка, используйте старый испытанный прием упрощения программы: удалите из нее все, что работает нормально, пока вы не получите наименьший возможный входной файл, содержащий ту же ошибку, что и исходный, судя по сообщениям системы. Чем короче файл, тем легче обнаружить причину возникшей проблемы. /£s Один из важнейших приемов, используемых для исправления ошибок в програм- JL мах, состоит в том, что величине tracingspecs присваивается положительное значение. В результате этого информация о всех ключевых и контрольных точках проблематичных путей будет записываться в log-файл. (См. пример в конце главы 24, часть "до разбиения".) Если с обработкой пути возникают проблемы, вы можете скопировать описание этого пути из log-файла непосредственно во входной файл, что позволит избежать сложностей, связанных с уравнениями, которые могли изначально использоваться при определении этого пути. fMbi обсудили процессы отслеживания, производимые системой при положительных значениях величин tracingstats и tracingspecs; кроме них, система METAFONT
242 Глава 27: Как исправлять ошибки умеет выполнять множество других отслеживаний. Например, в главе 22 мы обсуждали отслеживание заголовков (tracingtitles), в главе 18 — отслеживание макросов (tracingmacros), в главе 17 — отслеживание восстановленных значений (tracingrestores), в главе 9 — отслеживание уравнений (tracingequations). Кроме того, вы можете задать положительное значение величины tracingchoises; тогда система будет показывать все пути до и после того, как она выберет контрольные точки в соответствии с правилами, приведенными в главе 14. Вы также можете задать положительное значение величины tracingpens \ тогда система будет показывать прямоугольники, соответствующие перьям, по ходу их превращения из заготовок перьев в готовые перья (переменные типа реп). Вы также можете задать положительное значение величины tracingoutput; тогда система будет показывать все рисунки, которые отправляются на вывод, используя переходы между границами пикселей для указания пиксельных значений, как было продемонстрировано в главе 13. Как уже было сказано, для того, чтобы включить отслеживание, достаточно присвоить положительное значение соответствующей внутренней величине; например, если нужна информация о перьях, можно указать tracingpens := 1 (или interim tracingpens := 1). Если установлено tracing commands = 1, то система METAFONT показывает все команды, прежде чем выполнить их. Если tracing commands = 2, то METAFONT сверх того показывает все разложимые лексемы непосредственно перед тем, как произвести их разложение (кроме макросов, которые отслеживаются только если tracingmacros > 0). А если tracing commands = 3, то METAFONT показывает еще и все алгебраические операции, прежде чем их выполнить. Таким образом, при желании вы можете быть в курсе всех действий системы METAFONT. За получаемым на выходе оцифрованнным изображением можно проследить, установив tracingedges = 1. Например, если мы попросим METAFONT нарисовать для нас "ионическую" букву 'О' (см. главу 5) с разрешением 100 пикселей на дюйм (режим lowres с увеличением тад = .5), то при значении величины tracingedges, равном единице, система выдаст следующий отчет о граничных переходах: Tracing edges at line 15: (weight 1) (1,5)(1,2)(2,2)(2,1)(3,1)(3,0)(8,0)(8,1)(9,1)(9,2)(10,2)(10,8)(9,8) (9,9) (8,9) (8,10) (3,10) (3,9) (2,9) (2,8) (1,8) (1,5) . Tracing edges at line 15: (weight -1) (3,5)(3,2)(4,2)(4,1)(7,1)(7,2)(8,2)(8,8)(7,8)(7,9)(4,9)(4,8)(3,8)(3,5). Построив по этим граничным переходам (обнуляя их веса на внутренней границе) изображение, мы видим, что при таком низком разрешении символ будет симметричен: Более подробная информация об оцифровке изображения выдается, если установлено значение tracingedges > 1, когда начертание получено при помощи фиксированного пера посредством команды draw или filldraw. В этом случае предоставляется подробная информация о действиях в каждом из октантов; кроме того, при этом сообщается о "переходах" на краях прямых линий, которые совершаются, когда METAFONT переключает направления, выполняя команды penoffset. 'S\ Команды отслеживания tracing... помещают всю соответствующую информацию JL в log-файл. Если же установлено положительное значение величины tracing online, то информация о ходе работы не только записывается в log-файл, но и выводиться на экран. В базовом формате METAFONT имеется макрос tracingall, который включает максимальное число возможных отслеживаний. Он не только устанавливает положительные значения величин tracingcommands, tracingedges, tracingspecs и т.д., но и устанавливает ff
Глава 27: Как исправлять ошибки 243 tracing online := 1, а также showstopping := 1, так что вы получаете возможность вносить правки в интерактивном режиме при помощи команд show. Это — максимум того, что можно включить. Кроме того, имеется макрос logginall, который действует так же, как tracingall, не затрагивая лишь величины tracingonline и showstopping. Если вы хотите установить только tracingonline := showstopping := 1, можете использовать макрокоманду interact. Наконец, имеется макрос tracingnone, который выключает все виды отслеживаний, если они больше не нужны. Некоторые версии системы METAFONT, предназначенные для производственных целей, специально разработаны для работы с максимальной скоростью. В этих реализациях системы отслеживание tracingstats не включается ни при каком значении соответствующей внутренней величины, и при значении tracingedges > 1 вы не получаете дополнительной информации, так как система METAFONT работает быстрее, если ей не приходится отслеживать что-либо и вести статистику. Если вы хотите использовать все доступные в METAFONT средства диагностики, убедитесь, что они имеются в используемой вами версии. /$К/$\ Если вы установите pausing := 1, то система METAFONT позволит вам редакти- jC JL ровать отдельно каждую строку программы в тот момент, когда она считывается из входного файла. Таким образом вы сможете накладывать временные "заплаты" (например, вставлять команды show...) на те места в программе, где имеются какие-либо проблемы, не меняя при этом содержимое входного файла. При этом система METAFONT будет работать с той же скоростью, что и человек. Последнее указание: Работая над шрифтом с большим количеством символов, лучше разрабатывать за раз лишь несколько символов. Ведите два отдельных файла — "черновик" и "чистовик" и всю основную работу выполняете в черновом файле. (В приложении Е предлагается удобный способ организации, когда параметры, соответствующие, как отдельным черновым символам, так и всему шрифту, хранятся в отдельном контрольном файле.) Когда символы доведены до нужного вида, их можно перенести в файл-чистовик. Время от времени можно запускать METAFONT, подавая на ввод чистовой файл; это позволит проследить за процессом формирования шрифта. При возникновении непредвиденных проблем символы всегда могут быть возвращены в черновой файл. ► Упражнение 27.4 Последнее упражнение: Найдите в книге все места, где вас надули, и все шутки. Последнее наставление: Не останавливайтесь НА достигнутом и создавайте шедевры цифровой печати! Вероятно, выбирая маршрут, я сделал несколько ошибок. — ФРЕЙ ПЕДРО ФОНТ, Дневник (1776) Как мудрости достичь? Путь прост и ясен: вы ошибайтесь, ошибайтесь, ошибайтесь, но раз за разом меньше, меньше, меньше. — ГЕЙН ПИТ, Grooks (1966)
А Ответы ко всем упражнениям
Приложение А: Ответы ко всем упражнениям 245 В предисловии говорилось о том, что прежде чем заглянуть сюда за ответом к упражнению, разумно попытайтесь разобраться в нем самостоятельно. Однако ответы все равно полезно прочитать, поскольку иногда в них содержится дополнительная информация, которая поможет вам лучше разобраться в проблеме, над которой вы только что поработали. 2.1. Точка 5 = (100,0) ближе всех остальных. (См. диаграмму, приведенную ниже.) 2.2. Нет, не верно. Но все они имеют одинаковую координату у. 2.3. На 5 единичных отрезков влево и на 15 единичных отрезков вверх от точки отсчета. 2.4. (200,-60). 2.5. toplftzi = (0,6); topz2 = (a,b); top rt 23 = (2a - 1,6); bob Ift 24 = (0,0); 60*25 = (a,0); bot rt ze = (2a — 1,0). Если ширина символа в пикселях равна 2а, то смежные символы разделены одним столбцом белых пикселей, т.к. правая граница черных пикселей определена так, что ее координата х равна 2а — 1. 2.6. right = (1,0); left = (-1,0); down = (0, -1); up = (0,1). 2.7. Да, верно; он равен (2,3) — (5, —2). 2.8. 0(21,22] = 2i, т.к. мы смещаемся в направлении к 22 на нулевую часть от расстояния; точно так же, 1[21,2г] = 22, поскольку мы смещаемся на все расстояние. Продолжая движение в том же направлении до тех пор, пока не пройдем расстояние, вдвое большее, чем от 2i до 22, мы получим 2(21,22]. Если же, находясь в точке 2i и имея прямо перед собой точку 22, мы сместимся назад на расстояние, равное половине расстояния между ними, то окажемся в точке (—.5)[21,2г]. 2.9. (а) Да, верно; оба выражения равны 2i + \{z2 —21). (b) Не совсем, но почти верно; правая сторона должна быть |2i + 522. (с) Да, верно; оба выражения равны (1 — t)z\ + tz2- 2.10. По нескольким причинам. (1) Уравнения в программе для METAFONT должны представлять замысел программиста как можно более явно. Замысел будет трудно понять, если показаны только результаты, поскольку восстановить алгебраические преобразования, оставшиеся "за кулисами", непросто. (2) Алгебраические вычисления проще и безопаснее поручить компьютеру, чем производить их вручную. (3) Формула (^[xi,X5],6) даст разумное значение для zz даже в том случае, если определения точек 2i и 25 изменятся. Почти всегда лучше иметь в виду, что в дальнейшем может потребоваться внести изменения. Однако указанная для z$ формула — не единственный разумный способ определения. Мы можем задать, например, два уравнения: хз — xi = хь — xz\ уз = 6, первое из которых показывает, что расстояние по горизонтали от 1 до 3 равно расстоянию по горизонтали от 3 до 5. Позже мы убедимся, что METAFONT умеет решать множество различных уравнений. 2.11. Следующие четыре уравнения полностью определяют четыре неизвестные величины — х2, уг, х4 и у а'. 24 - 22 = whatever *dir20; £[2/2,1/4] = §[уз, t/i]; z2 = whatever\zu 23]; 24 = whatever [z3,zs]. 3.1. Направление в точке 22 параллельно линии 24 .. 2з, но вектор 24 — 2з определяет направление, которое на 180° отличается от направления 23 — 24, которое рассматривалось ранее. Поэтому встретившееся нам определение кривой — достаточно трудное, и METAFONT рисует кривую, похожую на крендель и столь закрученную, что мы не станем ее 1*1 111 1 1 1 1 J 1 1 1 1 y^iPY'I 1 1 1 1 1 1 ! 1 (-б.1Ь).| $ i i « 1 1 1 Л т Ml 11 i^
246 Приложение А: Ответы ко всем упражнениям здесь изображать. Первая часть пути, от z\ до 22, зеркально симметрична относительно линии z\ .. 25, которая делит пополам z\ .. 22, поэтому она выходит из начальной точки в юго-юго-западном направлении; вторая его часть зеркально симметрична относительно вертикальной линии, делящей пополам Z2 .. 23, поэтому, заканчивая обход в точке 2з, кривая направлена приблизительно на северо-запад. Мораль такова: не следует указывать направление, противоположное (т.е. обратное) тому, которое вам на самом деле нужно. 3.2. draw 25 .. 24(24 - 22} .. 21 .. 23 .. z&{z2 - 26} .. cycle. 4.1. (а) Эллипс высотой 0.8 pt и шириной 0.2 pt ('|'); (b) круг диаметром 0.8 pt (вращение не меняет форму круга!); (с) то же, что и в пункте (а). 4.2. Вместо прямых или кривых линий будут нарисованы шесть отдельных точек. Эти точки будут нарисованы при помощи выбранного на данный момент пера. Однако по техническим причинам, которые объясняются в главе 24, команда draw лучше всего работает в том случае, когда она передвигает перо; пиксели на концах кривых, особенно при малых разрешениях, иногда выглядят совсем не так, как можно было бы предположить. Как правило, для того, чтобы нарисовать только одну точку, лучше пользоваться не 'draw', a 'drawdot'. 4.3. Верно для всех видов перьев, рассмотренных до сих пор. Однако, вообще говоря, неверно, поскольку, как мы увидим позже, перья могут охватывать больше пикселей вверх, чем вниз от центра, т.е. в уравнениях для top и hot t и Ь могут быть не равны между собой. 4.4. х2 = xi; хз = £[х2,Х4]; Х4 = х5; botyi = -о; top у2 = h + о; 2/4 = 2/2; Уъ = 2/1; draw z\ .. Z2\ draw Z2 .. 23; draw 23 .. 24; draw 24 .. 25. Позже мы научимся заменять четыре команды draw одной: draw z\ - - z2 - - zz - - z\ - - z5; это несколько ускоряет работу METfiFONT. 4.5. Нужно либо дать команду 'fill 25 .. 24 .. z\ .. 23 .. zq .. Z5 .. cycle', где в определении кривой точка Zb дублируется и гладкость в этой точке нарушается, либо команду 'fill 25{curll} .. za •. z\ .. zz .. ze .. {curl l}cycle'. В последнем случае можно пропустить любое одно из определений величины загиба, но не оба. 4.6. После того как определены шесть исходных точек, нужно указать fill 25 .. za .. z\ .. zz .. zq .. cycle; 20 = (.8[xi,x2],. 5(2/1,2/4]); for k = 1 upto 6: z'k = .2[2fc,2o]; endfor unfill z'b .. 24 .. z[ .. z'z •. z'q .. cycle. 4.7. \[North, \[North, West]] = |[90, |[90,180]] = £[90,135] = 112.5. 4.8. 30°, 60°, 210° и 240°. Так как прибавление и вычитание 360° смысла не меняет, ответы -330°, -300°, -150° и -120° также верны. 4.9. 2i/ = (25,30), 2ir = (25,20). 4.10. Он указал: 'penstroke zie{up} .. Z2e{left} - • zze{down} .. 24e{right} .. cycle'. 4.11. Концы диагональных линий обрезаются перпендикулярно к (w,h) и (w, —h): хи = X4i = 0; Х2 = Х5 = .5w; ХЗг = Хбг = w, 2/ir = 2/2 = 2/з/ = h;
Приложение А: Ответы ко всем упражнениям 247 У\т = 2/5 = 2/6/ = 0; zv = .25[21,*б]; *б' = .75[г1,ге]; thetai := angle(tu, — h) + 90; penposi (6, t/ietai); penpos6(6, thetai); 27 = .5[zi,26]; penpos7(.6fe,i/ietai); penpos^ (6, i/ietai); penpos6, (6, f/ietai); penstroke zie •. Ziie{z& — z^} .. zye - • {z& — Zii}z&e .. z§e\ z3i = .25(23,24]; zv = .75(23,24]; thetaz := angle(-w, — h) + 90; penpos3(b, thetaz)', penpos4(b, thetaz)', 2g = .5[2i,2б]; penpos8(.66,thetaz); penpos3, (6, thetaz)] penpos4, (6, thetaz)', penstroke z3e .. 23/e{z4/ - 23/} .. 28e •. {24/ - гг>}г4>е .. 24e; penpos2(b, 0); penpos5(6,0); penstroke 22e • 2se• 5.1. Ширина равна 0.8em#, a em# равен 10 "настоящим" пунктам. Таким образом, ширина рамки, выраженная в единицах, не зависящих от устройства, равна 8 pt. Высота равна 7pt. (Глубина относительно базовой линии равна Opt.) 5.2. 8 х 3.6 = 28.8 округляется до значения w = 29; точно так же h = 25. (A d = 0.) 5.3. Вот один из возможных способов, в котором для контроля за шириной пера на концах линий используется переменная slab: slab#:=.8pt#; define.blacker.pixels(slab); beginchar("S",5/9em#,cap#,0); "The letter S"; penposi(slab,70); penpos2(.5slab,80); penpos3(.5[slab,thick],200); penpos5(.5[slab,thick],210); penpos6(.7slab,80); penpos7(.25[slab,thick],72); xl=x5; ylr=.94h+o; x2=x4=x6=.5v; y2r=h+o; y4=.54h; y61=-o; x3r=.04em; y3=.5 [y4,у2]; x51=w-.03em; y5=.5 [у4,уб]; .5[x71,x7]=.04em; y71=.12h-o; path trial; trial=z3{down}..z4..{down}z5; pair dz; dz=direction 1 of trial; penpos4(thick,angle dz-90); penstroke zle..z2e{left}..z3e{down} . .z4e{dz}..z5e{down}..z6e{left}..z7e; penlabels(l,2,3,4,5,6,7); endchar; Для того чтобы найти угол наклона пера в точке 4, METAFONT было позволено построить пробный путь, проходящий через центральные точки, после чего использовалось перпендикулярное направление. Эти буквы выглядят вполне неплохо в их номинальном размере: 'SO 10 IS ISIS'.* 5.4. Дойдя до конца нестандартного выражения METAFONT полагает, что находится в конце утверждения или команды, поэтому ожидает, что следующим символом будет точка с запятой. Чтобы система METAFONT была довольна, вы должны набрать, например, 'I; mode_setup\ 5.5. Да. * Если приглядеться, то в этой последовательности символов можно увидеть фразу на английском, которая в переводе звучит так: "Итак, Ио — это Исис". — Прим. перев.
248 Приложение А: Ответы ко всем упражнениям 6.1. (а) Нет, вторая лексема представляет 65^36. (Значение лексемы совпадает со значением 'О' только в том случае, когда его разложение в десятичную дробь строго меньше, чем 2~17 = .00000 76293 94531 25.) (Ь) Да, обе лексемы представляют 65*36, поскольку 1 — это ближайшее целое число, как для .00001 х 65536 = .65536, так и для 0.00002 х 65536 = 1.31072. (с) Нет, 0.00003 представляет 65^36. (d) Да, обе эти лексемы имеют значение "слишком большое число, которое нужно уменьшить"; в обоих случаях METAFONT сообщает об ошибке и подставляет наибольшую допустимую числовую лексему. (Округление 4095.999999 до ближайшего числа, кратного 65*36, дает 4096, а это число слишком большое.) 6.2. |хх|, |3.1[ (числовая лексема), 1.6| (еще одна числовая лексема), [77], | [[[, [а], ЕЗ, ГЬсЗП, 0, Q], [Г], 1"а %"| (лексема-цепочка), R1TL Ш (см. правило 5), Ш. В, Ш (числовая лексема), \ъ\ (также число), |'Ч—"| (лексема-цепочка) и |""| (лексема-цепочка, которая обозначает пустую последовательность символов). Все они являются символьными лексемами, если только в скобках не указан другой тип. (Заметьте, что четыре пробела и две точки были стерты, в соответствии с правилом 1. Проверить, что METAFONT находит именно эти лексемы, можно, например, таким способом: записать файл, в первой строке которого содержится фраза 'isolated expression;', а во второй — рассматриваемое выражение. Когда METAFONT станет выдавать сообщения об ошибках, нужно каждый раз отвечать ему, набирая '1'. Тогда за один раз METAFONT будет стирать по одной лексеме.) 6.3. Утверждение в целом верное, но может быть неверно истолковано. Вы можете вставить любое число пробелов между лексемами, не меняя смысла программы, однако нельзя вставить пробел внутрь лексемы, ничего при этом не изменив. Вы можете стереть пробелы между лексемами, если только это не приведет к "склеиванию" смежных лексем. 6.4. Неверно. Может показаться, что лексемы этого нового типа будут распознаваться только в тех случаях, когда за точкой не следует цифра, поскольку точка все равно будет отбрасываться по правилу 1. Однако, если это новое правило применить, например, к строке 'draw zl. .z2', то последствия будут катастрофическими! 7.1. Можно вставить пробел между нижними индексами, как например в 'al 5'. (Как мы увидим позже, обратная косая черта действует как нулевой символ, следовательно, 'al\5' — еще одно решение.) 7.2. Нет; не используя [ и ], нельзя получить доступ к а[-1]. Без квадратных скобок (нижним индексом) могла бы быть только (числовая лексема) с неотрицательным значением. (По правде говоря, вы могли бы указать 'let ?=[; let ??=]' и затем сослаться на 'а?-!??', но это было бы жульничеством.) 7.3. Если предположить, что в момент, когда он указал 'let plus=+', лексема '+' все еще была "вызовом", то он не сможет сослаться на переменную 'a.plus 1', если только снова не изменит смысл лексемы plus так, чтобы сделать ее "ярлыком". (Сделать это, не меняя без конца смысл plus, можно при помощи конструкции 'begingroup save plus; a.plusl endgroup'.) 7.4. Верно. (Однако (суффикс) — это не всегда (переменная).) 7.5. Да, так как этим мы удалим любое значение, которое имела переменная х, независимо от ее типа. Иначе вы не могли бы без риска использовать х в уравнениях, где фигурируют числа. Если вы не уверены в том, какой статус имела до сих пор переменная, или вас не интересует ее предыдущее значение, то вполне разумно объявить ее как числовую переменную. Кроме того, явные объявления переменных типа numeric, как и комментарии, полезны в качестве документации. (Кстати, объявление 'numeric х' никак не влияет на такие переменные, как 'х2' или 'х.х', которые, возможно, используются в программе.)
Приложение А: Ответы ко всем упражнениям 249 7.6. (а) Нижние индексы должны быть общими, поэтому запись '42' неверна. (Ь) Согласно правилам, (объявляемая переменная) должна начинаться с (символьной лексемы), а не с (числовой лексемы), поэтому запись '24' неверна, (с) В последующих запятых нет ошибки; со второй запятой начинается (объявляемая переменная), поэтому она утрачивает свой прежний смысл и превращается в ярлык. Таким образом, METAFONT пытается объявить переменную ',t,path', но лексема 'path' не может содержаться в суффиксе, поскольку является "вызовом". (Это, надо признаться, действительно сложно. Но ничего не поделаешь — компьютеры подчиняются правилам.) 8.1. ((zl+z2).. ((z3/4)*5))..(z6-(7*(8z9))). 8.2. Вначале вычисляется значение 100/3 (поскольку такое деление имеет приоритет предшествования); затем ошибка, возникающая при округлении, увеличивается в 100 раз. 8.3. Операция sqrt имеет приоритет предшествования перед любой другой операцией с двумя операндами, поэтому машина производит вычисления в таком порядке: (sqrt 2)**2. По счастливой для системы METAFONT случайности результат оказывается равным в точности 2. (При выполнении операции sqrt ответ вычисляется с точностью до ближайшего числа кратного 65^36, и при возведении в квадрат погрешность увеличивается. Если вы попробуете вычислить sqrt 3**2, то получите 3.00002; точно так же, sqrt 2**4 оказывается равным 4.00002.) Кстати, в базовом формате METAFONT операция ** имеет такой же порядок приоритета предшествования, как * и /; поэтому 'х*у**2' значит то же, что и '(х*у)**2', а '-х**2', в отличие от соглашений принятых в языке программирования FORTRAN, значит '(-х)**2\ 8.4. Так как оператор 'or' имеет приоритет предшествования перед '<' и '>', то система METAFONT попытается произвести вычисления в следующем порядке, задаваемом скобками: '(0 > (1 or а)) < а'. Но выражение '1 or а' не имеет смысла, поскольку оператор 'or' применим только к булевым переменным; в таких случаях в качестве результата METAFONT выбирает правый операнд. Выражение '0 > а' не определено, так как переменная а неизвестна; METAFONT считает такое выражение ложным. Наконец, 'false < а' — это еще одна неправильная комбинация переменных разного типа. 8.5. Лексема '++-' не определена, поэтому является "ярлыком"; следовательно, ++-7 — индексированная переменная, умноженная на ноль. 8.6. Ассоциативное правило справедливо лишь для точных вычислений, но не для вычислений с округлением. Например, даже в случае с умножением оно не срабатывает, поскольку при вычислениях с округлением до ближайшего кратного 65*36 получается (.1 * .1) * 10 = 0.09995, в то время как .1 * (.1 * 10) = .1. Однако это не до конца объясняет наш случай, когда, вычисляя 2 ++ 4 с предельной точностью, система METAFONT по идее должна всегда выдавать ответ 7! Ближайшее приближение \/20 есть 4|||||, но 2 +4- 4 оказывается равным 4|Щ|. Дело в том, что METAFONT считает правильным ответом самое точное приближение при выполнении операций умножения, деления и извлечения квадратных корней, но не при выполнении пифагоровых операций. 8.7. Из последовательности вида '(числовая лексема) (числовая лексема)' построить выражение невозможно, т.к. это противоречит правилу синтаксиса для (скалярного оператора умножения). Система воспринимает первый символ '2' как (первичное числовое), которое однозначно рассматривается как (числовое выражение). Второй символ '2' будет рассматриваться как лишняя лексема и отбрасываться после выдачи сообщения об ошибке. 8.8. Если после числовой лексемы стоит последовательность '/(числовая лексема)', и перед ней не стоит '(числовая лексема)/', то, согласно правилам синтаксиса, она может стать частью некоторого выражения только благодаря первой из альтернатив, упоминаемых в правиле для выражения типа (первичная числовая лексема). Следовательно, последовательность '1/2/3/4' должна интерпретироваться как '(1/2)/(3/4)', а последовательность 'а/2/3/4' должна интерпретироваться как 'а/(2/3)/4'.
250 Приложение А: Ответы ко всем упражнениям 8.9. (первичная цепочка) —> (переменная-цепочка) | (лексема-цепочка) | ((выражение-цепочка) ) | substring (выражение-пара) of (первичная цепочка) (вторичная цепочка) —У (первичная цепочка) (третичная цепочка) —> (вторичная цепочка) (выражение-цепочка) —► (третичная цепочка) | (выражение-цепочка) ft (третичная цепочка) (В полном перечне правил синтаксиса, приведенном в главе 25, присутствует еще несколько вариантов величины (первичная цепочка), которые мы до сих пор не указывали.) 9.1. (а) Рассматривая только положение по горизонтали, можно сказать, что точка 1 должна лежать на девять пикселей левее точки 7; относительно расположения точек по вертикали ничего сказать нельзя. (Ь) Точка 7 должна сидеть непосредственно над или под точкой 4, а расстояние от нее до базовой линии по вертикали должно составлять половину расстояния по вертикали между точками 4 и 5. (с) Если центр пера, выбранного в данный момент, находится в точке 21, то его левый край должен находиться на один пиксель правее левого края этого же пера с центром в точке 20. (Таким образом, между образами пера в точках 20 и 21 должно остаться белое пространство шириной в один пиксель.) 9.2. (а) 2/13 = -уп (или -t/i3 = г/и, или г/13 + уп = 0). (b) zw = z\2 + (mm,-1). (c)z43 = H(0,/i),K-d)]. 9.3. (a) z\ = 22 = zz = (w,/i); 24 = 25 = z$ = (0,0). (b) z\ = z& = (.5u;, .5h); z2 = (.75w, .75Л); 23 = (w,h); 24 = (0,0); 25 = (.25w, .25/i). 9.4. 2 = whatever[2 1,22]; 2 = whatever-[23,24]. (Кстати, было бы интересно проследить эти вычисления. Запустив METAFONT, установите \tracingequations:*tracingonline:=l и укажите, например, z=vhatever[(l,5),(8,19)]; z=whatever[(0,17),(6,1)] ; Решение появится, как по волшебству. Если бы вместо whatever, вы использовали alpha и beta, то машина точно так же вычислила бы значения alpha и beta.) 9.5. 2 = whatever[z\,Z2]', 2 — 23 = whatever * (25 — 24). 9.6. 2ц — 2i2 = whatever * (213 — 214) rotated 90 в предположении, что известна разность 2i3 — 2i4. (Можно также указать '(2ц — 212) dotprod (213 — 214) = 0', хотя в этом случае есть риск выйти за пределы допустимых чисел, если значения координат велики.) 9.7. Одно из возможных решений состоит в том, чтобы, используя такие же идеи, как и в двух предыдущих упражнениях, построить такую точку 24 на прямой 22 . • 2з, чтобы прямая 24 •. 2i была перпендикулярна прямой 22 .. 23: '24 = whatever^, 23]; 24—21 = whatever + (23 — 22) rotated 90'. Тогда искомое расстояние есть длина (24 —2i). Но существует и более хитрое решение: достаточно вычислить abs ypart((2i — 22) rotated — angle(zs — 22)). 9.8. Было бы неплохо иметь возможность просто указать Kz = whatever'[22,23]', а затем — либо 4ength(2 — 21) = Г, либо lz — z\ =■ (/, 0) rotated whatever'. Но ни одно из этих уравнений не является правильным решением. (Правильное решение не может иметь такой вид, поскольку это решение определяет точку z единственным образом, а искомых точек в общем случае две.) Вероятно, лучше всего сначала вычислить точку 24, как в предыдущем упражнении, а затем положить v = (/Н hlength(24 — 2i))*unitvector(23 — 22); тогда искомые точки имеют вид 24 + v и 24 — v.
Приложение А: Ответы ко всем упражнениям 251 9.9. Такое уравнение не содержит новой информации о переменных а и 6. Действительно, при каждом использовании whatever появляется новая независимая переменная, а на каждую новую переменную "тратится" одно уравнение, так как для того, чтобы определить значения п неизвестных, нужно п уравнений. С другой стороны, уравнение, связывающее пары, считается за два. Поэтому, когда whatever появляется в уравнении для пар, то мы имеем одно уравнение "чистой прибыли". 10.1. Да, но это нужно делать в два шага: 'numeric newcode\ newcode = code + 1; numeric code] code = newcode1. 10.2. Присваивание 'хз := whatever1 позволит вам сделать как раз то, что вы хотите. 10.3. Результат показывает, что теперь si = S3 = «4 и 52 = ss = sq: s[]=unknown string sl=unknown string s3 s2=unknown string s6 s3=unknovn string s4 s4=unknown string si s5=unknown string s2 s6=unknown string s5 (Присваивание S2 := 55 разрушает прежнюю связь S2 с si, S3 и S4.) 10.4. В результате получится ## а=1 ## а=Ъ+1 (после первого присваивания) ## Ъ=0. 5а-0.5 (после второго присваивания) ### -1.5a=-'/,CAPSULEnnnn-0.5 (после третьего присваивания) ## a=7,CAPSULEnnnn (после третьего, см. ниже) » а (после 'show'; переменная а независима) » 0.33333а-0.33333 (это окончательное значение 6) Пусть а*; — значение переменной а после к присваиваний. Тогда ао = 1, а значение ai зависит от независимой переменной 6. Затем ai уничтожается, и b становится зависимой от независимой переменной ог- Таким образом, правая сторона в третьем присваивании имела вид аг + Ь. К тому времени, когда значение аг должно было быть уничтожено, система METAFONT имела два уравнения зависимости: b = О.баг —0.5 и к = 1.5аг — 0.5, где к — безымянная "капсула" (capsule) в компьютере, содержащая новое значение, которое должно было быть присвоено. Поскольку переменная к имела больший коэффициент зависимости, чем Ь, система METAFONT выбрала к на роль независимой переменной, после чего во всех уравнениях зависимости произошла замена —1.5а2 на —к — 0.5. Поэтому значение переменной b было равным О.ЗЗЗЗЗк — 0.33333. После третьего присваивания к исчезла, и вместо нее независимой стала переменная аз- (Строчка '## a='/,CAPSULEnnnn' означает, что до того, как переменная к была уничтожена, а временно зависела от к. Если бы уравнение а = к задавало зависимость к от а, а не наоборот, то строчка с '##' в начале не была бы напечатана; такие строки пропускаются, когда капсула или часть капсулы становится зависимой, если только вы не указали значение tracing capsules > 0.) 11.1. Почти, но не совсем. Значения стандартных переменных единиц измерения, таких, как pt и mm, при обеих установках будут идентичны, равно как и значения специальных переменных, таких, как ет и x_height. Однако "перьевые" единицы измерения, которые определяются посредством define-blacker.pixels, будут слегка различаться, поскольку в режиме cheapo установлено значение blacker = 0.65, тогда как в режиме luxo установлено значение blacker =0.1 (в силу того, что принтер luxo имеет другие физические характеристики). Точно так же, процедура define.corrected.pixels (которую мы очень скоро
252 Приложение А: Ответы ко всем упражнениям рассмотрим) будет давать несколько различные результаты, будучи примененной в этих двух режимах работы. 11.2. При увеличении ht# сама буква и ограничивающая ее рамка станут выше; при увеличении хдар# точка 5 сдвинется влево, так что средняя перемычка станет короче; при увеличении и* буква и ограничивающая ее рамка станут шире; при увеличении s# сама буква не изменится, но ограничивающая ее рамка станет шире в обе стороны; при увеличении о* точки 4, 5 и 6 сдвинутся вправо; при увеличении рх# перо станет толще (но при этом верхний край верхней перемычки, нижний край нижней перемычки, середина средней перемычки и ножка останутся на прежних местах.) 11.3. Единственное, что может стать неожиданностью, — это положение t/i, которое соответствует аналогичным элементам букв 'М' и 'Т' из главы 4: beginchar("F",14*u#+2s#,ht#,0); pickup logo.pen; xl=x2=x3=leftstemloc; x4=w-xl+o; x5=x4-xgap; y2=y5; y3=y4; bot yl=-o; top y3=h; y2=barheight; draw zl—z3—z4; draw z2—z5; labels(l,2,3,4,5); endchar; 11.4. Величина, которая в главе 4 именовалась 55, теперь называется leftstemloc. beginchar("M",18*u#+2s#,ht#,0); pickup logo.pen; xl=x2=leftstemloc; x4=x5=w-xl; x3=w-x3; yl=y5; y2=y4; bot yl=-o; top y2=h+o; y3=yl+ygap; draw zl—z2—z3—z4—z5; labels(1,2,3,4,5); endchar; beginchar("T",13*u#+2s#,ht#,0); pickup logo.pen; lft xl=0; x2=w-xl; x3=x4=.5w; yl=y2=y3; top yl=h; bot y4=-o; draw zl—z2; draw z3—z4; labels(l,2,3,4); endchar; 11.5. 'NONfiTONEMENT'; возможно также 'METfiFOOTNOTE'; если же верить Джорджии Тобин, то 'fiNTE ENFEOFFMENT — также допустимый термин. 11.6. Сотрите из файла logo.mf строчку, в которой определяется величина barheight#, и вставьте ее в каждый из параметрических файлов logolO.mf, logo9.mf и logo8.mf. Теперь, создавая новые параметрические файлы, можно изменять высоту перемычки. Таким образом, наш меташрифт переходит на новый метауровень. 11.7. (Делается это довольно хитро.) Вставьте в файл logo.mf строчки if known pixmag: begingroup interim hppp:=pixmag*hppp; special "title cheapo simulation" endgroup; extra_endchar:="currentpicture:=currentpicture scaled pixmag;" ft "w:=w*pixmag;" ft extra_endchar; fi сразу после 'mode\_setup' и в самый конец файла поместите строку if known pixmag: hppp:=pixmag*hppp; vppp:=pixmag*vppp; fi Затем запустите METfi FONT с командной строкой \mode="cheapo"; input cheaplogolO где файл 'cheaplogolO.mf' содержит всего одну строку вида 'pixmag=10; input logolO'. (Задавая значение промежутка hppp и используя команду special, мы перехитрим систему METfi FONT, заставив ее дать нужное расширение имени gf-файлу. Кстати, если вы укажете системе TeX 'cheaplogolO scaled 10000', то сможете печатать этим шрифтом, увеличенным
Приложение А: Ответы ко всем упражнениям 253 десятикратно, на устройстве cheapo; а работая с устройством luxo, вы можете называть этот шрифт просто 'cheaplogolO'.) 12.1. Все изменения достаточно очевидны, за исключением курсивной поправки (для которой вполне годится грубая оценка вроде той, что использована здесь): "Right parenthesis"; numeric ht*, dp*] ht* = body.height*; .b[ht*, -dp*] = axis*] beginchar (")", 7u#, ht#, dp*); italcorr axis* * slant — .bu*\ pickup fine.nib; penposl(hair — /me,0); penpos2(.7b[thin, thick] — /me,0), penposz(hair — fine,0)] Iftxu = Iftxzi = u; rtx2r =xi+4u] top t/i = h\ y2 = -5[у1,уз] = axis] filldraw zu{(z2i — 2ц) xscaled 3} ... Z2i... {(23/ — Z21) xscaled 3)2:3/ -- Z3r{(z2r — 2зг) xscaled 3} ... z2r -. • {(zir — Z2r) xscaled 3}zir -- cycle; penlabels(l, 2,3); endchar; В главе 15 мы увидим, что, используя трансформации рисунков, можно добиться идеальной симметрии правой и левой скобок. 12.2. При наборе горизонтальных строк система TeX оставляет для них полоски, высота и глубина которых равны максимальной высоте и максимальной глубине рамок в строке; так она определяет, потребуется ли вставить дополнительные пробелы между базовыми линиями этих строк. Высота и глубина также используются в тех случаях, когда нужно поместить значок над или под символом и разместить символы в математических формулах. Кроме того, иногда рамки стыкуются вертикально, и тогда их высота и глубина имеют столь же важное значение, как и ширина при горизонтальном наборе. 13.1. (4,4), (4, 5), (5,5), (5,4). (Таким образом, команда unfill (4,4) - - (4,5) -- (5, 5) - - (5,4) - - cycle уменьшит значение этого пикселя на 1.) 13.2. Результат был бы точно таким же. Команды fill и unfill могут применяться в любом порядке. (Если вначале применяется команда unfill, то некоторые пиксели будут иметь значение —1, а остальные — нулевые значения.) 13.3. unfill (4,1) -- (4,8) -- (5,8) -- (5,1) --cycle. 13.4. Вот два из множества возможных решений: fill (0,3),-- (9,3) -- (9,6) -- (6,6) -- (6,9) -- (3,9) - - (3,0) - - (6,0) - - (6,6) - - (0,6) - - cycle; fill (0,3) -- (9,3) -- (9,6) -- (0,6) -- (0,3) -- (3,3) -- (3,0) -- (6,0) - (6,9) -- (3,9) -- (3,3) -cycle. (Оказывается, что любой массив пикселей может быть получен при помощи одной, достаточно разветвленной команды fill. Однако неестественные команды, как правило, неэффективны и неудобочитаемы.) 13.5. Значения пикселей, ограниченных данной линией, увеличатся на 2. ( Этого, как мы увидим позже, можно добиться и более простым способом.) 13.6. Да, верно; j — к = / — ш, поскольку к + I = j + т. (За каждым восхождением обязательно следует спуск.) 13.7. Тут весь фокус в том, чтобы не забыть, что 'erase draw Z{ -- Zj1 сотрет пиксели в окрестности точек Zi и Zj. Поэтому, если линию zz - - za нарисовать прежде, чем линию 24 -- гг, то мы не сможем стереть z\ -- 22, не потеряв при этом части 23 -- z\\ а нам нужно
254 Приложение А: Ответы ко всем упражнениям стереть только лишь часть одной линии. Один способ решить эту проблему — проделать следующее после того, как определены точки и выбрано перо: draw 23 — 24; draw 25 -- ze; cullit; pickup pencircle scaled 1.6pt; undraw Z7 -- ^[27, 25]; undraw z2 -- ^[22, 24]; cullit; pickup pencircle scaled Apt; draw 23 - - z\ - - 22 - - 24; draw 25 - - 27 - - z% - - 26; for A; = 1 upto 4: draw 2fc -- 2fc+4j endfor. (Заметьте, что стереть линию только от 27 до 5(27,25]!) Эту проблему можно также решить, не прибегая к частичному стиранию. Но для этого нужно использовать дополнительные возможности системы METAFONT, о которых мы пока еще вам не рассказывали. Рассмотрим процесс рисования только двух линий: 27 - - 25 - - 2в и 2з - - 24 - - 22, поскольку остальные восемь линий можно потом без особого труда дорисовать. В альтернативном решении номер 1 используются операции над рисунками: pen eraser; eraser = pencircle scaled l.6pt; draw 23 --24; erase draw 27 -- 25 withpen eraser; draw 27 -- 25; picture savedpicture; savedpicture = currentpicture; clearit; draw 26 -- 25; erase draw 22 -- 24 withpen eraser; draw 22 -- 24; addto currentpicture also savedpicture. Альтернативное решение номер 2 сложнее, но поучительнее; в нем к командам добавляется расширение 'withweight' и используется то, что если путь прямолинейный, то команда draw увеличивает значения пикселей не более чем на указанный в этом расширении "вес": draw 23 --24; undraw 27 -- 25 withpen eraser; draw 27 -- 25 withweight 2; cullit withweight 2; draw 26 --25; undraw 22 -- 24 withpen eraser; draw 22 --24 withweight 2; (Эти альтернативные решения были предложены Брюсом Лебаном.) 13.8. Вот решение, аналогичное первому решению предыдущего упражнения: beginchar('•*••, 10р*# 7pt#, 2pt#); pair center; ...(см. указания) pickup pencircle scaled Apt; draw star; cullit; pickup pencircle scaled l.6pt; for k = 0 upto 4: undraw subpath(A; + .55, k + .7) of star; endfor cullit; pickup pencircle scaled Apt; for k = 0 upto 4: draw subpath(fc + .47, k + .8) of star; endfor labels(0,l,2,3,4); endchar. Однако, как и в предыдущем случае, существует альтернативное решение номер 1, принадлежащее Брюсу Лебану, которое более предпочтительно, поскольку свободно от непонятных констант, таких как 0.55 или 0.47: beginchar ... (см. выше) ... scaled Apt; picture savedpicture; savedpicture = nullpicture; pen eraser; eraser := pencircle scaled l.§pt; for k = 0 upto 4: draw subpath(fc,k + 1) of star; cullit; undraw subpath(A; -I- 3, k -f 4) of star withpen eraser; cullit; addto savedpicture also currentpicture; clearit; endfor currentpicture := savedpicture; labels(0,l,2,3,4); endchar.
Пргможение А: Ответы ко всем упражнениям 255 13.9. Она увеличивает на 1 значения пикселей, принадлежащих лучам этой звезды, и на 2 — значения тех пикселей, которые лежат в центральной ее части, напоминающей правильный пятиугольник. 13.10. def overdraw ехрг с = erase fill с; draw с enddef. 13.11. Для начала нужно обобщить макрос overdraw из предыдущего упражнения так, чтобы его можно было применять к произвольному циклическому пути с, даже к такому, который имеет самопересечения: def overdraw ехрг с = begingroup picture region; region := nullpicture; interim turningcheck := 0; addto region contour c; cull region dropping (0,0); cullit; addto currentpicture also —region; cullit; draw с endgroup enddef; (В этом определении используются операции, которые будут описаны ниже в этой главе. Макрокоманда overdraw стирает область region, значения пикселей в которой после применения команды 'fill с' могут быть ненулевыми.) Ремешок, изображенный на логотипе, формируется из звеньев, которые рисуются по отдельности. Звенья получаются последовательным применением команды overdraw, причем сначала изображаются те, которые находятся на заднем плане: beginchar(M, 1.25tn#, .5tn#,0); pickup pencircle scaled Apt; zi = (20, -13); 22 = (30,-6); zz = (20,1); z* = (4, -7); zb = (-12, -13); z6 = (-24,-4); z7 = (-15,6); path M; M = (origin .. z\ .. z2 .. z3 .. zA .. zb .. z6 .. zl.. origin .. — zl.. — z6 .. —zb .. — zA .. — z3 .. — z2 .. —zl .. cycle) scaled (Zi/26) shifted (.5u>, .5/i); def link(expr n) = overdraw subpath |(n,n-|-l)ofM -- subpath |(n -f 25, n + 24) of M -- cycle enddef; for к = 1 upto 12: link (A; -f 11); link(12 — к); endfor endchar; 13.12. После "подравнивания" командой cullit массив пикселей \\ примет вид }}, и системе METAFONT понадобиться рассортировать грани; поэтому результат будет выглядеть так: row 1: 10+2- row 0: I 0+ 2- 13.13. Непосредственно перед финальным вращением массив пикселей будет иметь вид: 2i + 2i + i2~"43=ii> причем точка отсчета будет находиться в левом нижнем углу пикселя со значением 4; а после вращения массив примет вид \ J, и точка отсчета будет совпадать с нижним правым углом пикселя со значением 4. Для того чтобы выполнить вращение, системе METAFONT придется сортировать грани, однако при переходе через каждую грань значение может меняться не более, чем на ±3. Знать это не обязательно, но иначе нельзя понять отчет, выданный системой: row 1: | -2++ -1+ 0 row 0: I -2+ -1+++ 0 0- 13.14. Трансформации подвергаются не значения пикселей, а их координаты, поэтому 'V scaled-1' — то же самое, что 'V rotated 180'. (Отметим, между прочим, что можно использовать трансформации 'V xscaled-1' и 'V yscaled-1' и что 'V scaled-1' — это все равно, что 4V xscaled-1 yscaled-1'.)
Приложение А: Ответы ко всем упражнениям 13.15. Результат будет тот же, что и для 'V shifted (2,3)'; при сдвиге рисунка координаты сдвига округляются до ближайших целых чисел. 13.16. row row row row 3: 2: 1: 0: 0+ 4- | 0+ 4- | 0+ 4- 0+ 0+ 4- 0+ 2- I 2- I (Масштабировать рисунки можно только в целое число раз.) 13.17. На текущий момент система METAFONT выполняет инструкции, которые она получила, считав из файла expr.mf все до строки 5 включительно. 13.18. Значения пикселей в currentpicture станут равными 1, если до этого они равнялись ±1, в противном случае они станут равными 0. 13.19. (a) addto Vi also V2; cull V\ keeping (2,2). (b) To же, но с заменой аргумента keeping на (1,2). (с) То же, но с заменой аргумента keeping на (1,1). 13.20. Надо вычесть один рисунок из другого и применить к разности команду cull с расширением dropping (0,0), а затем проверить, равен ли суммарный вес нулю. 13.21. (а) То же, что и 'draw р\ но с использованием q вместо пера, выбранного на данный момент. (Ь) Результат тот же, что и для 'draw р; draw р; draw р' (но быстрее), (с) То же, что и 'draw р withweight w\ так как команды undraw с расширением 'withweight — Г аннулируются. (d) То же, что и 'unfilldraw с; unfilldraw с', но с использованием q вместо пера currentpen. (е) То же, что и 'erase fiUdraw с', так как расширение 'withweight 2' аннулируется. [Поскольку команда erase сделала все веса равными 0 или 1, нет необходимости в "двойном стирании".] (f) Тот же результат, что и для 'cullit; addto currentpicture also currentpicture' (но быстрее). 13.22. vardef safefill expr с = save region:, picture region', repton=nullpicture; interim turningcheck := 0; addto region contour c; cull region dropping (0,0); addto currentpicture also region enddef. 13.23. cull currentpicture keeping (1, infinity); picture u; v := currentpicture] cull currentpicture keeping (1,1) withweight 3; addto currentpicture also v — v shifted right — v shifted left — v shifted up — v shifted down] cull currentpicture keeping (1,4). 13.24. (Мы предполагаем, что задана начальная конфигурация currentpicture, в которой значения всех пикселей равны нулю или единице; единица означает "живой".) picture v; def с = currentpicture enddef; forever: v := с; showit; addto с also с shifted left -f с shifted right] addto с also с shifted up + с shifted down; addto с also с — v\ cull с keeping (5, 7); endfor. (Было бы разумно не тратить слишком много времени, наблюдая за работой этой программы.) 14.1. beginchar ("Ъ", bpt#, bpt#, 0); fill ((0,0) -- quartercircle --cycle) scaled Wpt; end char.
Приложение А: Ответы ко всем упражнениям 257 14.2. Путь quartercircle соответствует окружности единичного диаметра, а радиус ее равен £. 14.3. beginchar ("с", 5р*#,5р*#,0); pickup pencircle scaled (Apt + blacker); draw quartercircle rotated 90 scaled lOpt shifted (5p£,0); endchar. 14.4. beginchar ("d", bpt* * sqrt 2, 5p*#, 0); pickup pencircle scaled (Apt + blacker)] draw ((0,0) -- quartercircle --cycle) rotated 45 scaled lOpt shifted (.5tu,0); endchar. 14.5. beginchar("e", 10p*#, 7.5p*#, 2.5j>*#); pickup pencircle scaled (Apt + blacker); for D = .2u>, .6w, w: draw fullcircle scaled D shifted (.5tu, .5[—d, /i]); endfor endchar. Программа для '^>' имеет такой же вид, но 4fullcircle scaled £>' в ней заменяется на unitsquare shifted —(.5, .5) rotated 45 scaled (D/sqrt 2). 14.6. Появятся точки перегиба, поскольку в определении макроса superellipse (см. приложение В) участвуют операции '...', а для них ограничивающие треугольники существуют только тогда, когда .5 < з < 1. 14.7. (0,0) .. (1,0) & (1,0) .. (1,1) & (1,1) .. (0,1) & (0,1) .. (0,0) & cycle. Кстати, если каждый символ '&' в этом пути заменить на '..', то мы получим путь, который проходит через те же самые точки; но это будет путь длины 8, который полностью останавливается в каждом из углов. Иными словами, путь остается неподвижным в моменты времени 1<£<2,3<£<4и т.д. Этот путь длины 8 ведет себя несколько странно по отношению к операции 'directiontime'. Лучше использовать '&', чем повторно записывать точки пути. 14.8. Пусть Si = zi - г0, <Ь = z2 - zi, <*з = z0 - z2\ h = |<$i|, h = |<Ь|> *з = |<Ы; xpi = arg(^2/<5i), ^2 = arg(^3/<b), грз = arg^i/fo)- Уравнения, которые нужно решить, — это уравнения (*) и (**) для 1 < к < 3, где аз = аю и Ра = Pi- Эти шесть уравнений определяют 0i, 02,#з и ф\,02,фз- 14.9. Это путь длины 9, эквивалентный пути вида '(0,1) -- (1,1) -- (1,2) -- (0,2) -- (0,1){Жшп} .. {right}(l,0) -- (2,0) -- (2,1) -- (1,1) -- (1,0)'. Путь unitsquare — циклический, но цикл разбивается, если он используется внутри большего пути. Поэтому в результате получается нециклический путь, который приходит в конечную точку, двигаясь в направлении down, а из начальной точки выходит, двигаясь в направлении right. 14.10. Да. Например, путь *zo .. zi .. Z2 -- zz был бы эквивалентен пути 'zo .. z\ .. {curl2}z2{curl2} .. {сиг12}гз'. Но такой путь, как 'zo -- z\ -- zi -- гз\ не изменился бы, так как все направления в нем остались бы прежними. (Путь 'zo{curla} .. {curlb}zi' представляет собой прямую линию независимо от значений а и 6, поскольку при п = 1 уравнения (***) и (***') всегда имеют решение во = ф\ = 0.) 14.11. Она интерпретирует его как '((0,0) .. (3,3) .. cycle){curl 1}'; т.е. все что стоит до cycle включительно рассматривается как подпуть (сравните с 'р2' в главе 8). Этот цикл разбивается, после чего мы получаем путь '(0,0) .. controls (2, —2) and (5,1) .. (3,3) .. controls (1,5) and (—2,2) .. (0,0){curll}\ Наконец, часть '{curll}' отбрасывается, так как все контрольные точки известны. (Как вы, возможно, догадались, для ответа на этот вопрос одних только правил синтаксиса недостаточно. Вам также нужно знать, что, если система METAFONT использует вторую или третью из альтернатив в определении выражения типа (выражение-путь), то она вычисляет направления и контрольные точки.)
258 Пргллоэюение А: Ответы ко всем упражнениям 14.12. Верно. Длина пути есть число содержащихся в нем отрезков вида 'z* .. controls Uk andt?fc+i .. Zk+i после того, как определены все контрольные точки. 14.13. Верно, если 0 < t < 1, конечно, с точностью до ошибок округления; в противном случае неверно. Путь zo -- z\ раскладывается в '«го .. controls l/3[zo,£i] and2/3[zo,2i] • • z\ и полином Бернштейна упрощается, поскольку t[w, w + <5, w + 2£, w + 36] = w + StS. Кстати, точки 'point t of (zo — 2i)' и t[zo, zi] обычно не совпадают. 14.14. Если р — циклический путь или если его длина больше 4, то данный подпуть имеет длину 2. В противном случае его длина есть max(0, length р — 2). 14.15. vardef posttension ехрг t of р = save q\ path q\ q = point t of p {direction t of p) .. {direction t + 1 of p) point t + 1 of p; length (postcontrol 0 of q — point 0 of q) /length (postcontrol t of p — point t of p) enddef; vardef pretension expr t of p = save ^; path 4; q = point £ — 1 of p {direction t — 1 of p} .. {direction £ of p} point t of p; length(precontrol 1 of q — point 1 of q) /length(precontrol t of p — point £ of p) enddef; Указанная величина 'posttension' оказывается равной 4.54019. 14.16. Связку '&' нужно было заменить на '..', поскольку точка 'point t of р' может не совпадать с точкой 'point и of q\ 14.17. Поскольку при (£,£) путь р пересекается сам с собой бесконечное число раз, то задача может показаться невыполнимой. Однако решение есть, и его обеспечивает имеющаяся в системе METAFONT процедура поиска с бинарным перемешиванием. А именно: р intersectiontimes reverse р = (0.17227,0.28339), из чего мы можем заключить, что t = 0.17227 и1-« = 0.28339. 15.1. (х,-у). 15.2. (х,у) rotated 180, или (х,у) scaled —1. 15.3. Верно тогда и только тогда, когда xparti = ypartf = 0. Если данное уравнение справедливо хотя бы для одной пары (x,j/), то оно справедливо для всех (х,у). Согласно правилам синтаксиса, приведенным в главе 8, METAFONT интерпретирует '—(х,у) transformed V как '(—(х,2/)) transformed V. (Кстати, то, что на языке METAFONT называется преобразованиями, математики называют "аффинными преобразованиями", а частный случай, когда 'xpart' и 'ypart' равны нулю, называется "однородным".) 15.4. zi +z2- 15.5. heginchar(l26,25u#, hheight* + border*, 0); "Dangerous left bend"; currentpicture := dbend reflectedabout ((.5tu,0), (.5u>, h)); endchar; Ту же идею можно использовать для создания правых скобок как точных зеркальных отражений левых скобок, если эти скобки — не наклонные. 15.6. Надо вместо строки 9 подставить draw (zi.. .р) rotatedaround((.5tu, .5/i), —45) withpen pencircle scaled 3/4p* yscaled 1/3 rotated —15; 15.7. Надо заменить строку 10 на pickup pencircle scaled 3/4p* yscaled 1/3 rotated —60; draw (zi.. .p) transformed t;
Приложение А: Ответы ко всем упражнениям 259 16.1. Если имеются две точки с максимальной координатой у, то значение выражения 'penoffset (—infinity, epsilon) of p' будет равно Zk, а значение 'penoffset (—infinity,— epsilon) of p' будет равно Zk+i; значение же 'penoffset left of p' будет равно либо тому, либо другому. Если верхняя точка единственна, то ее можно получить при помощи любой из этих трех формул. (METAFONT допускает и такие перья, у которых три и более вершин лежат на одной прямой. Если число вершин с максимальной координатой у больше двух, то вы опять-таки можете использовать операцию 'penoffset' для нахождения первой и последней из них, как это делалось выше; если же вам непременно нужно найти все эти точки, то, применив команду makepath, вы сможете вывести их непосредственно.) 16.2. 'pencircle scaled 1.06060' — ромб, a 'pencircle scaled 1.06061' — уже квадрат. (Предполагается, что fillin = 0. Если же, к примеру, fillin = .1, то изменение происходит только тогда, когда значение диаметра достигает 1.20204.) Следующее изменение формы происходит при значении диаметра 1.5, и дает ромб вдвое больший первоначального. 17.1. (а + 1) + (2а + 2) = За + 3 и (2а) + (2а + 1) = 4а + 1, соответственно. Окончательное значение х в первом случае будет равно 2а ■+- 2, следовательно, а = .Ъх — 1; ответ 1.5х, который даст программа ехрг, будет выражен через новое значение х, поскольку про 'а' мы ей ничего не говорили. Точно так же во втором случае программа ехрг выдаст 2х-1. Из данного примера видно, что а + /? не обязательно равно /3 4- а, если в а и /3 входят выражения-группы. METAFONT находит значения выражений строго слева направо, выполняя то, что требуется утверждениями группы, в порядке их следования. 17.2. Этим лексеме '?' придается новый смысл; теперь '?' — это числовая переменная, не связанная с другими переменными. После того как группа заканчивается и лексема '?' приобретает прежний смысл, значение выражения-группы остается без имени. (Если вы захотите вывести его на экран при помощи команды show, то оно будет названо "капсулой".) Таким образом, значение группы есть новая, безымянная переменная, что и требовалось. 17.3. Это безымянная пара с равными компонентами 'xpart' и 'ypart', т.е. практически то же самое, что и 'whatever * (1,1)'. 17.4. Присваивание 'из := begingroup save ?; picture ?; ? endgroup' обновляет переменную-рисунок t>3, не меняя при этом такие переменные, как V2- Эта конструкция применима также к парам, перьям, цепочкам и т.д. 18.1. Да; направления в точке z. j будут равны, соответственно, left и right. 18.2. beginlogochar("AM5); xl=.5w; x2=x4=leftstemloc; x3=x5=w-x2; top yl=h+o; y2=y3=barheight; bot y4=bot y5=-o; draw z4—z2—z3—z5; superjialf (2,1,3); labels(l,2,3,4,5); endchar; Заметьте, что все три вызова подпрограммы super_half в файле logo.mf имеют вид 'super-half (2, j, 3)'. Но отбросить параметры ink — неудачная мысль и признак дурного стиля, несмотря на то, что это подпрограмма специального назначения. Если бы мы это сделали, она стала бы подпрограммой слишком специального назначения. И
260 Приложение А: Ответы ко всем упражнениям 18.3. Если bracket = 0 или serif-darkness = 0. (По видимому, задать значение величины serif-darkness равным нулю — не очень хорошая идея, так как при этом мы получим вырожденный треугольник '...', неустойчивый относительно вычислений, в которых присутствуют ошибки округления.) Еще один случай, по правде говоря, нежелательный, это когда left-jut = right-jut = 0. 18.4. Это довольно странный вопрос. Подпрограмма serif содержит команду penpos, которая устанавливает соотношения между точками z%\, 2$ и <г$г, и уже по отношению к ним serif устанавливает положения остальных шести точек. Для того чтобы расставить по местам все точки, вне подпрограммы пользователь должен определить всего одну координату х и одну координату г/. Это можно сделать и до, и после того, как будет вызвана подпрограмма serif, но вы облегчите системе METAFONT задачу, если сделаете это заранее. 18.5. Да; см. предыдущее упражнение. (Однако в программе для "А" необходимо определить координаты t/4/ и уы для того, чтобы можно было вычислить величины thetai и theta$.) 18.6. beginchar(,,H,,,13ti# Л<# 0); #i = #2 = #5 = 3ti; хз — ха = Хб = и) — xi; 2/ic = Узе = h; у2с = Уас = 0; serif (1, thick, —90, jut, jut); serif (2, thick,90, jut, jut); serif (3, thick, —90, jut, jut); serif (4, thick, 90, jut, jut); fill serif-edge 2 - - reverse serif-edge x - - cycle; fill serif-edge 4 - - reverse serif-edge 3 - - cycle; penpos5 (thin, 90); penpos6 (thin, 90); 2/5 = Уб = -52/i; penstroke 25e -- zee; penlabels(l, 2,3, 4,5, 6); endchar. 18.7. def topserif (suffix $)(expr xx, theta, left-jut, right-jut) — penpos$(xx,0); z$a — z$i = z$/ — z$r = (6racfce£/abssind theta) * dir theta; У$с = V$d = У$; *$c = *$* - left-jut; xu = x$r + right-jut; z$b = -г$/ + whatever * dir theta = z$c + whatever * dir —phi; z$e = z$r 4- whatever * dir theta = z$d + whatever * dir p/ii; labels($o, $6, $c, $d, $e, $/) enddef; def top serif-edge suffix $ = (z$a . . Controls Z$b . . Z$c -- (flex(z$c, .b[z$c, z$d] - dishing, z$d)) shifted (0, +epsilon) -- z$d • • controlsz$e .. z%f) enddef; 18.8. Если py = 0, то суммарный вес линии, нарисованной пером broad-pen за один проход, будет равен рх -sin(05 — ф), а ххх sin 6$ — это дополнительный вес, соответствующий каждой рисуемой отдельно линии ххх. Правая часть уравнения содержит те же вычисления суммарного веса для вертикальной линии (в = 90°) в букве "Iм. (Поскольку подобные вычисления требуются и для букв К, V, W, X, Y и Z, то было бы неплохо выделить их в отдельный макрос.) 18.9. beginchar("HM,13u#,/i*#, 0); xi = Х2 = хъ = Ъи; хз = Х4 = xq = w — xi; 2/1=Уз= h; у2 = 2/4 = 0;
Приложение А: Ответы ко всем упражнениям 261 top.senf(l, xx, —90, jtxt, jut); bot.serif(2, xx, 90, jut,jut) \ top.serif (3, xx, —90, jut, jut); bot.serif (4, xx, 90, jut, jut); filldraw bot.serif.edge 2 - - reverse top.serif.edge l - - cycle; fill bot.serif .edge 4 - - reverse top.serif.edge 3 - - cycle; У5 = t/6 = 52/i; draw гь -- zs\ penlabels(l, 2,3,4,5,6); endchar. 18.10. Подставляемый текст содержит десять лексем: |*jf] (t) 0 <e) f^ddif] 0 @ 0 0 (p) где (t), (e) и (p) стоят в местах вхождения параметров. Если разложение этого макроса происходит при tracingmacros > 0, система METAFONT напечатает строку fоо(TEXTO)<expr>of<primary>->def(TEXTO)=(EXPR1)enddef;def.t=(EXPR2) после которой будут стоять значения аргументов (TEXTO), (EXPR1) и (EXPR2). 18.11. Согласно только что приведенным правилам, первая запятая — это краткая форма записи последовательности '}} {{'. Следовательно, первый аргумент есть капсула, содержащая значение х; второй аргумент — это текст Ч,\ а третий — текст '(}})'. 18.12. Это позволяет "отлавливать" заготовки перьев до того, как они превратятся в готовые перья, чтобы команда pickup могла выполнить масштабирование по у на величину aspect.ratio до того, как эллипсы превратятся в многоугольники. 18.13. Конструкция 'hide ((список утверждений))' раскладывается в последовательность 'gobble begingroup (список утверждений); endgroup', поэтому должен быть вычислен аргумент макроса gobble. Считав лексему begingroup, система METAFONT начинает выполнять утверждения. После того, как это будет сделано, окончательное утверждение окажется (пустым утверждением), и, таким образом, аргумент макроса gobble превратится в вырожденное выражение (см. главу 25). Итак, подставляемый текст макроса gobble оказался пустым, так что текст действительно "растворился". (На самом деле макрос hide, определенный в приложении В, действует более эффективно, но и устроен он сложнее.) 19.1. Если переменная mag известна, то "желудок" системы METAFONT увидит ';', если же она не известна, то никаких изменений не произойдет. От лишних точек с запятой нет вреда, так как METAFONT допускает пустые утверждения. А привычка ставить ';' перед fi полезна, поскольку, во-первых, этим вы экономите время, а, во-вторых, ';' имеет прямое отношение к оператору endfor. 19.2. Нет, это было бы шокирующе. 19.3. Да, причем тогда и только тогда, когда число п — ^ — четное целое. (Поскольку неоднозначно определенные значения округляются.) 19.4. Нет. 19.5. def even = not odd enddef. 19.6. Значение первого выражения равно 5, так как пара не рассматривается как путь. Второе и третье равны 0, так как пара вынужденно превращается в путь. ГгЛ
262 Приложение А: Ответы ко всем упражнениям 19.7. (а) Текст данного цикла не выполняется никогда. (Ь) Выполняется только однажды, с х = 1. (с) Выполняется бесконечное число раз, с х = 1,1,1, (d) Поскольку число .1, умноженное на 10, во внутреннем представлении METAFONT будет немного превосходить 1, правильный ответ может показаться вам несколько неожиданным. Текст цикла выполняется со значениями х, равными 0, .1, 0.20001, 0.30002, 0.40002, 0.50003, 0.60004, 0.70004, 0.80005 и 0.90005. (Если вам нужно получить последовательность значений (0, .1, .2,..., 1), задайте цикл иначе: ' for хх = 0 upto 10: х := жж/10; (текст) endfor'.) 19.8. m = 1, n = 0. 19.9. def exitunless expr b = exitif not b enddef. (Более простой вариант 'def exitunless = exitif not enddef работать не будет, так как оператор 'not' действует* только на следующую непосредственно ним (первичную булеву формулу).) 19.10. numeric р[]; boolean n_is_prime; р[1]=2; k:=l; for n=3 step 2 until infinity: n_is_prime:=true; for j=2 upto k: if n mod p[j]=0: n_is_prime:=false; fi exitif n/p[j]<p[j]; endfor if n_is_prime: p[incr k]:=n; exitif k=30; fi endfor fi show for k=l upto 30: str p[k]&"="&decimal p[k], endfor "done" end. 19.11. (0; exitif true;'. 20.1. Нет, неверно; достаточно рассмотреть 'al(2)\ 20.2. Значение очень близкое к Z2- 20.3. vardef lo_cube(expr х)=х*х*х<10 enddef; show solve lo_cube(0,10), 10**1/3; end. При значении tolerance равном 0.1, которое устанавливается по умолчанию, мы получим значения, равные, соответственно, 2.14844 и 2.1544. Можно написать и более общую процедуру, где '10' будет фигурировать в качестве параметра: vardef lo_cube[](expr х)=х*х*х<в enddef; show solve lo_cubel0(0,10); Если мы установим минимальное возможное значение величины tolerance (tolerance := epsilon), то получим 2.15445; истинное значение « 2.15443469. 20.4. begingroup(p5dx,p5dy)endgroup. 20.5. Для этого достаточно указать 'f irst#®', дав предварительно определение 'vardef first.a[]e#=e enddef. (Существуют и другие решения, например, с использованием подцепочек или конструкции str #в, но такой способ решения является, вероятно, самым поучительным.) 20.6. Машина выдаст вам следующее: incr=macro:<suffix>-> begingroup(SUFFIX2):=(SUFFIX2)+1;(SUFFIX2)endgroup z«#=macro:->begingroup(x(SUFFIX2),у(SUFFIX2))endgroup Параметры макроса нумеруются последовательно, начиная с нулевого, и классифицируются как (EXPRn), (SUFFIXn) или (ТЕХТП). В переменных-макросах параметры (SUFFIX0) и (SUFFIX1) всегда резервируются под неявные параметры #в и ©; (SUFFIX2) будет равен в#, если последний используется в ("шапке" параметров), в противном случае (SUFFIX2) будет равен первому явному параметру, при условии, что он имеет тип suffix.
Приложение А: Ответы ко всем упражнениям 263 20.7. secondarydef t transum tt = begingroup save T; transform T; for z=origin,up,right: z transformed t + z transformed tt ■ z transformed T; endfor T endgroup enddef. 21.1. Нет, неправда; чаще, но примерно в два раза (в 2/3 случаев, против 1/3). 21.2. 1+floor uniformdeviate п. 21.3. Точку, случайным образом выбранную из отрезка прямой с концами в z\ и zi. (Вероятность выбора точки z\ равна 1/65536, а точка zi не может быть выбрана никогда.) 21.4. Случайные "очертания на фоне неба" шириной 100 pt и высотой 10 pt (с увеличением высоты плотность равномерно убывает): шШмИпЫЙ^Ц mtt 21.5. Гистограмму, по форме отдаленно напоминающую колокол: 22.1. (а) Тогда и только тогда, когда п есть целое число в промежутке от 0 до 255. (Ь) Тогда и только тогда, когда s есть цепочка длины 1. 22.2. Тот кто говорит, что такой примитивной операции не существует, должно быть, забыл о команде scant о kens. 22.3. vardef octal primary n = save m,s; m:=abs round n; string s; s=decimal(m mod 8); forever: m:=m div 8; exitif m=0; s:=decimal(m mod 8) k s; endfor s enddef; вместо 'decimal (m mod 8)' можно также использовать 'str[m mod 8]'. 23.1. Точка (x, у) — верхний левый угол, (х + с\ — со, у) — верхний правый угол, (х, у — П -fro) — нижний левый угол и (х + с\ — со, у — г\ + го) — нижний правый угол. (Пиксели, расположенные внутри этого прямоугольника, на экране не отображаются.) 23.2. Нужно переопределить макрос openit так, чтобы он помещал левый верхний угол окна в точку (—50, 280). 23.3. (Эта программа написана Джоном Хобби.) nevinternal n.windows; У, количество размещенных окон nevinternal screen.bot; '/• первая незатронутая строка экрана pair screen.corner; У, верхний левый угол следующего окна def vipescreen s % интиртализация~переинициализация for i:=l upto n.windows: display blankpicture invindov i; endfor n.windows := screen_bot :s 0; screen_corner := origin enddef; vipescreen; vardef new.windowfl*(expr u,v) = save r,c,up_lft; pair up.lft; if n_windows=15: errmessage "No more windows left" else: window€# := incr n.windows; up.lft = (min(xpart u,xpart v), max(уpart u, ypart v)); (r,c) = (u+v-2up_lft) rotated 90; if ypart screen_corner + с > screen_cols: screen.corner:=(screen_bot,0); fi openwindow windowQ# from screen.corner to screen_corner+(r,c) at up_lft; screen.bot := max(screen_bot,xpart screen.corner + r); screen.corner := screen.corner + (0,c) fi; enddef;
264 Приложение А: Ответы ко всем упражнениям 24.1. Все точки пути, за исключением (0,0), имели бы отрицательные координаты, поэтому контур заливаемой области имел бы вид (0,-1) -- (10,-1) -- (10,0) -- (0,0) -- (0,1) - - cycle. (Заметьте, что, прежде чем снова пойти строго вниз, оцифрованный контур поднимается в точку (0,1). Никакие пиксели при этом не заливаются, но благодаря этому система METAFONT включает "взаимно-уничтожающиеся" грани, ведущие из (0,0) в (0,1) и обратно из (0,1) в (0,0), в общую структуру границы, что совершенно верно, поскольку точка (0, .5) лежит на границе и округляется до (0,1).) 24.2. О горизонтальных касательных заботиться нечего, поскольку уравнения top t/i = Л + ои bot j/4 = —о проделывают всю необходимую работу. Однако, чтобы удачно расположить вертикальные касательные, нужно указать: х2 = w — хз = good.x(l.bu + s). Поскольку w — целое, а перо logo.pen симметрично относительно вертикальной оси, значение w — хз будет "хорошим" тогда и только тогда, когда таковым является значение хз. 24.3. Пусть 6 — ширина пера. Тогда .bw будет подходящим значением для х тогда и только тогда, когда Ift .bw есть целое число; но Ift .bw = .bw — .56, а это число будет целым тогда и только тогда, когда число w — Ь четно. 24.4. На контурах этой линии, за исключением, возможно, верхней и нижней границ, нет точек неоднозначности; причем наличие на них таких точек возможно, только если 'round ру1 — нечетное число. Поэтому симметрия относительно вертикальной оси будет иметь место всегда, а симметрия относительно горизонтальной оси может нарушаться из- за недостающей строки внизу (например, если рх = ру = 3). В случае буквы Т' будет иметь место и та, и другая симметрия, так как у\ и Х4 имеют удачные значения. 24.5. При любом таком положении восьмиугольника, когда он не задевает ни одну точку неоднозначности, внутри него находятся ровно семь точек неоднозначности. Поэтому команда draw с указанием одной точки заливает в точности семь пикселей. (На самом деле вы получаете один из следующих массивов пикселей «5§, gjjjg, ®* или ^.) 24.6. / = .5(х4 —хз); искомое у равнение имеет вид'х 1-х 2 = round(xi—Х2-К5(х4 —хз))'. 24.7. Пусть хз = п + | -f 0, где п — целое, и 0 < в < 1. Рисуя линии под углом 30° по отношению к центрам пикселей, мы находим, что возможны три случая для четырех крайних справа столбцов: Случай А: &*; Случай В: |Н; Случай С: ||*. Случай А соответствует значениям 0 < в < 2\/3 — 3; случай В соответствует значениям 2\/3 -3<0<\/3-1;и случай С соответствует значениям v/3-l < в < 1. В случае А угол выглядит слишком острым, а в случае С — туповатым. Лучше всего он выглядит в случае В, когда значение хз — почти целое. Поэтому лучше, чтобы хз было целым. 24.8. Пусть yi = п + в. Если в лежит между £\/3 — £ и £\/3 — £, то после оцифровки в верхней строке будет два черных пикселя. Если же в лежит между £ч/3-|и§ч/3-|, то мы получим угол нужной формы. В остальных случаях угол будет иметь вид ' |&Л 24.9. (Мы выбираем в = ^\/3 в предыдущем упражнении, поскольку это — среднее значение искомой величины.) Уравнения приобретают вид xi = Х2 = w — хз = rounds; 2/з = .5-|- floor .5/i; zi — Z2 = (гз - Z2) rotated 60; 2/1 := .5sqrt3 + round(yi — .5sqrt3); 2/2 :=Л-уи после чего мы, как и прежде, указываем: fill 21 -- 22 -- 23 -- cycle.
Приложение А: Ответы ко всем упражнениям 265 24.10. vardef vround primary v = floor(v * aspect-ratio + .b)/aspect-ratio enddef; vardef good.у primary у = vround (y + pen-top) — pen-top enddef. 24.11. (m -f- 1/2, (n -f- 1/2)/aspect-ratio). Это те точки, которые переводятся в центры пикселей преобразованием currenttransform. 25.1. Глядя на правила синтаксиса, мы можем написать, например, следующее: (булево выражение) true (числовое выражение) 0 (выражение-пара) (0,0) (выражение-путь) makepath pencircle (выражение-перо) nullpen (выражение-рисунок) nullpicture (выражение-цепочка) " " (выражение-преобразование) Невозможно! (вырожденное выражение) begingroup endgroup Любое (выражение-преобразование) содержит либо переменную, либо капсулу. Кстати, для (выражения-пары) существует интересное альтернативное решение, в котором используется всего пять лексем: postcontrol 0 of makepath nullpen makepath pencircle intersectiontimes makepath nullpen 26.1. В ответ мы получим > a=left delimiter that matches :: > b=(outer) a > c=a так как: лексема а, ранее бывшая внутренней величиной, была переопределена как разделитель; лексема 6 по-прежнему представляет собой внутреннюю величину (названную а), которой был присвоен статус запрещенной; лексема с представляет собой ту же самую внутреннюю величину, но не является запрещенной. 27.1. Для того чтобы исправить правую квадратную скобку, которая, как мы видели, ошибочна так же, как и левая, нужно удалить лексемы @ Ш 0 0 S § ш из последовательности, которую METAFONT собирается считать на следующем шаге. Однако возможно и другое решение (которое было бы более предпочтительным, если бы выражение в скобках было длиннее). Мы могли бы просто стереть 2 лексемы, затем набрать 'К'. После этого мы получили бы еще одно сообщение об ошибке: ! Missing ')' has been inserted. <to be read again> ] <*> show round[1 + sqrt43] » ? h I found no right delimiter to match a left one. So I've put one in, behind the scenes; this may fix the problem. ? (Это значит: "He найден правый разделитель, соответствующий левому; он вставлен по усмотрению системы. Может быть, это решит проблему.".) После этого достаточно стереть лексему ']', и работа успешно продолжится.
266 Приложение А: Ответы ко всем упражнениям 27.2. Система METAFONT заглянула чуть вперед, ожидая, что выражение, значение которого нужно вычислить, будет чем-то вроде 'round 0[l+sqrt43,x]\ Но, не обнаружив запятой, вернула несколько лексем, поэтому они могут быть снова считаны. (Значение подвыражения l+sqrt43 уже было вычислено и помещено в капсулу. Поэтому готовая капсула, содержащая значение, равное 7.55743, была вставлена между лексемами, подлежащими повторному считыванию.) Выражение заканчивалось лексемой '0', поэтому на экран было выведено значение 'round 0'. Затем система METAFONT обнаружила лишние лексемы, следующие за командой show; на этом месте должна была стоять точка с запятой. Для того чтобы продолжить, пользователю нужно без колебаний нажать клавишу "ввод" еще раз, позволив системе METAFONT стереть эти нежелательные лексемы. 27.3. Коротенькая программа вида path p,q; p=flex((-32,481),(-42,455),(-62,430)); q=flex((-62,430),(-20,452),(42,448)); show p intersectiontimes q, p intersectionpoint q, angle -direction 2 of p, angle direction 0 of q; end дает следующие результаты: » (1.88403,0.07692) » (-59.32149,432.59523) » 43.14589 » 45.47263 (На самом деле пути должны пересекаться и в случае, когда вместо 452 стоит число 451, но в этом случае METAFONT не назовет путь "необычным". Когда угол поворота близок к 180°, система METAFONT предпочитает совершать его против часовой стрелки, даже если поворот получается больше.) 27.4. Если это упражнение — шутка, то название этого приложения — надувательство. (Выполнив это упражнение, вы можете сравнить все шутки и/или ложные утверждения в этой книге и в книге Все про TeX и найти одинаковые.)
Приложение А: Ответы ко всем упраэюнениям 267 Ты тщательно в сем деле разберись И призови злодеев мерзких к ответу. — ВИЛЬЯМ ШЕКСПИР, Генрих IV, второе отделение (1594) Если вам не удается решить проблему, вы всегда можете заглянуть в ответ. Но вначале все же попытайтесь решить ее самостоятельно; это, во-первых, ускорит ваше обучение, а, во-вторых, так вы и узнаете больше. — ДОНАЛЬД Э. КНУТ, Все про METAFONT (1986)
в Базовые определения
Приложение В: Базовые определения 269 В этом приложении определяются макросы базового формата METAFONT (plain METAFONT). Для начала давайте произведем инвентаризацию всех средств, которыми мы располагаем. known ^ Объекты булева типа: true, false; I unknown > (выражение); > (выражение); < I cycle J odd (числовое); charexists (числовое); ( boolean ^ numeric pair path pen picture string ktransformJ not (булево); (булево) and (булево); (булево) or (булево). ■ Объекты числового типа: tracingtitles, ..., yoffset (см. главу 25); eps, epsilon, infinity; tolerance, join_radius, displaying; (константа); ( 1ft ^ (булево) ^ (числовое) (пара) (цепочка) , (преобразование) > > < f < у <= о ► < г (булево) ' (числовое) (пара) (цепочка) . (преобразование). sqrt> sind cosd mlog mexp, > (числовое); / floor > round < hround 1 vround I ceiling, i } (числовое); < rt top bot Igood.x (good.уJ > (числовое); rxpart j Г (пара) 1 . Iypart J |(преобразование)J ' fxxpart\ xypart Iyxpart ,yypart normaldeviate; unif ormdeviate (числовое); whatever; angle (пара); turningnumber (циклический путь); totalweight (рисунок); (ASCIП > (преобразование); < oct > (цепочка); I hex J , г finer\ , \ u 4. /(числовое)! (числовое); < , > (переменная); byte < \ / > ; (константа) J I deer J x * \ (цепочка) J (числовое) < [ (числовое); (числовое) < > (числовое); (числовое) < / > (числовое); (числовое) \ . > (числовое); UJ UivJ . . ((числовое)* (пара) dotprod (пара); {^} ((числовые)); {le^gSth} | |^| I (цепочка) ) (числовое) [ (числовое), (числовое)]; solve (функция) ((числовое), (числовое)); directiontime (пара) of (путь). ■ Пары: left, right, up, down, origin; г(суффикс); ((числовое), (числовое)); dir (числовое); unit vector (пара); round (пара);
270 Приложение В: Базовые определения > (пара); I lft rt top bot J {good, lft good.rt good.top good.botJ point precontrol postcontrol x direction ) > (числовое) of (путь); < " > (пара); (пара) l [(пара); (числовое) [(пара), (пара)]; [(константа)J ^ ~ J (числовое) * (пара); (пара) < . > (числовое); (пара) (преобразователь); , ч Г intersect ionpointl , v fmax4} . чч (ПУТЬ) {intersection's/ <ПуТЬ>; Uinl «паРЫ>); penoff set (пара) of (перо); directionpoint (пара) of (путь). ■ Пути: quartercircle, halfcircle, fullcircle; unitsquare; f lex ((пары)); makepath (перо); super ell ipse ((пара), (пара), (пара), (пара), (числовое)); reverse (путь); counterclockwise (путь); tensepath (путь); (путь)(преобразователь); interpath((числовое),(путь),(путь)); Г (пара) 1 I \ (путь) J | {(пара)} ' {(загиб)} (пустое) .. (натяжение).. . (контрольные точки).. г {(пара)} ' {(загиб)} (пустое) ' (пара) Л (путь) > ; kcycle J к softjoin subpath (пара) of (путь). ■ Перья: pencircle, pensquare, penrazor, penspeck; nullpen; currentpen; makepen (путь); (перо)(преобразователь). ■ Рисунки: nullpicture, blankpicture; unitpixel; currentpicture; < > (рисунок); (рисунок) < > (рисунок); (рисунок)(преобразователь). ■ Цепочки: "constant"; ditto; jobname; readstring; str (суффикс); decimal (числовое); char (числовое); (цепочка) & (цепочка); < > ((цепочки)); substring (пара) of (цепочка). ■ Трансформации: identity; current transform; inverse (преобразование); (преобразование)(преобразователь). ■ Преобразователи: transformed (преобразование); ( scaled N \ \ (числовое); < xscaled > (числовое); \ \ (пара); IslantedP lyscaled/ IzscaledP reflectedabout ((пара), (пара)); г ot at edaround( (пара), (числовое)). ■ Условия: if (булево): (текст) {elseif (булево): (текст)}-°< . ' * . ' > f i.
Приложение В: Базовые определения 271 for v (числовое) upto (числовое) (числовое) downto (числовое) (числовое) step (числовое) until (числовое) ) i Циклы: forever: (текст) endf or; for € \ ,_\ (выражения): (текст (б)) endf or; f orsuf f ixes a < # = > (суффиксы): (text(cr)) endf or; exitif (булево); ; exitunless (булево); . ■ Диагностика: ???; interact; hide ((утверждения)); loggingall, tracingall, tracingnone. (text (г/)) endf or; Начало работы: \mode=(имя режима); mag: -{. (числовое) [magstep(4HcnoBoe) screenchars; screenstrokes; imagerules; gfcorners; nodisplays; notransforms; input (имя файла). }■ > ((имена)). ■ Перевод в пиксельные единицы: mode.setup; fix_units; pixels.per.inch, blacker, f illin, o.correction; mm#, cm#, pt#, pc#, dd#, cc#, bp#, in#; mm, cm, pt, pc, dd, cc, bp, in; mode.def; extra.setup; define.pixels define.whole.pixels define.whole.vertical.pixels def ine.good.x.pixels def ine.good.y.pixels def ine.blacker.pixels def ine.whole.blacker„pixels def ine.whole.vert ical.blacker.pixels def ine.corrected.pixels define.horizontal.corrected.pixels , ■ Символы и атрибуты шрифта: beginchar ((код), (ширина^), (высота^), (глубина^)); extra.beginchar; italcorr (числовое^); change.width; endchar; extra.endchar; font.size font.slant font.normal.space font.normal.stretch font.normal.shrink font.x.height font.quad font.extra.space (ligt able (лигатуры/кернинг) спагНэ^коды) extensible (коды) f ont d imen (информация) I headerbytes (информация) [(пустое)J (числовое^); {f ont _ ident if ier font.coding.scheme (пустое) (цепочка).
272 Приложение В: Базовые определения pickup Рисование: penpos(суффикс) ((длина), (угол)); penstroke (путь(е)); Г (перо) .Ь (номер пера): =savepen; clear_pen_memory; (номер сохраненного пера) pen_lft, pen.rt, pen_top, pen_bot; fill unfill filldraw lunfilldrawJ erase (команда, применяемая к рисунку); cutoff ((пара), (угол)); addto (переменная-рисунок) also (рисунок); \ (циклический путь); < undraw > (путь); < \ (пара); IcutdrawJ , , Г contour (цикл, путь)! Г withpen(nepo) 1 -° addto (переменная-рисунок) < , ,_, ' , , ч М .^ . \_^/ \ г ; 4 * J ' \ doublepath (путь) J ^withweight (числовое) J fwithweight (числовое)] „_, . . Г keeping 1 . . fwithweight (числовое)! cull (переменная-рисунок) ^^^^ (пара) | ^^ ' j . ■ Вывод на экран: currentwindow; screen_rows, screen.cols; openwindow (числовое) from (пара экран, коорд.) to (пара экран, коорд.) at (пара); display (переменная-рисунок) inwindow (числовое). Утверэюдения: (пустое); (цепочка); begingroup (утверждения) endgroup; > (имена); ( (булево) ^ (числовое) (пара) 1 (путь) | (перо) (рисунок) (цепочка) 1 (преобр-ние) > > < г {=■-} • . f (булево) ' (числовое) (пара) (путь) (перо) (рисунок) (цепочка) ^ (преобр-ние) Л ч > ' J >1 > ; < r boolean ^ numeric pair path pen picture string с transform j save (имена); interim (внутренняя величина) := (числовое); let (имя) < _| (имя); I \ (имя) (параметры) \ ,\ (текст) enddef; ( primarydef ^ \ secondarydef \ а (имя) ft I \ (текст(а,/?)) enddef; I tertiarydef J showit; shipit; cullit; openit; clearit; clearxy; clearpen; f message ^ stop (цепочка); show (выражения); < errmessage \ (цепочка); \ errhelp J fshowvariablel , K fshowdependenciesl { ) (имена); { r >; I showtoken J I showstats J см. также главу 26, где описаны некоторые более экзотические команды.
Приложение В: Базовые определения 273 ( labels 1 |penlabelsj | makelabel< Информация на пробных оттисках: top lft rt bot L(пустое)J top lft rt bot l(пустое) J {p > ((пара), (пара)); makegrid((пары)) ((пары)); S Cicclli 1116' proofrulethickness (числовое); proof of f set (пара). | {(пустое)} (Аффиксы)); {(пус^е)} «Цепочка),(пара)); titlefont^ labelfont grayfont IslantfontJ > (имя); Специальные средства: gobble, gobbled, killtext; capsule_def; numtok. f Оставшаяся часть приложения содержит отредактированную и снабженную комментариями версию базового форматного файла, который представляет собой набор макросов, предназначенных для использования в стандартных реализациях системы. Эти макросы выполняют три основные функции. (1) Они облегчают работу с МETA- FONT, так как примитивные команды системы METAFONT аппелируют к операциям очень низкого уровня. "Девственная" система METAFONT, не оснащенная макросами, подобна ребенку, которому еще многое нужно узнать об окружающем мире, но который очень быстро все схватывает. (2) Макросы, содержащиеся в базовом форматном файле, — это фундамент, на основе которого можно разрабатывать более сложные форматы с богатым инструментарием, приспособленные для индивидуальных нужд и предназначенные для выполнения конкретных задач. Вазовый формат предоставляет множество возможностей, но и такого количества вам скоро покажется мало. (3) На примере макросов базового формата иллюстрируется общий принцип построения форматных файлов. В памяти вашей машины должен находиться файл plain.mf, содержимое которого автоматически загружается в начале каждого сеанса работы той версии системы METAFONT, которую вы используете. Содержимое этого файла наверняка совпадает с тем, что описано ниже, за исключением некоторых элементов, которые записаны в эквивалентном, но несколько оптимизированном виде. Если здесь нам встретится макрос, смысл которого нигде на протяжении глав с 1 по 27 не объяснялся, мы будем рассматривать его с точки зрения пользователя. Но большинство комментариев в этом приложении адресовано потенциальному разработчику форматного файла. Для инсталляции системы METAFONT используется специальная программа INIMF. Эта программа в целом подобна METAFONT, но обладает дополнительной способностью "демпировать" (операция 'dump') форматный файл, подходящий для предзагрузки. Эта операция требует дополнительной оперативной памяти, поэтому программа INIMF располагает меньшим объемом доступной памяти, чем полноценная "рабочая" версия METAFONT. 1. Начало работы. В начале базового форматного файла plain.mf имеется команда delimiters, определяющая лексемы-разделители, поскольку программа INIMF не имеет встроенных разделителей. Кроме того, как показано ниже, в первых строках форматного файла указывается имя файла и номер версии.
274 Приложение В: Базовые определения У, This is the plain METAFONT base that's described in The METAFONTbook. У, N.B.: Please change "base_version" whenever this file is modified! У, And don't modify the file under any circumstances. string base_name, base_version; base_name="plain"; base_version="2.71"; message "Preloading the plain base, version " ft base.version; delimiters () ; '/, this makes parentheses behave like parentheses Затем мы "очеловечиваем" язык METAFONT, вводя в него понятные синонимы наиболее часто встречающихся конструкций. Например, команда 'stop "hello"' выводит на экран 'hello' и останавливает работу до тех пор, пока не будет нажата клавиша (return). def upto = step 1 until enddef; def downto = step -1 until enddef; def exitunless expr с = exitif not с enddef; let relax = \; У, ignore the word 'relax', as in TeX let \\ = \; У, double relaxation is like single def ]] = ] ] enddef; У, right brackets should be loners def — = {curl l}..{curl 1} enddef; def = .. tension infinity .. enddef; def ... = .. tension atleast 1 .. enddef; def gobble primary g = enddef; def killtext text t = enddef; primarydef g gobbled gg = enddef; def hide(text t) = exitif numeric begingroup t; endgroup; enddef; def ??? = hide(interim showstopping:=l; showdependencies) enddef; def stop expr s = message s; gobble readstring enddef; (В главе 20 указывалось, что Л' является разложимой лексемой, разложение которой пусто. В базовом формате 'W определяется как синоним Л', так как существует специальная программа MFT, позволяющая получать элегантные распечатки программ для METAFONT, в которой \\ используется для вставки дополнительных пробелов.) В основе "умного" определения макроса hide лежит тот факт, что вырожденное выражение не является числовым; поэтому команда не влечет выхода из цикла, и компьютер даже не задумывается о том, находится ли он вообще внутри какого-либо цикла. Далее мы по порядку задаем значения внутренних величин: smoothing: =1; autorounding:=2; У, this adjusts curves to the raster turningcheck:=2; У, this will warn about a "strange path" granularity : = 1; У, this says that pixels are pixels def interact = '/, prepares to make "show" commands stop hide(showstopping:=l; tracingonline:=1) enddef; def loggingall = '/, puts tracing info into the log tracingcommands:=3; tracingedges:=2; tracingtitles:=l; tracingequations:=l; tracingcapsules:=l; tracingspecs:=l; tracingpens:=l; tracingchoices:=l; tracingstats:=l; tracingoutput:=l; tracingmacros:=l; tracingrestores:=l; enddef; def tracingall = У, turns on every form of tracing tracingonline:=1; showstopping:=l; loggingall enddef; def tracingnone = У, turns off every form of tracing tracingcommands:=0; tracingonline:=0; showstopping:=0; tracingedges:=0; tracingtitles:=0; tracingequations:=0; tracingcapsules:=0; tracingspecs:=0; tracingpens:=0; tracingchoices:=0; tracingstats:=0; tracingoutput:=0; tracingmacros:=0; tracingrestores:=0; enddef;
Приложение В: Базовые определения 275 Пользователь может использовать команду interact внутри утверждения, но команды loggingall, tracingall и tracingnone должны стоять между утверждениями. (После них не обязательно ставить ';', так как точка с запятой в них уже встроена.) 2. Математические подпрограммы. Вторая главная часть файла plain.mf содержит определения основных констант и математических макрокоманд, расширяющих запас выражений языка METAFONT, который изначально состоит только из примитивных выражений. У, numeric constants nevinternal eps,epsilon,infinity; eps := .00049; 7, this is a pretty small positive number epsilon := 1/256/256; '/, but this is the smallest infinity := 4095.99998; 7. and this is the largest У, pair constants pair right,left,up,down,origin; origin=(0,0); up=-dovn=(0,1); right=-left=(1,0); 7* path constants path quartercircle,halfcircle,fullcircle,unitsquare; quartercircle=(right{up}..(right+up)/sqrt2..up{left}) scaled .5; halfcircle=quartercircle & quartercircle rotated 90; fullcircle=halfcircle & halfcircle rotated 180 ft cycle; unitsquare=(0,0)--(l,0) —(1,1) —(0,1)—cycle; У. transform constants transform identity; for z=origin,right,up: z transformed identity = z; endfor У, picture constants picture blankpicture,unitpixel; blankpicture=nullpicture; У, 'display blankpicture...' unitpixel=nullpicture; addto unitpixel contour unitsquare; У, string constants string ditto; ditto = char 34; У, ASCII double-quote mark У, pen constants def capsule.def(suffix s) primary u = def s = u enddef enddef; capsule.def(pensquare) makepen(unitsquare shifted -(.5,.5)); capsule.def(penrazor) makepen((-.5,0)—(.5,0)—cycle); pen penspeck; penspeсk=pensquare scaled eps; Константы pensquare и penrazor определяются здесь довольно неожиданным образом, при помощи трансформации roundabout так, как если бы они были не перьями, а заготовками перьев. Трансформация заготовки пера выполняется системой METAFONT гораздо быстрее, чем трансформация готового пера, так как перья имеют сложную внутреннюю структуру данных. Таким образом, этот трюк позволяет сэкономить время. Но, как же это срабатывает? Дело в том, что переменная не может быть заготовкой пера, а капсула — может. Поэтому pensquare и penrazor определяются посредством команд capsule.def, как макросы, разложения которых состоят из одной капсулы. Кстати, константа penspeck представляет собой крошечное перо, которое затем используется в макросе drawdot. Поскольку мы не собираемся его больше трансформировать, его лучше определить как готовое перо, которое можно будет сразу использовать. Теперь, когда мы определили основные константы, можно перейти к математическим операциям. Существует одна операция, не зависящая от аргументов: У, miliary operators vardef whatever ■ save ?; ? enddef;
276 Приложение В: Базовые определения Смысл этой операции обсуждается в упражнении 17.2. Далее определяются унарные операторы. У, unary operators let abs = length; vardef round primary u = if numeric u: floor(u+.5) elseif pair u: (hround xpart u, vround ypart u) else: u fi enddef; vardef hround primary x = floor(x+.5) enddef; vardef vround primary у = floor(y.o_+.5)_o_ enddef; vardef ceiling primary x = -floor(-x) enddef; vardef byte primary s = if string s: ASCII fi s enddef; vardef dir primary d = right rotated d enddef; vardef unitvector primary z = z/abs z enddef; vardef inverse primary T = transform T_; T_ transformed T = identity; T. enddef; vardef counterclockwise primary с = if turningcheck>0: interimautorounding:=0; if turningnumber с <= 0: reverse fi fi с enddef; vardef tensepath expr r = for k=0 upto length r - 1: point к of r~ endfor if cycle r: cycle else: point infinity of r fi enddef; Заметьте, что функция inverse не сохраняет значение переменной 'Т_'. Эффективность подпрограмм базового форматного файла повышается за счет использования в них "зарезервированных" лексем, отличающихся от лексем, используемых пользователем. Зарезервированные лексемы имеют на конце символ подчеркивания '_'. Если обычная программа пользователя не содержит таких лексем, никаких неожиданностей не произойдет, при условии, что используемые им макросы были определены достаточно аккуратно, так чтобы избежать возможных конфликтов между зарезервированными лексемами. Как мы вскоре увидим, зарезервированные лексемы 'о_' и '_о_', используемые в определении vround, обозначают '*aspect_ratio' и 7aspect_ratio', соответственно. Далее мы определяем операции 'mod' и 'div', следя за тем, чтобы при этом выполнялись тождества а(х mod у) = (ах) mod (ay) и (ах) div (ay) = х div у. 7, binary operators primarydef x mod у = (x-y*floor(x/y)) enddef; primarydef x div у « floor(x/y) enddef; primarydef w dotprod z = (xpart w * xpart z + ypart w * ypart z) enddef; Оператор '**' эффективно выполняет возведение в квадрат. Отдельная подпрограмма 'takepower' используется для возведения в степень, отличную от 2, так что в общем случае системе METAFONT не приходится перескакивать через множество лексем. Подпрограмма takepower дает правильный результат и в случае таких выражений, как '(-2)**3' и'0**0'. primarydef х ** у = if у=2: х*х else: takepower у of х fi enddef; def takepower expr у of x = if x>0: mexp(y*mlog x) elseif (x=0) and (y>0): 0 else: 1 if y=floor y:
Приложение В: Базовые определения 277 if у>=0: for n=l upto у: *х endfor else: for n=-l downto у: /x endfor fi else: hide(errmessage "Undefined power: " ft decimal xft"**"ftdecimal y) fi fi enddef; Примитивные операции с путями определены в METAFONT так, что задать следующие операции высокого уровня не составляет труда: vardef direction expr t of p = postcontrol t of p - precontrol t of p enddef; vardef directionpoint expr z of p = a_:=directiontime z of p; if a_<0: errmessageC'The direction doesn't occur"); fi point a. of p enddef; secondarydef p intersectionpoint q = begingroup save x_,y_; (x_,y_)=p intersectiontimes q; if x_<0: errmessageC'The paths don't intersect"); (0,0) else: .5[point x_ of p, point y_ of q] fi endgroup enddef; Зарезервированная лексема 'a_' объявляется как внутренняя величина. Внутренние величины более эффективны в использовании, чем обычные числовые переменные. Определенная в базовом формате METAFONT операция плавного соединения путей 'softjoin' позволяет сцеплять пути, избегая резкой перемены направления в точке сцепления, как в случае операции 'ft'. В предположении, что последняя точка пути р является первой точкой пути д, путь 'р softjoin </' начинается путем р, дойдя до общей точки, огибает ее по кривой с кривизной, определяемой величиной join-radius:, затем путь продолжается путем q. Значение внутренней величины joinjradius должно быть установлено до применения операции 'softjoin'. (Эта подпрограмма разработана Н.Н.Биллавалой). tertiarydef р softjoin q = begingroup c_:=fullcircle scaled 2join_radius shifted point 0 of q; a_:=ypart(c_ intersectiontimes p); b_:=ypart(c_ intersectiontimes q); if a_<0:point 0 of p{direction 0 of p} else: subpath(0,a_) of p fi ... if b_<0:{direction infinity of q}point infinity of q else: subpath(b_,infinity) of q fi endgroup enddef; nevinternal join_radius,a_,b_; path c_; Остальные математические операторы не укладываются в обычные рамки, в каждой из них есть что-то необычное. Во-первых, имеются операции 'incr' и 'deer', применимые только к переменным; они имеют побочный эффект — меняют значения переменных. vardef incr suffix $ = $:=$+1; $ enddef; vardef deer suffix $ = $:=$-l; $ enddef; Внутри выражения можно писать как 'incr х', так и 'incr (х)'; но само по себе 'incr х' не является правильным выражением. Для того чтобы выполнить отражение относительно прямой, мы "на лету" находим соответствующее преобразование: def reflectedabout(expr v,z) = 7, reflects about the line v..z transformed begingroup transform T_; v transformed T_ « w; z transformed T_ = z; xxpart T. * -yypart T_; xypart T_ = yxpart T_; 7, T_ is a reflection T_ endgroup enddef;
278 Приложение В: Базовые определения def rotatedaround(expr z, d) = '/, rotates d degrees around z shifted -z rotated d shifted z enddef; let rotatedabout = rotatedaround; У, for roundabout people Сейчас мы научим вас интересному трюку. Если пользователь пишет что-то вроде 'min(a,6)' или 'тах(а,6, с, d)\ то система METAFONT, следуя правилам разложения макросов, сама отделяет первый аргумент от остальных (в предположении, что имеется не менее двух аргументов). vardef max(expr u) (text t) = 7, t is a list of numerics, pairs, or strings save u_; setu_ u; for uu = t: if uu>u_: u_:=uu; fi endfor u_ enddef; vardef min(expr u) (text t) = '/, t is a list of numerics, pairs, or strings save u_; setu_ u; for uu = t: if uu<u_: u_:=uu; fi endfor u_ enddef; def setu_ primary u = if pair u: pair u_ elseif string u: string u_ fi; u_=u enddef; В приложении D рассматриваются некоторые вариации на эту тему. Макрос flex определяет участок пути, причем направления в крайних точках будут зависеть от направлений в смежных точках, если только флекс не заключен в скобки. def flex (text t) = У, t is a list of pairs hide(n_:=0; for z=t: z_[incr n_]:=z; endfor dz_:=z_[n_]-z_l) z_l for k=2 upto n_-l: ...z_[k]{dz_} endfor ...z_[n_] enddef; newinternal n_; pair z_[],dz_; Макрос 'superellipse' зависит от пяти аргументов, определяющих показатель де- формированности и положение крайних точек (правой, левой, нижней и верхней). def superellipse(expr r,t,l,b,s)= r{up}...(s[xpart t,xpart r],s[ypart r.ypart t]){t-r}... t{left}...(s[xpart t,xpart l],s[ypart l,ypart t]){l-t}... Hdown}... (s[xpart b,xpart l],s[ypart l,ypart b]){b-l}... b{right}...(sCxpart b,xpart r],s[ypart r,ypart b]){r-b}...cycle enddef; В главе 14 иллюстрируется применение подпрограммы 'interpath', которая производит интерполяцию и находит путь, занимающий промежуточное положение между другими путями. Этот промежуточный путь можно было бы представлять как 'а\р} q]\ если бы обозначения системы METAFONT имели большую степень общности. vardef interpath(expr a,p,q) = for t=0 upto length p-1: a[point t of p, point t of q] ..controls a[postcontrol t of p, postcontrol t of q] and a[precontrol t+1 of p, precontrol t+1 of q] .. endfor if cycle p: cycle else: a[point infinity of p, point infinity of q] fi enddef; Наконец, мы добрались до макроса solve, который представили вам в главе 20. В приложении D содержатся дополнительные примеры его применения. vardef solveQ#(expr true_x,false_x)= У, <D#(true_x)=true, fl#(false_x)=false tx_:=true_x; fx_:=false_x; forever: x_:=.5[tx_,fx_]; exitif abs(tx_-fx_)<=tolerance; if <D#(x_): tx_ else: fx_ fi :=x_; endfor
Приложение В: Базовые определения 279 х. enddef; '/, now х_ is пеги: where <D# changes from true to false newinternal tolerance, tx_,fx_,x_; tolerance:=.1; S. Перевод в пиксельные единицы. Следующий из основных подразделов файла plain.mf содержит макросы и константы, помогающие переводить размеры из "точных" или "настоящих" единиц в пиксельные единицы, соответствующие тому или иному устройству. Сначала вводится подпрограмма, которая вычисляет значения восьми основных единиц измерения, в предположении, что известна величина pixels-perAnch. def fix_units = У, define the conversion factors, given pixels_per_inch mm: s5pixels_per_inch/25.4; pt:=pixels_per_inch/72.27; dd:=1238/1157pt; bp:=pixels_per_inch/72; hppp:=pt; vppp:=aspect_ratio*hppp; enddef; cm:=pixels_per_inch/2.54; pc:=pixels_per_inch/6.0225; cc:=12dd; in:=pixels_per_inch; 7, horizontal pixels per point У, vertical pixels per point На самом деле точные единицы длины выражаются в пунктах, но не стоит ожидать, что пользователь будет использовать этот факт в своих программах. mm#=2.84528; cm#=28.45276; pt#=l; рс#=12; dd#=1.07001; сс#=12.84010; bp#=1.00375; in#=72.27; Как говорилось в главе 11, мы предполагаем, что для описания особенностей конкретного устройства вполне достаточно четырех параметров: pixels-per.inch, blacker, o.correction и fillin. Подходящие значения этим параметрам присваиваются командой mode.setup. newinternal pixels_per.inch; У, the given resolution newinternal blacker, o_correction; '/, device-oriented corrections (Четвертый параметр — fillin — уже является внутренней величиной системы METAFONT.) Вот десять основных способов перевода точных единиц в пиксельные: def define.pixels(text t) = forsuffixes $=t: $:=$.#*hppp; endfor enddef; def define_whole.pixels(text t) = forsuffixes $=t: $:=hround($.#*hppp); endfor enddef; def define„whole_vertical_pixels(text t) = forsuffixes $=t: $:=vround($.#*hppp); endfor enddef; def define„good_x_pixels(text t) = forsuffixes $=t: $:=good.x($.#*hppp); endfor enddef; def define.good.y.pixels(text t) = forsuffixes $=t: $:=good.y($.#*hppp); endfor enddef; def define_blacker_pixels(text t) = forsuffixes $=t: $:=$.#*hppp+blacker; endfor enddef; def define_whole„blacker_pixels(text t) = forsuffixes $=t: $:=hround($.#*hppp+blacker); if $<=0: $:=1; fi endfor enddef; def define_whole_vertical_blacker.pixels(text t) = forsuffixes $=t: $:=vround($.#*hppp+blacker); if $<=0: $:=l_o_; fi endfor enddef; def define_corrected_pixels(text t) = forsuffixes $=t: $:=vround($.#*hppp*o_correction)+eps; endfor enddef; def define_horizontal_corrected_pixels(text t) = forsuffixes $=t: $:=hround($.#*hppp*o_correction)+eps; endfor enddef;
280 Пргыожение В: Базовые определения В главе 24 мы обсуждали подпрограмму lower-fix, помогающую исправить аномалии, которые могут возникнуть при округлении точных величин до целого числа пикселей. def lowres_fix(text t) expr ratio = begingroup save min,max,first; forsuffixes $=t: if unknown min: min=max=first=$; min#=max#=$.#; elseif $.#<min#: min:=$; min#:=$.#; elseif $.#>max#: max:=$; max#:=$.#; fi endfor if max/min>ratio*max#/min#: forsuffixes $=t: $:=first; endfor fi endgroup enddef; 4- Режимы работы. Стандартный вариант начала работы по созданию программы при помощи базового формата METAFONT выглядит следующим образом. Вы вводите \mode=(имя режима); mag= (увеличение); input (имя входного файла) в ответ на выданное системой приглашение к работе '**'. Если увеличение должно быть равным 1, то команду mag можно опустить; если mode=proof, то можно опустить команду mode. Дополнительные команды, которые можно давать до команды 'input', такие как 'screenchars', мы обсудим чуть позже. Если кроме базового форматного файла вы используете еще какой-либо, например форматный файл 'super', то в самом начале этой строки должно стоять 'ftsuper'. Режим работы должен быть предварительно объявлен в форматном файле при помощи подпрограммы mode.def, приведенной ниже. Однако, если понадобится использовать особый режим, не объявленный в форматном файле, вы можете поместить соответствующие команды в отдельный файл (например, 'specmode.mf'), и вызвать этот режим, указав в командной строке \smode="specmode"; mag=•• • Ниже приводится подпрограмма mode_setup, к которой программы для МETA- FONT обращаются в числе первых. def mode_setup = varningcheck:=0; if unknown mode: mode=proof; fi numeric aspect_ratio; transform currenttransform; scantokens if string mode:("input "ftmode) else: mode.name[mode] fi; if unknown mag: mag=l; fi if unknown aspect_ratio: aspect_ratio=l; fi displaying:=proofing; pixels_per_inch:=pixels_per_inch*mag; if aspect_ratio=l: let o_=\; let _o_=\ else: def o_=*aspect_ratio enddef; def _o_=/aspect_ratio enddef fi; fix_units; scantokens extra_setup; '/, the user's special last-minute adjustments currenttransform:= if unknown currenttransform: identity else: currenttransform fi yscaled aspect_ratio; clearit; pickup pencircle scaled (.4pt+blacker); varningcheck:=1; enddef; def smode = string mode; mode enddef; string extra.setup, mode_name[]; extra_setup=n"; '/, usually there's nothing special to do newinternal displaying; '/, if positive, endchar will 'showit'
Приложение В: Базовые определения 281 Первая команда 'scantokens' в подпрограмме mode_setup либо считывает файл, либо обращается к макросу, который раскладывается в последовательность команд, определяющих тот или иной режим работы. Отметим, что при выполнении этих команд значение величины aspecLratio становится неопределенным; установив значения mode и mag, вы не можете присвоить этой величине произвольное значение. Если подпрограмма, определяющая режим работы, не присваивает величине aspect-ratio определенное значение, то оно принимается равным единице, и в последующих вычислениях величины 'о.' и '.о.' будут просто опускаться. Команды, определяющие режим, могут влиять на величину тор, поскольку после того, как подпрограмма установки режима выполнена, эта величина уже не проверяется. Кроме того, может быть присвоено определенное значение величине currenttransform. На время этих вычислений система METfi FONT временно прекращает выдачу предупреждений о слишком больших значениях, контролируемую величиной warningcheck, поскольку разрешение может превышать 4096 пикселей на дюйм. По завершении работы подпрограммы mode_setup значение currenpicture будет нулевым, значение величины currenttransform будет выбираться с учетом значения aspect-ratio, a currentpen будет равным круглому перу стандартной толщины, устанавливаемой по умолчанию, равной 0.4 pt. (Для того чтобы использовать это перо, его нужно сохранить, так как команда beginchar начисто сотрет установленное значение пера.) В базовом формате ГЩХ (plain TeX) увеличение описывается в терминах "шагов увеличения", где m-тый шаг увеличения равен 1.2Ш. Геометрическая прогрессия шагов увеличения удобна тем, что последовательное увеличение на m-тый и n-ный шаги эквивалентно увеличению на m -f п-ный шаг. vardef magstep primary m = mexp(46.67432m) enddef; После того как установлен режим работы (например, 'proof'), генерируется числовая переменная, соответствующая названию режима, и этой переменной присваивается уникальное значение (например, 1). Затем к цепочке, выражающей название режима, добавляется символ нижнего подчеркивания (например, 'proof..'). Массив mode-name используется для перевода номеров режимов в их названия (например, mode-name i = "proof."). def mode.def suffix $ = $:=incr number_of_modes; mode_name[$]:=str$ ft "_"; expandafter quote def scantokens mode_name[$] enddef; nevinternal number.of.modes; (Стратегия использования процедуры mode_def предложена Брюсом Лебаном.) Далее мы определяем три основных режима работы, первые два из которых предназначены для подготовки пробных оттисков. У, proof mode: for initial design of characters mode.def proof = proofing:=2; % yes, we're making full proofs fontmaking:=0; '/, no, we're not making a font tracingtitles:=l; '/. yes, show titles online pixels_per_inch:=2601.72; '/, that's 36 pixels per pt blacker:=0; '/, no additional blackness fillin:=0; '/, no compensation for fillin o.correction:=l; '/, no reduction in overshoot enddef; '/, smoke mode: for label-free proofs to mount on the wall mode.def smoke = proof.; '/, same as proof mode, except:
282 Приложение В: Базовые определения proofing:=1; 7. yes, we're making unlabeled proofs extra_setup:=extra_setup&"grayfont black"; 7, with solid black pixels let makebox=maketicks; 7, make the boxes less obtrusive enddef; Заметьте, что в определении режима smoke множество мелких деталей задается не прямо, а посредством вызова макроса 'proof _', который предварительно был определен подпрограммой mode-def. Далее определяется типичный режим, предназначенный для создания шрифта. 7. lovres mode: for certain devices that print 200 pixels per inch mode_def lowres = proofing:=0; 7, no, we're not making proofs fontmaking:=l; % yes, we are making a font tracingtitles:=0; % no, don't show titles at all pixels_per_inch:=200; 7. that's pretty low resolution blacker: = .65; 7, make pens a bit blacker fillin: = .2; 7, compensate for diagonal fillin o_correction: = .4; 7. but don't overshoot as much enddef; localfont:=lowres; 7. the mode most commonly used to make fonts Как правило, в распространяемых версиях системы METAFONT определяются еще несколько режимов работы, а значение localfont устанавливается несколько иным. Такие альтернативные не вносятся в файл plain.mf; они должны быть вынесены в отдельный файл, о чем мы еще поговорим ниже. 5. Рисование и заливка. Теперь мы подошли вплотную к макросам, которые служат посредниками между пользователем и примитивными командами системы МЕТА FONT, обеспечивающими работу с рисунками. Вначале вводится несколько констант, которые играют важную роль в программе. pen currentреп; path currentpen_path; picture currentpicture; transform currenttransform; def t_ = transformed currenttransform enddef; Ключевыми являются макросы fill, draw, filldraw и drawdot. def fill expr с = addtо.currentpicture contour c.t_ enddef; def addto_currentpicture = addto currentpicture enddef; def draw expr p = addto_currentpicture doublepath p.t. withpen currentpen enddef; def filldraw expr с = fill counterclockwise с withpen currentpen enddef; def drawdot expr z = if unknown currentpen.path: def_pen_path_ fi * addto.currentpicture contour currentpen.path shifted (z.t_) withpen penspeck enddef; def def_pen_path_ = hide(currentpen_path=tensepath makepath currentpen) enddef; У всех этих макрокоманд имеются "антиподы". def unfill expr с = fill с withweight -1 enddef; def undraw expr p = draw p withweight -1 enddef; def unfilldraw expr с = filldraw с withweight -1 enddef; def undrawdot expr z = drawdot z withweight -1 enddef;
Приложение В: Базовые определения 283 def erase text t = begingroup interim default_wt_:=-l; cullit; t vithveight -1; cullit; endgroup enddef; newinternal default.wt.; default.wt.:=1; Обрезание концов линий — задача более сложная, но следующие макросы (рассмотренные в конце главы 16) отлично с ней справляются: def cutdrav expr р s У, caution: you may need autorounding=0 cutoff(point 0 of p, 180+angle direction 0 of p); cutoff(point infinity of p, angle direction infinity of p); culldrav p enddef; def culldrav expr p s addto pic_ doublepath p.t. withpen currentpen; cull pic_ dropping(-infinity,0) withweight default.wt.; addto.currentpicture also pic_; pic.:=nullpicture; killtext enddef; vardef cutoff(expr z,theta) ■ interim autorounding := 0; interim smoothing := 0; addto pic_ doublepath z.t. withpen currentpen; addto pic. contour (cut. scaled (1+max(-pen.lft,pen.rt,pen.top,-pen.bot)) rotated theta shifted z)t_; cull pic. keeping (2,2) withweight -default.wt.; addto currentpicture also pic_; pic.:=nullpicture enddef; picture pic_; pic_:=nullpicture; path cut.; cut, = ((0,-1) —(1,-1) —(1,1) —(0,1)—cycle) scaled 1.42; В макросе 'erase.cutdraw' используется зарезервированная переменная default-wt-. С целью экономии памяти зарезервированная переменная pic обычно приравнивается нулевому рисунку — nullpicture. Выбирая перо, мы не только устанавливаем значение величины currentpen, но и определяем значения penJft, pen-rt, pen-top и pen-bot, которые затем используются операторами Ift, rt, top и bot. def pickup secondary q = if numeric q: numeric.pickup, else: pen.pickup. fi q enddef; def numeric.pickup, primary q = if unknown pen.[q]: errmessage "Unknown pen"; clearpen else: currentpen:=pen_[q]; pen.lft:=pen.lft.[q]; pen.rt:=pen.rt.[q]; pen.top:=pen.top.[q] ; pen.bot:=pen_bot.[q]; currentpen.path:=pen_path_[q] f i; enddef; def pen.pickup. primary q = currentpen:=q yscaled aspect.ratio; pen.lft:=xpart penoffset down of currentpen; pen.rt:=xpart penoffset up of currentpen; pen.top:=(ypart penoffset left of currentpen).o_; pen.bot:=(ypart penoffset right of currentpen)_o_; path currentpen.path; enddef; newinternal pen.lft,pen.rt,pen_top,pen.bot,pen.count_; А при сохранении пера соответствующие величины также сохраняются. vardef savepen = pen_[incr pen_count.]=currentpen; pen.lft.[pen.count.]=pen_lft; pen.rt.[pen.count.]=pen.rt; pen.top.[pen.count.]=pen_top; pen.bot.[pen.count.]=pen_bot;
284 Приложение В: Базовые определения pen_path_[pen_count_]=currentpen_path; pen.count. enddef; def clearpen = currentpen:=nullpen; pen_lft:=pen_rt:=pen_top:=pen_bot:=0; path currentpen_path; enddef; def clear_pen_memory = pen_count_:=0; numeric pen_lft_[] ,pen_rt_[] ,pen_top_[] ,pen_bot_[] ; pen currentpen,pen_[]; path currentpen_path, pen_path_[]; enddef; Четыре основные функции, определяющие положение краев пера, имеют вполне предсказуемый вид: vardef 1ft primary х = х + if pair х: (pen_lft,0) else: pen_lft fi enddef; vardef rt primary x = x + if pair x: (pen_rt,0) else: pen_rt fi enddef; vardef top primary у = у + if pair y: (0,pen_top) else: pen.top fi enddef; vardef bot primary у = у + if pair y: (0,pen_bot) else: pen_bot fi enddef; Имеется шесть функций, предназначенных для округления координат пера до значений, оптимальных с точки зрения расположения на растре. vardef good.x primary х = hround(x+pen_lft)-pen_lft enddef; vardef good.у primary у = vround(y+pen_top)-pen_top enddef; vardef good.lft primary z = save z_; pair z_; (z_+(pen_lft,0))t_=round((z+(pen_lft,0))tJ; z. enddef; vardef good.rt primary z = save z_; pair z_; (z_+(pen_rt,0))t_=round((z+(pen_rt,0))t_); z_ enddef; vardef good.top primary z = save z_; pair z_; (z_+(0,pen_top))t_=round((z+(0,pen_top))t_); z_ enddef; vardef good.bot primary z = save z_; pair z_; (z_+(0,pen.bot))t_=ro\ind((z+(0,pen_bot))t.) ; z. enddef; Мы уделили достаточно внимания неподвижным перьям. Если, линии определяются контурами, важнейшую роль играет макрос penpos. Поскольку команда penpos может использоваться довольно часто, чтобы выиграть в скорости мы могли бы определить его для точек, не используя z обозначение, а оперируя непосредственно с координатами х и у. vardef penpos®#(expr b,d) = (хв#г-х®#1,ув#г-ув#1)=(Ь,0) rotated d; x®#=.5(х®#1+хФ#г); y«#=.5(ув#1+уф#г) enddef; Построение линий при помощи перьев обеспечивает удобная команда penstroke. def penstroke text t = forsuffixes e = l,r: path_.e:=t; endfor if cycle path_.l: cyclestroke_ else: fill path_.l — reverse path_.r — cycle fi enddef; def cyclestroke_ = begingroup interim turningcheck:=0; addto pic_ contour path_.l.t_ withweight 1; addto pic_ contour path_.r.t_ withweight -1; cull pic_ dropping origin withweight default_wt_; addto.currentpicture also pic_; pic_:=nullpicture endgroup enddef; path path_.l,path_.r;
Приложение В: Базовые определения 285 6. Метки и прямые на пробных оттисках. Следующий раздел файла plain.mf содержит макросы, позволяющие размещать специальную информацию на пробных оттисках. Эти макросы обсуждаются в приложении Н, а используемые в них команды special и numspecial обсуждаются в приложении G. Метки генерируются на самом низком уровне командой makelabel. vardef makelabeie#(expr s,z) = У, puts string s at point z if known z: special lcode_®# ft s; numspecial xpart(z.t_); numspecial ypart(z.t_) fi enddef; string lcode_,lcode_.top,lcode_.1ft,lcode_.rt,lcode_.bot, lcode_.top.nodot,lcode..1ft.nodot,lcode_.rt.nodot,lcode_.bot.nodot; lcode_.top=" 1"; lcode_.lft=" 2"; lcode_.rt=" 3"; lcode_.bot=" 4"; lcode_=" 0"; '/, change to " /" to avoid listing in overflow column lcode_.top.nodot=" 5"; lcode_.lft.nodot=" 6"; lcode_.rt.nodot=M 7M; lcode_.bot.nodot=M 8"; Пользователю незачем обращаться к команде makelabel прямо, поскольку существуют более удобные средства. Например, макрос ' labels (1,2,3)' раскладывается в последовательность вида 'makelabel("1", z\); makelabel("2",22); makelabel("3й, 2:3)'. (Но этого не произойдет, если proofing < 1.) vardef labelsfl#(text t) = if proofing>l: forsuffixes $=t: makelabelO#(str$,z$); endfor fi enddef; vardef penlabelsfi#(text t) = if proofing>l: forsuffixes $$=l,,r: forsuffixes $=t: makelabeie#(str$.$$,z$.$$); endfor endfor fi enddef; Если вам нужно расставить множество меток, которые представляют собой просто числа, вы можете, например, использовать команду labels(l, range 5 thru 9, range 100 thru 124, 223) которая эквивалентна команде 'labels(l,5,6, 7,8, 9,100,101,..., 124,223)'. Если соответствующее значение z неизвестно, то метки просто опускаются, поэтому не будет (почти) никакого вреда, если в перечне будут присутствовать метки, которые фактически не используются. def range expr х = numtok[x] enddef; def numtok suffix x=x enddef; tertiarydef m thru n = m for x=m+l step 1 until n: , numtok[x] endfor enddef; (Команда range применима также к списку цикла типа forsuffixes; а в случае цикла типа for вы можете даже опустить слово 'range'. Но задавая множество значений, вы рискуете забить ими весь основной раздел памяти системы.) Команда proofrule рисует на пробном оттиске прямую линию. В отличие от команды makelabel, команда proofrule не учитывает значение currenttransform. Кроме того, имеется соответствующая подпрограмма screenrule, которая проводит прямую линию на экране поверх текущего рисунка, позволяя, таким образом, сделать видимыми все вспомогательные линии. def proofrule(expr w,z) = special "rule"; numspecial xpart v; numspecial ypart v; numspecial xpart z; numspecial ypart z enddef; def screenrule(expr w,z) = addto currentpicture doublepath w—z withpen rulepen enddef; pen rulepen; rulepen = pensquare scaled 2;
286 Приложение В: Базовые определения (Используемое здесь перо rulepen имеет два пикселя в ширину, поскольку прямые на экране проводятся, как правило, поверх растровых линий. Края пера, имеющего ширину два пикселя, располагаются так, что вы можете "видеть" правильное положение линии. Если линии шириной в два пикселя окажутся слишком жирными, вы можете переопределить перо rulepen, сделав его равным pensquare; тогда METAFONT будет проводить на экране линии наименьшей возможной толщины, но эти линии будут располагаться на полпикселя выше и на полпикселя правее.) При помощи команды makegrid вы можете построить на пробных оттисках сетку из прямых линий, которые задаются произвольными наборами координат х и у. def makegrid(text xlist,ylist) = xmin_ := min(xlist); xmax_ := max(xlist); ymin. := min(ylist); ymax_ := max(ylist); for x=xlist: proofrule((x,ymin_), (x,ymax_)); endfor for y=ylist: proofrule((xmin_,y), (xmax_,y)); endfor enddef; Наконец, имеется несколько макросов, которые обеспечивают дальнейшее взаимодействие с программой, генерирующей пробные оттиски, которая описана в приложении Н. Они позволяют задавать шрифт, которым будут набираться метки, толщину прямых линий, а также изменять положение картинки на странице. vardef titlefont suffix $ = special "titlefont "ftstr$ enddef; vardef labelfont suffix $ = special "labelfont "&str$ enddef; vardef grayfont suffix $ = special "grayfont "&str$ enddef; vardef siantfont suffix $ = special "slantfont "&str$ enddef; def proofoffset primary z = */, shift proof output by z special "offset"; numspecial xpart z; numspecial ypart z enddef; vardef proofrulethickness expr x = special "rulethickness"; numspecial x enddef; 7. Символы и организация шрифта. Выполнив всю эту подготовительную работу, мы, наконец, готовы рассмотреть команды для построения символов шрифта. Весь процесс происходит в рамках конструкции beginchar ... endchar. Каждая лексема beginchar открывает новую группу, которая должна заканчивается лексемой endchar. Затем команда beginchar сохраняет код символа и аппаратно-независимые размеры символа в виде внутренних величин charcode, charwd, charht и chardp. Затем вычисляются аппаратно- независимые параметры рамки символа: w, h и d. Наконец, команда "очищает" значения всех переменных 2, текущего рисунка и текущего пера. def beginchar(expr c,w_sharp,h_sharp,d_sharp) = begingroup charcode:=if known c: byte с else: 0 fi; charwd:=w_sharp; charht:=h_sharp; chardp:=d_sharp; w:=hround(charwd*hppp); h:=vround(charht*hppp); d:=vround(chardp*hppp); charic:=0; clearxy; clearit; clearpen; scantokens extra.beginchar; enddef; Курсивная поправка обычно равна нулю, если только пользователь не использовал команду 'italcorr'; но даже в этом случае поправка останется равной нулю, если пользователь специально не задаст ей положительное значение. def italcorr expr x.sharp = if x_sharp>0: charic:=x_sharp fi enddef;
Приложение В: Базовые определения 287 Когда требуется изменить ширину пикселя w с четной на нечетную, или наоборот, мы обращаемся к макросу change.width. def change.width = w:=w if w>charwd*hppp:- else:+ fi 1 enddef; (Пользователь может также изменить величину w каким-либо другим способом.) Текущее значение w на момент применения команды endchar будет считаться "официальной" шириной пикселя для данного символа. Эта величина, chardx, выводится затем в выходной gf-файл. def endchar = scantokens extra.endchar; if proofing>0: makebox(proofrule); fi chardx :=w; '/, desired width of the character in pixels shipit; if displaying>0: makebox(screenrule); showit; fi endgroup enddef; Эти подпрограммы можно дополнить новыми командами, "поместив" последние в переменные-цепочки extra-beginchar и extra-endchar. string extra.beginchar, extra_endchar; extra.begincharsextra.endchar*""; Ограничивающая рамка, которая строится вокруг символа на основании информации, заданной в команде beginchar, генерируется командой makebox. Эта команда учитывает возможность того, что пиксели могут быть не квадратными. Если курсивная поправка не равна нулю, то проводится дополнительная линия, отмечающая ширину символа с учетом этой поправки. def makebox(text г) = for y=0,h.o.,-d.o.: r((0,y),(w,y)); endfor 7, horizontals for x=0,w: r((x,-d.o_) , (x,h.o.)) ; endfor '/, verticals if charicOO: r((w+charic*hppp,h.o.) , (w+charic*hppp, .5h.o.)); fi enddef; Альтернативой подпрограмме makebox является подпрограмма maketicks, которая рисует более "заметные" рамки. Она позволяет отследить визуально, насколько близко символ подходит к границам ограничивающей его рамки. def maketicks(text г) = for y=0,h.o_,-d.o_: r((0,y),(10,у)); r((w-10,y),(w,y)); endfor for x=0,w: r((x,10-d.o.),(x,-d.o_)); r((x,h.o.-10),(x,h.o_)); endfor if charicOO: r((w+charic*hppp,h.o.-10), (w+charic*hppp,h.o_)); fi enddef; Информация о шрифте в целом задается следующими командами, описанными в приложении F. def font.size expr х = designsize:=х enddef; def font.slant expr x = fontdimen 1: x enddef; def font_normal_space expr x = fontdimen 2: x enddef; def font.normal.stretch expr x = fontdimen 3: x enddef; def font.normal.shrink expr x = fontdimen 4: x enddef; def font.x.height expr x = fontdimen 5: x enddef; def font.quad expr x = fontdimen 6: x enddef; def font.extra.space expr x = fontdimen 7: x enddef; def font.identifier expr x = font.identifier_:=x enddef;
288 Приложение В: Базовые определения def font_coding_scheme expr х = font_coding_scheme_:=x enddef; string font_identifier_, font_coding_scheme_; font_identifier_=font_coding_scheme._=,,UNSPECIFIED,,; 8. Последние штрихи. Что мы упустили? Нужно определить еще несколько объектов. Во-первых, мы до сих пор не определили z как сокращенную форму записи для точек. vardef гв#=(хв#,ув#) enddef; Затем мы должны дать элементарное описание "окон" системы METfi FONT и связанных с ними операций. newinternal screen.rovs, screen.cols, currentvindov; screen_rovs:=400; У, these values should be corrected, screen_cols:=500; '/, by reading in a separate file after plain.mf def openit = openvindov currentvindov from origin to (screen_rovs,screen_cols) at (-50,300) enddef; def shovit = openit; let shovit=shovit_; shovit enddef; 7, first time only def shovit. = display currentpicture invindov currentvindov enddef; В базовом формате имеется еще несколько кратких команд, таких как 'openit' и 'showit': def clearxy = save x,y enddef; def clearit = currentpicture:=nullpicture enddef; def shipit = shipout currentpicture enddef; def cullit = cull currentpicture dropping (-infinity,0) enddef; Следующие несколько макросов определяют команды, которые вы можете дать в командной строке в самом начале сеанса работы (т.е. до того, как вы введете 'input (имя входного файла)'): ■ screenchars. Наберите эту команду, если вы создаете шрифт и хотите, чтобы символы выдавались на экран перед тем, как будут отправлены в выходной файл. ■ screestrokes. Наберите эту команду, если вы работаете в режиме proof и хотите, чтобы на экран выдавалась каждая новая линия перед тем, как она будет внесена в текущий рисунок. ■ imagerules. Наберите эту команду, если вы хотите включить в текущий символ ограничивающую рамку еще до того, как начать его рисовать. ■ gf corners. Наберите эту команду, если вы планируете создавать пробные оттиски с крупными пикселями для символов шрифта с низким разрешением. ■ nodisplays. Наберите эту команду, чтобы сэкономить машинное время, если вы не хотите, чтобы в режиме proof каждый символ автоматически выводился на экран. ■ notransf orms. Наберите эту команду, чтобы сэкономить машинное время, если вы заранее знаете, что currenttransform — тождественное преобразование. def screenchars = У, endchar should 'shovit* extra.endchar:=extra_endcharftMshovit;" enddef; def screenstrokes = У, every stroke should 'shovit* def addto_currentpicture text t= addto currentpicture t; shovit enddef; enddef; def imagerules = '/• a box should be part of the character image extra_beginchar:=extra_beginchar ft "makebox(screenrule);" enddef; def gf corners = У, 'make ticks' should send rules to the gf file extra_setup:=extra_setup ft "let makebox=maketicks;proofing:=1;" enddef;
Приложение В: Базовые определения 289 def nodisplays « У, endchar shouldn't 'showit* extra_setup:=extra_setup & "displaying:=0;" enddef; def notransforms ■ '/, currenttransform should not be used let t_ = \ enddef; Мы определяем команду 'bye' как синоним 'end' специально для пользователей ГЩХ, которые могут ожидать, что программы в системах METAFONT и TeX оканчиваются одинаково. let bye = end; outer end,bye; Наконец, мы задаем установки, принимаемые по умолчанию. Они определяют те условия, в которые попадает пользователь, когда провести небольшие эксперименты, вроде описанных в главе 5. clear_pen_memory; У, initialize the 'savepen* mechanism mode.setup; '/, establish proof mode as the default numeric mode,mag; '/, but leave mode and mag undefined Ну вот мы и добрались до конца файла plain.mf. 9. Адаптирование к местным условиям. Для того чтобы программы для МЕТА FONT можно было использовать на разных машинах, все должны использовать один и тот же базовый форматный файл plain.mf. Но есть вещи, которые по понятным причинам должны выбираться в зависимости от особенностей конкретного аппаратного обеспечения. ■ Дополнительные режимы должны определяться с учетом того, для какого выходного устройства разрабатывается шрифт. ■ Должен быть установлен подходящий режим localfont. ш Должны быть установлены правильные значения величин screen.rows и screen.cols. Ниже приводится примерный вариант файла 'local .mf', подходящий для использования на компьютере, к которому подключены гипотетические принтеры cheapo и luxo, описанные в главе 11. Мы предполагаем, что режим работы cheapo в целом совпадает с режимом lowres, с той лишь разницей, что в режиме cheapo шрифты генерируются с отрицательным значением параметра fillin (поскольку при печати принтером cheapo диагональные линии выходят тоньше, чем обычно). Предполагается, что экран монитора имеет 768 пикселей в ширину и 512 пикселей в высоту. У, A file to be loaded after "plain.mf". base.version:=base_version&"/drofnats"; screen.rovs:=512; screen_cols:=768; mode.def cheapo = */, cheapo mode: to generate fonts for cheapo lowres.; '/, do as in lowres mode, except: fillin:=-.l; 7, compensate for lighter diagonals enddef; mode.def luxo = '/, luxo mode: to generate fonts for luxo proofing:=0; '/, no, we're not making proofs fontmaking:=l; */, yes, we are making a font tracingtitles:=l; У, yes, show titles online pixels_per_inch:=2000; У, almost 30 pixels per pt blacker: = .2; У, make pens a teeny bit blacker fillin: = .l; У, but compensate for heavy diagonals o.correction:=l; У, and keep the full overshoot enddef; localfont:=cheapo;
290 Приложение В: Базовые определения Макрокоманду Ъуе' можно переопределить, как это предлагается сделать в приложении F. Для того чтобы подготовить форматный файл, приспособленный к местным условиям, человек, который уполномочен это сделать, должен запустить программу INIMF и провести сеанс работы следующим образом: This is METAFONT, Version 2.0 (INIMF) 8 NOV 1989 10:09 ♦♦plain (plain.mf Preloading the plain base, version 2.0) ♦input local (local.mf) ♦dump Beginning to dump on file plain.base (To, что стоит после символов ♦♦ и ♦, набрано пользователем; все остальное напечатала сама система. На самом деле появится еще несколько сообщений.) Заметьте, что файл local.mf не содержит новых макросов или иных средств, которые бы программист мог использовать особым образом. То есть он не делает базовый форматный файл несовместимым с реализациями системы на других машинах. Изменения и/или дополнения в макросы файла plain.mf можно вносить только при условии, что измененный файл вы будете строго отличать от стандартного базового форматного файла. Но создание новых форматных файлов с другими именами только приветствуется. Например, автор разработал специальный форматный файл, предназначенный для разработки шрифтов семейства Computer Modern, что позволило ему избежать ввода 700 строк определений макросов в начале каждого сеанса работы. Для того чтобы быстро загрузить этот форматный файл, вы можете набрать '&ст' в ответ на приглашение ♦♦ системы METAFONT. (Для некоторых машин разработана специальная версия системы 'cmmf', в которую этот новый формат заложен изначально.)
Приложение В: Базовые определения 291 По сути, лишь основа заслуживает восхищенья. — МАЙКЛ ДРЕЙТОН, Роберт, герцог Нормандский (1605) До сих пор все шло, как говорится, без сучка и задоринки, но м-р Тилль знал, что главные трудности впереди. — ФРЕНСИС Е. ПЕЙДЖЕТ, Милфорд Мальвойсин (1842)
Коды символов
Приложение С: Коды символов 293 Несмотря на то что разные компьютеры по-разному представляют символы в текстовых файлах, система METAFONT на всех машинах дает одинаковые результаты. Дело в том, что, считывая файл, METAFONT переводит все символы в стандартную внутреннюю кодировку. А при записи текстового файла система переводит символы из своей внутренней кодировки в подходящую внешнюю, так что пользователям не приходится даже задумываться о том, что внутри машины происходит постоянный переход от одной кодировки к другой. Цель данного приложения — дать определение внутренней кодировки системы METAFONT, которая во всех реализациях имеет одни и те же характеристики. Существование такой кодировки очень важно, так как именно бна делает программы для METAFONT совместимыми. Схема кодировки METAFONT основана на Американской стандартной кодировке для обмена информацией (American Standard Code for Information Interchange), известной как кодировка "ASCII". Она содержит 128 кодов с номерами от 0 до 127. Мы условимся выражать их либо в восьмеричной системе счисления как числа от ocf'OOO" до oct"177", либо в шестнадцатеричной системе счисления как числа от hex"00" до hex"7F". Так, например, стандартное значение символа "Ъ" в кодировке ASCII будет равным не 98, a oct"142", или hex"62". В схеме ASCII коды с номерами от ocf'OOO" до ocf '040" и номер ocf'177" заняты под специальные функции; например, код ocf '007" называется BEL и означает: "Подать звуковой сигнал." Остальные 94 кода обозначают различные символы. Следующая таблица ASCII-кодов построена так, чтобы по ней легко определялось соответствие между восьмеричными и шестнадцатеричными кодами. '00х '01х '02х 'ОЗх Щх '05х '06х '07х '10х 'Их '12х '13х 'Цх '15х '16х '17х '0 NUL BS DLE CAN ( 0 8 в Н Р X < h Р X 8 '1 S0H НТ DC1 ЕМ ! ) 1 9 А I Q Y а i q У 9 '2 STX LF DC2 SUB к * 2 : В J R Z b j r z A '3 ETX VT DC3 ESC # + 3 » с к S [ с k s { В '4 EOT FF DC4 FS $ 9 4 < D L T \ d 1 t 1 С '5 ENQ CR NAK GS У. - 5 = E M и ] e m u } D '6 ACK SO SYN RS & . 6 > F N V " f n V ~ E 7 BEL SI ETB US > / 7 7 G 0 W _ g 0 w DEL F Ox lx 2x 3x 4x 5x 6x 7x С тех пор как в начале 1960-х была принята кодировка ASCII, люди постоянно придумывали все новые и новые применения кодам с номерами с oct"000" по oct"037" и oct"177", так как большинство изначально приписаных им функций было пригодно лишь для выполнения специальных операций, например, преобразо-
294 Приложение С: Коды символов вания файлов. Но эти функции были непригодны для таких приложений, как печать или интерактивные вычисления. Кроме того, производители вскоре наладили выпуск матричных принтеров, генерирующих 128 символов, 33 из которых могли модифицироваться с учетом нужд того или иного заказчика; таким образом, преимущество стандартного кода частично было утрачено. Поэтому приблизительно в 1965 году несколькими американскими университетами был разработан новый расширенный вариант кодировки ASCII, приспособленный для редактирования текста. В течение многих лет в университетах Станфорд, Карнеги-Меллон, Массачусетском институте технологий и пр. использовались вычислительные машины, у которых количество вводимых символов не ограничивалось числом 95, а достигало 120 и даже 121. К примеру, автор этой книги при разработке METAFONT использовал клавиатуру, содержащую символы '#', '<', '>' и '«-', пользоваться которыми удобнее, чем парами символов '<>', '<=', '>=' и ':='. Вот как выглядит полный набор этих символов: '00х '01х '02х 'ОЗх '04х '05х '06х '07х '10х 'Их '12х '13х 'Цх '15х '16х '17х '0 • Л С ♦- ( 0 8 Q Н Р X с h Р X 8 '1 4. 7 э -» 1 ) 1 9 А I Q Y а i q У 9 '2 а 6 П Ф и * 2 : В J R Z ъ j г z А '3 Р т и о # + 3 1 с к S [ с к s { В V Л ± V < $ > 4 < D L Т \ d 1 t 1 С '5 -i ф 3 > У. - 5 = Е М и ] е m и } D '6 € 00 9 = & 6 > F N V ** f n V - Ё 7 7Г д 5 V ) 1 7 7 G 0 W _ g о W / F бх 1х 2х Зх 4х 5х 6х 7х Систему METAFONT можно приспособить к работе с любым из символов с кодом от 128 до 255 и даже к работе со всеми этими символами. Однако маловероятно, что программы, в которых используется более 95 стандартных ASCII-символов, будут работать в других системах. Поэтому использование дополнительных символов себя не оправдывает. Было предложено компромисное решение этой проблемы, основанное на том, что легко написать такую программу, которая превращала бы файлы, содержащие дополнительные символы, в стандартные файлы, подставляя 'о' вместо '^' и т.д. В версии автора этой книги, которую он использовал в Станфорде, при формировании лексем (см. главу 6) символы '#', '<', '>' и **-' расценивались как символы того же класса, что и '<', '=', ':' и *>\ В этом случае такие лексемы, как '#=' и '<>', отличаются друг от друга, несмотря на то, что после перевода в стандартные
Приложение С: Коды символов 295 символы обе они превращаются в '<>='. Но, поскольку автор избегал использования таких лексем, его программы легко легко поддаются переводу в совместимую форму, предназначенную для повсеместного распространения. (Еще один возможный подход состоит в том, чтобы система METAFONT переводила нестандартные коды в пары непосредственно при вводе файла; но это менее эффективно.) Для компьютеров с нестандартным набором символов (отличным от ASCII) должно быть задано соответствие между 95 различными символами и стандартными ASCII-кодами в позициях с oct"040" по oct"176". Программы для METAFONT, написанные на любой из таких машин, будут абсолютно взаимозаменяемы. Кое-кому некоторые из объектов, на исследовании которых мы настаиваем (в частности, буквы алфавита), могут показаться слишком незначительными и тривиальными, и не заслуживающими того, чтобы серьезный человек тратил на них свое драгоценное время и усилия мысли. Да будет же известно этим господам, что с открытия истинной природы и первопричины любой самой ничтожной вещи начинается путь к настоящему знанию, и потому ничто не может быть недостойным внимания того, кто стремиться внести свой вклад в развитие науки. — ДЖОН ВИЛКИ НС, По следам реального символа (1668) Понятно, что даже простая аббревиатура А.В. С. таит в себе нечто загадочное. Как всякий шифр, она требует почтительного к себе отношения. Боюсь, однако, что в наше время к ним не всегда относились с должным уважением. — СТЭНЛИ МОРИСОН, О шрифтах (1923)
D Нестандартные приемы
Приложение D: Нестандартные приемы 297 Всякий мощный язык программирования может использоваться вовсе не так, как изначально задумывали его разработчики; это в большей степени касается языков, допускающих определение макрокоманд. Иногда такие непредусмотренные конструкции просто забавны, но не более; иногда они так заумны, что вызывают отвращение. Но случается и так, что они оказываются настолько полезными, что из разряда "приемчиков" переходят в разряд "методов". (К примеру, некоторые из макросов, которые теперь по праву занимают место в приложении В, изначально планировалось включить в приложение D.) Как бы то ни было, асы программирования всегда стремятся использовать возможности языка на полную катушку. И хотя мы не претендуем на то, что постигли всю глубину возможностей системы METAFONT, но смеем надеяться, что это приложение существенно расширяет наше представление о них. По крайней мере так было в то время, когда оно писалось. Благодарность: более половины идей, использованных в этом приложении, принадлежат Джону Хобби (John Hobby), который без устали, с огромным энтузиазмом принимал участие в работе по созданию обновленной версии METAFONT. Пожалуйста, воздержитесь от чтения изложенного здесь материала до тех пор, пока как следует не освоите базовый формат METAFONT (plain METAFONT). После того как вы узнаете тайны, раскрытые в этом приложении, вы будете знать о всевозможных хитросплетениях команд системы METAFONT, и у вас часто будет возникать искушение использовать в определениях макросов. Следует, однако, помнить: что бы вы ни делали, не стоит сразу использовать тот способ, который первым пришел вам в голову. Наверняка есть возможность сделать это лучше. Вы вполне можете обойтись и вовсе без ухищрений, поскольку METAFONT многое позволяет делать прямыми методами. Решая проблему, ищите сначала простое решение. 1. "Безумные" макросы. Если требуется написать сложный макрос, необходимо твердо усвоить первые пять пунктов главы 20. Символьные лексемы языка METAFONT делятся на две основные категории — разложимые и неразложимые. Первая категория включает все макросы, условные конструкции вида if...fi и циклы вида for... endfor, а также специальные операторы, такие, например, как input. Вторая же состоит из примитивных операторов и команд, перечисленных в главах 25 и 26. Разложение разложимых лексем происходят во "рту" системы METAFONT, а примитивные операции (включая обработку уравнений, объявлений и разного рода команд) выполняются в ее "желудке". Между этими органами системы существует определенная связь, поскольку в желудке вычисляются значения аргументов, которые входят в состав макросов, выполняемых во рту. Всякое утверждение можно поместить внутрь выражения-группы, так что сколь угодно сложную конструкцию можно представить как часть процесса разложения. Для начала рассмотрим простую и бесполезную задачу, с которой мы уже имели дело в приложении D книги Все про TeX. Заглянув туда, вы можете сравнить между собой системы TeX и METAFONT. Задача заключается в следующем. Дана числовая переменная п > 0, и требуется определить макрос asts, подставляемый текст которого представляет собой п звездочек. Задача осложнена тем, что разложение подавляется во время считывания подставляемого текста. Мы хотели бы использовать цикл for, но выполнение цикла — частный случай разложения. Иными словами, утверждение def asts = for х=1 upto n: * endfor enddef определяет макрос asts как цикл по его подставляемому тексту. Лексема asts будет вести себя так, как если бы она содержала п звездочек (возможно, с разными значениями п). ффффффффффф
298 Приложение D: Нестандартные приемы Однако это не решает поставленную проблему. В альтернативном определении def makedef primary n = def asts = for x=l upto n: * endfor enddef'enddef; makedef n значение n оказывается "замороженным"; но и это не решает проблему в целом. Еще одно возможное решение состоит в том, чтобы добавлять по одной звездочке за раз, используя команду expandafter следующим образом: def asts = enddef; for x=l upto n: expandafter def expandafter asts expandafter = asts * enddef; endfor. Три команды expandafter, содержащиеся в этом определении, позволяют вклиниться в подставляемый текст до того, как разложение будет подавлено. Без них подставляемый текст имел бы вид 'asts *', что привело бы к бесконечной рекурсии. Это решение требует времени порядка п2, поэтому читатель может удивиться, почему не было предложено более простое решение, такое как expandafter def expandafter asts expandafter = for x = 1 upto n: * endfor enddef Причина очень проста. Дело в том, что такой макрос работает только в случае п = 0! Из-за наличия команд expandafter цикл for не раскладывается до конца; выполняется только первый шаг разложения. А именно: считывается текст цикла, и в конце помещается специальная "недостижимая" лексема 'ENDFOR' (которая, кстати, не упоминалась в главе 20); затем текст цикла заново помещается во входную последовательность, если, конечно, цикл не закончился. Специальная лексема ENDFOR является запрещенной и, следовательно, не должна содержаться в подставляемом тексте. Поэтому, если вы попытаетесь применить определенный выше макрос при п > 1, то METAFONT остановится, выдав сообщение о запрещенной лексеме. Можно попытаться изменить статус лексемы с запрещенного на разрешенный, указав for х=1: inner endfor; но сделать это METAFONT не позволит. И даже если бы это сработало, проблема не была бы решена. Лексема ENDFOR просто вставлялась бы в подставляемый текст макроса asts, поскольку на время считывания подставляемого текста разложение приостанавливается. Существует еще одно решение проблемы, которое, как может показаться на первый взгляд, требует времени, порядка п, а не п2. Оно имеет вид: scantokensC'def asts=M for х=1 upto п: ft "* " endfor) enddef; Но для выполнения операции сцепления цепочек системе METAFONT требуется время, пропорциональное длине обрабатываемых цепочек; поэтому время работы по-прежнему будет иметь порядок п2. Кроме того, операции с цепочками в системе METAFONT довольно примитивны, так как они не являются основными элементами языка. Поэтому при таком подходе требуется порядка п2 ячеек памяти, зарезервированной под цепочки, хотя впоследствии они и освобождаются. Даже если бы объем памяти, используемой под цепочки, был неограниченным, все равно при больших п мы бы переполнили буфер системы. Дело в том, что операция scantokens, прежде чем сканировать цепочку, помещает ее в буфер. Существует ли вообще решение порядка п? Конечно! Например, def а=а* enddef; for х=0 upto n: if x=n: def a=quote quote def asts = enddef; fi expandafter endfor a enddef; shovtoken asts.
Приложение D: Нестандартные приемы 299 (Первая лексема 'quote' удаляется командой for, так что в процессе переопределения а "выживает" лишь одна команда quote. Если вы не понимаете эту программу, попытайтесь запустить ее с п = 3, вставьте "странное" выражение '0;' непосредственно перед лексемой 'if и внимательно просмотрите строчки контекста, которые METAFONT выдаст в четырех сообщениях об ошибках.) Единственное слабое место этого метода — это то, что в нем используется п ячеек, так что при п > 25, возможно, придется увеличить размер стека. Задача со звездочками — просто интересная головоломка. Теперь давайте займемся настоящим делом. Предположим, нужно определить макрос Чеп\ подставляемый текст которого представляет собой содержимое параметрического файла logo 10. mf из главы 11, за исключением двух последних строк. Эти две строчки имеют вид input logo У. now generate the font end 7, and stop. Макрос ten позволит нам устанавливать параметры 10-пунктового шрифта неоднократно по ходу программы (возможно, вы захотите также время от времени подключать параметры 9-пунктового шрифта, определив соответствующий макрос nine); в приложении Е описано, как использовать такие макросы в метаразработке шрифта. Первое, что приходит в голову, — попытаться вставить весь файл logo .mf целиком в макрос ten в качестве подставляемого текста. Нейтрализовать нежелательное действие последних трех лексем можно, указав save input,logo,end; forsuffixes s=input,logo,end: let s=\; endfor непосредственно перед тем, как использовать макрос ten. Для того чтобы сделать файл подставляемым текстом, мы можем воспользоваться методом, разработанным в процессе решения задачи о звездочках: expandafter def expandafter ten expandafter = input logo10 enddef. Однако эта наша первая попытка провалится, если мы предварительно не переопределим команду 'end'. Лексема 'end' определяется в приложении В как запрещенная, если она используется в подставляемом тексте. Поэтому мы укажем 'inner end' и предпримем новую попытку. Увы! В ответ мы получим лишь сообщение о том, что нарушено неписаное правило системы, о котором не было сказано в главах 20 и 26: Runaway definition? font_sizel0pt#;ht#:=6pt#;xgap#:=0.6pt#;u#:=4/9pt#;s#:=0;o#:=l/ ETC. ! File ended vhile scanning the definition of ten. <inserted text> enddef 1.2 ...fter ten expandafter = input logolO enddef; t Конец файла "невидим", но рассматривается как запрещенная лексема в том смысле, что файл не может заканчиваться в тот момент, когда METfiFONT быстро просматривает текст. Итак, этот подход в целом оказался непригодным. Нужно найти способ оборвать подставляемый текст до того, как будет обнаружен конец файла. Ну и ладно. Переопределим лексему 'input' так, чтобы она имел смысл 'enddef, а лексему logo так, чтобы она означала 'endinput'. let INPUT = input; let input = enddef; let logo = endinput; expandafter def expandafter ten expandafter = INPUT logo10; shoutoken ten. Ух ты, заработало! Кстати, строчку с тремя командами 'expandafter' можно заменить более элегантной конструкцией, в которой используется команда scantokens: scantokens "def ten=" INPUT logolO;
300 Приложение D: Нестандартные приемы Это и есть искомое решение, поскольку METAFONT всегда заглядывает вперед и раскладывает лексему, следующую за выражением, значение которого определяется в данный момент. (В нашем случае, это выражение "def ten=", которое является аргументом команды s cant оке ns. Система METAFONT всегда рассматривает следующую за выражением лексему, чтобы убедиться, что выражение закончилось.) Интересно, что в варианте с командой expandafter подставляемый текст начинается с лексем 'f ont_sizelOpt# ;ht# : = ...', а в варианте со scantokens он начинается с 'designsize:=(10) ;ht#: = ...'. Понимаете, почему? Во втором случае разложение продолжается до тех пор, пока не будет обнаружена неразложимая лексема ('designsize'), поэтому макрос font_size заменяется соответствующим подставляемым текстом. В первом же случае команда expandafter просто раскладывает лексему 'INPUT'. Давайте теперь немного усложним задачу. Допустим, мы знаем, что в конце входной последовательности должна стоять лексема 'input', но не знаем, что за ней последует лексема 'logo'. Мы знаем только, что следующей лексемой будет имя файла. Кроме того, предположим, что лексема 'end' может отсутствовать, так что мы не можем просто переопределить ее как enddef. В этом случае мы можем превратить лексему 'input' в правый разделитель и считать файл как выделенный текстовый аргумент. Тогда у нас будет достаточно времени, чтобы вставить другие лексемы, которые остановят ввод и сотрут ненужное имя файла. Но такая конструкция более сложна: let INPUT = input; delimiters begintext input; def makedef(expr name)(text t) = expandafter def scantokens name = t enddef; endinput flushfilename enddef; def flushfilename suffix s = enddef; makedef("ten") expandafter begintext INPUT logolO; shovtoken ten. Этот пример стоит внимательно изучить; при этом вы можете воспользоваться макросом 'tracingall', чтобы отследить все действия системы. Здесь предполагается, что неизвестное имя файла может быть классифицировано как суффикс; это решает проблему с тем, что файл не может заканчиваться внутри параметра типа text или ложного условия. (Точно зная, что лексема 'end' присутствует, можно было бы заменить 'endinput flushf ilename' на 'if false:' и переопределить 'end' как 'fi'.) Давайте теперь рассмотрим задачу попроще. Система METAFONT позволяет рассматривать логическое произведение ('and') двух булевых выражений, но при этом всегда вычисляет значение обоих операндов. Последнее иногда проблематично. Например, в ситуации с if pair х and (х>(0,0)): A else: В fi вычисление выражения 'х>(0,0)' будет приостановлено и выдано сообщение об ошибке, если только переменная х не имеет тип pair. Очевидный способ избежать этой ошибки if pair х: if х>(0,0): A else: В fi else: В fi громоздок и требует использования величины В дважды. Здесь лучше использовать условное логическое умножение — операцию, предусматривающую вычисление второго булева выражения только в том случае, когда первое выражение истинно. Имея в арсенале такой оператор 'cor', мы сможем написать if pair х cand (х>(0,0)): A else: В fi. Нам также может понадобиться оператор условного сложения — 'cor', который вычисляет значение второго операнда только в том случае, когда первый оказывается ложным. Например, он может использоваться в ситуации if unknown х cor (х<0): A else: В fi.
Приложение D: Нестандартные приемы 301 Макросы с and и cor можно определить следующим образом: def сand(text q) = startif true q else: false fi enddef; def cor(text q) = startif true true else: q fi enddef; tertiarydef p startif true = if p: enddef; текстовые аргументы будут теперь вычисляться только в случае необходимости. Фактически мы заменили первоначальную строку на строку вида if if pair х: х<(0,0) else: false fi: A else: В fi. Использование такой конструкции требует, чтобы операнды в правой части от cand и cor были заключены в скобки или другие лексемы, играющие роль разделителей. Но это требование — всего лишь незначительный нюанс, поскольку операнды логического умножения и логического сложения и так зачастую требуют обрамления разделителями. Невозможно определить операторы cand и cor так, чтобы они самостоятельно учитывали иерархическую структуру выражений. Дело в том, что макросы, различающие первичные, вторичные и третичные формулы, сначала вычисляют значения своих аргументов, а ведь именно этого мы стремились избежать, определяя операторы cand и cor. Если бы в макросах cand и cor использовались невыделенные текстовые аргументы, то эти аргументы не реагировали бы на двоеточие. Однако мы можем использовать такие модифицированные макросы с разделителями, имеющими смысл начала и конца группы. Например, предварительно определив let {{ = begingroup; let }} = endgroup; def cand text q = startif true q else: false fi enddef мы можем написать if {{pair x cand x>(0,0)}}: A else: В fi. (Этим мы ничего не выигрываем, а всего лишь иллюстрируем одно из свойств невыделенных текстовых аргументов.) Следует отметить, что в случае выделенных текстовых аргументов лексемы, определенные как начало и конец группы, нельзя использовать в качестве разделителей. Лексемы-разделители begingroup и endgroup, которые автоматически вставляются при определении переменной-макроса, обычно весьма полезны, но не всегда. Допустим, мы хотим определить макрос zz так, чтобы последовательность 'zzl. .zz2. .zz3' раскладывалась в zl{dzl}..z2{dz2}..z3{dz3} С использованием def это было бы тривиально: def zz suffix $ = z${dz$} enddef; но тогда лексема zz превратилась бы в "вызов". Если же мы непременно хотим определить этот макрос через vardef, чтобы zz можно было использовать в суффиксах и именах переменных, то дополнительные разделители begingroup и endgroup внесут сумятицу в правила синтаксиса для путей, поэтому необходимо от них избавиться. Вот один из способов решить эту проблему: vardef zz$# s endgroup gobbled true z®#{dz®#> gobble begingroup enddef. Функции gobbled и gobble, определенные в приложении В, удалят вырожденныеные выражения 'begingroup endgroup' в начале и конце подставляемого текста. (Вырожденные выражения не поглощаются командами gobbled и gobble, если переменная-макрос считывается как первичная формула. Но в таких случаях вы, вероятно, не будете против этого возражать.)
302 Приложение D: Нестандартные приемы 2. Неожиданные циклы. В макросах 'min' и 'max', приведенных в приложении В, используется тот факт, что в списках их аргументов запятые действуют как ') ('. Хотя "шапка" определения имеет вид def max(expr х)(text t) мы можем написать 'тах(а,6, с)' и в результате получим х = а и t = '6, с'. Конечно, не предполагается, что кому-то придет в голову написать 'тах(а)(Ь)(с)\ Приведем еще два примера использования этой идеи. Мы хотим, чтобы выражение 'inorder(a, 6, с)' было истинным тогда и только тогда, когда a < Ь < с; мы также хотим, чтобы команда 'equally-spaced(xi, хг,хз) dx' заменяла уравнения 1Х2 — xi = хз — хг = dx\ def inorder(expr х)(text t) = ((x for u=t: <= u) and (u endfor gobbled true true)) enddef; def equally_spaced(exp x)(text t) expr dx = x for u=t: - u = u endfor gobbled true - dx enddef. Вы не находите это забавным? (Присмотритесь повнимательней.*) Однако, попытавшись использовать эти макросы, содержащие циклы, в аргументах, мы столкнемся с проблемой. Рассмотрим выражения: inorder(for n=l upto 10: a[n], endfor infinity), inorder(a[l] for n=2 upto 10: ,a[n] endfor), inorder(a[l],a[2] for n=3 upto 10: ,a[n] endfor); В ответ на первые два выражения мы получим сообщения об ошибке. Но в третьем случае все проходит гладко! Причина в том, что в первых двух случаях разложение цикла for начинается прежде чем METAFONT начинает считывать текстовый аргумент, поэтому ENDFOR вновь поворачивается к нам своей неприглядной стороной. Этой проблемы можно избежать, определив макрос более сложно, не пытаясь отделить первый аргумент х: def inorder(text t) = expandafter startinorder for ust: <= u endgroup and begingroup u endfor gobbled true true endgroup) enddef; def startinorder text t = (begingroup true enddef; def equally_spaced(text t) expr dx = if pair dx: (whatever,whatever) else: whatever fi for u=t: - u = u endfor gobbled true - dx enddef; Здесь использованы две уловки: (1) Лексема 'endgroup', содержащаяся в макросе 'inorder', прерывает невыделенный текстовый аргумент; это позволяет избежать нежелательных лексем '<= и' в начале. (2) Псевдопеременная 'whatever' аннулирует нежелательное уравнение в начале макроса 'equally.spaced'. При этих новых определениях все три вышеприведенные выражения будут восприняты системой нормально, как и выражения типа equally.spaced(for n=l upto 10: x[n], endfor whatever) dx. Более того, теперь можно использовать макросы с одним аргументом: значение 'inorder(а)' всегда будет истинным, a 'equally-spaced(x) dx' не даст никаких новых уравнений. Для знатоков английского: *u endfor gobbled true' может быть прочитано как "You end for gobbled true". — Прим.перев.
Приложение D: Нестандартные приемы 303 Если мы хотим таким же образом усовершенствовать макрокоманды 'min' и 'max', чтобы в их аргументах можно было задавать циклы: max(a[l] for n=2 upto 10: ,a[n] endfor) и чтобы в случае одного аргумента выполнялось равенство 'шах(а) = а', то придется повозиться, поскольку макросы 'max' и 'min' обрабатывают свои аргументы по-особенному. Придется применить специальный макрос setu-> который определяет тип вспомогательной переменной и_. Самый быстрый способ решения этой задачи состоит, вероятно, в том, чтобы использовать лексему, смысл которой изменяется во время первого прохода цикла: vardef max(text t) = let switch. = firstset.; for u=t: switch. u>u_: u. := u ;fi endfor u. enddef; vardef min(text t) = let switch. = firstset.; for u=t: switch. u<u_: u. := u ;fi endfor u_ enddef; def firstset. primary u = setu. u; let switch. = if; if false: enddef. Между прочим, в первоначальных вариантах макросов 'min' и 'max', написанных автором, содержалась одна забавная ошибка. Они начинались командой 'save tx_' и пытались определить, является ли проход первым и является ли переменная и_ неизвестной. Но этот подход оказался неудачным, так как величина tx_ может все время оставаться неизвестной, даже в таких корректно определенных случаях, как случай тах(х, х + 1, х + 2). 3. Типы. Определенные выше макросы inorder, equally.spaced и max являются достаточно гибкими в том смысле, что фигурирующее в них выражение может иметь практически любой тип. Функции 'round' и 'byte' (см. приложение В) также являются примерами макросов, которые ведут себя по-разному в зависимости от типа их аргумента, представляющего собой выражение (т.е. параметр типа ехрг). Давайте рассмотрим более детально, как проверяется тип выражения. Когда автор разрабатывал макросы базового формата METAFONT, он сначала определил макрос max как vardef max(text t) = save u.; boolean u_; for u=t: if boolean u_: setu. u elseif u_<u: u. := u fi; endfor u_ enddef. Интересно, что при таком определении не было необходимости устанавливать значение переменной и_ истинным или ложным. Уже того, что эта переменная была булевой, было достаточно, чтобы определить, является ли проход цикла первым. (В то время использовался несколько иной вариант макроса setu-.) Если бы требовалось обобщить оператор 'scaled' так, чтобы команда 'scaled(x, j/)' заменяла последовательность 'xscaled х yscaled у\ то сделать это было бы довольно легко: let SCALED = scaled; def scaled primary z = if pair z: xscaled xpart z yscaled ypart z else: SCALED z fi enddef; Примитивную операцию 'SCALED z' стоит сохранить, так как вариант 'xscaled z yscaled z' требует больше времени для выполнения.
304 Приложение D: Нестандартные приемы METAFONT позволяет сравнивать на предмет равенства булевы и числовые переменные, а также переменные типов pair, string и transform. Но выражение 'р = q\ где р и q — пути, перья или рисунки, грамматически неверно. Давайте напишем более общий макрос для проверки равенства — такой, чтобы выражение (р == q* было истинным тогда и только тогда, когда величины р и q произвольного типа известны и равны. tertiarydef р == q = if unknown р or unknown q: false elseif boolean p and boolean q: p=q elseif numeric p and numeric q: p=q elseif pair p and pair q: p=q elseif string p and string q: p=q elseif transform p and transform q: p=q elseif path p and path q: if (cycle p = cycle q) and (length p = length q) and (point 0 of p = point 0 of q): patheq p of q else: false fi elseif pen p and pen q: (makepath p == makepath q) elseif picture p and picture q: piceq p of q elseif vacuous p and vacuous q: true else: false fi enddef; vardef vacuous primary p = not(boolean p or numeric p or pair p or path p or pen p or picture p or string p or transform p) enddef; vardef patheq expr p of q = save t; boolean t; t=true; for k=l upto length p: t := (postcontrol k-1 of p = postcontrol k-1 of q) and (precontrol k of p = precontrol k of q) and (point k of p = point k of q); exitunless t; endfor t enddef; vardef piceq expr p of q = save t; picture t; t=p; addto t also -q; cull t dropping origin; (totalweight t=0) enddef; Если p и q — числовые выражения или выражения-пары, мы можем ослабить условие, чтобы р и q были известными, указав 'if known р — q: р = q else false ft'; точно так же мы можем поступить и с преобразованиями, проверяя по очереди все шесть их компонент. Но если переменные являются булевыми, путями и т.п. и обе они неизвестны, приравнивая их, мы рискуем изменить значения других переменных. 4- Нелинейные уравнения. В системе METAFONT имеется встроенный механизм решения линейных уравнений. Но если уравнение нелинейно, этот механизм не работает. Зачастую нелинейные уравнения можно решить вручную средствами алгебры или анализа, но иногда проще воспользоваться макросом * solve1 из базового формата METAFONT. Он позволяет решить п уравнений с п неизвестными при условии, что, если одна из неизвестных фиксирована, то нелинейным может быть лишь одно из уравнений.
Приложение D: Нестандартные приемы 305 Мы проиллюстрируем этот общий метод на примере частного случая, когда п = 3. Попробуем отыскать такие числа а, бис, что -2а + ЗЬ/с = с - 3; ас + 26 = с3 - 20; з , , з _ 2 а +о = с . При фиксированном с первые два уравнения линейны по переменным а и 6. Оставшееся уравнение мы превращаем в неравенство, заменяя знак '=' на '<', и помещая систему уравнений внутрь булевозначной функции: vardef f(expr с) = save a,b; -2а + ЗЬ/с = с - 3; а*с + 2Ь = с*с*с - 20; а*а*а + Ъ*Ь*Ъ < с*с enddef; с = solve f(1,7); -2а + ЗЬ/с = с - 3; а*с + 2Ъ = с*с*с - 20; show a, b, с. Если мы установим tolerance = epsilon (т.е. минимальное возможное значение, при котором подпрограмма solve не зацикливается), то получим значения о = 1, Ь = 2ис = 3 (из чего ясно, что пример был специально подобран). При обычном значении величины tolerance, равном 0.1, мы получаем а = 1.05061, Ъ = 2.1279, с = 3.01563. Для практических целей такой точности вполне достаточно (если речь идет о единицах пикселей). (Уменьшая значение tolerance, мы получаем выигрыш во времени, т.к. уменьшается число итераций в процедуре solve; нужно найти золотую середину между точностью и затратами времени.) Единственный непонятный момент в определении макроса solve — это выбор чисел 1 и 7 в '/(1,7)'. В типичной ситуации, как правило, очевидно, при каких значениях неизвестных / имеет значение true, а при каких — false. Но в данном случае пришлось немного поэкспериментировать. Действительно, как выясняется, для рассматриваемой системы /(—3) истинно, а /(—1) — ложно. Установив в программе с = /(—3,-1), мы получим другое решение: а = 7.51442, Ь = —7.48274, с = —2.3097. Более того, данная система не имеет решения, если значение с находится в промежутке от —1 до -1-1, несмотря на то, что значение /(+1) истинно, а /(—1) — ложно! При с -> 0 величина а3+63 стремится к —оо, когда с отрицательно, и к +оо, когда с положительно. При попытке вычислить 4solve /(1,-1)' возникает ошибка деления на ноль и, как следствие, несколько ошибок переполнения в арифметических операциях. Теперь рассмотрим реальную, а не выдуманную проблему. Пусть требуется найти вершины параллелограмма 2u,zir,2oj,2(Hi такие, что хи = a; yir = 6; z0r = (c,d); length(zir - zu) = length(zor - z0i) = stem, и такие, что линии z\T -- zu и z\T -- zqt пересекаются под заданным углом ф. В качестве "нелинейной" неизвестной мы можем выбрать общий угол наклона в векторов z\r — z\\ и zot — zoi- Тогда уравнения, которые нужно решить, могут penpos^stem^d)] penpos0(stem ,0); хц = a; yir = 6; z0r = (c,d); angle(zir - z0r) = в + ф. (a,b) (c,d) быть записаны в виде
306 Приложение D: Нестандартные приемы При фиксированном значении в все эти уравнения, за исключением последнего, являются линейными. Поэтому мы можем решить их, использовав общий метод: vardef f(expr theta) = save x,y; penposl(stem,theta); penposO(stem,theta); xll=a; ylr=b; z0r=(c,d); angle(zlr-zOr)<theta+phi enddef; theta=solve f(90,0); penposl(stem,theta); penposO(stem,theta) xll=a; ylr=b; z0r=(c,d); show zll,zlr,z01,z0r,theta,angle(zlr-zOr). Например, если a = 1, b = 28, с = 14, d = 19, stem = 5 и ф = 80, то при значении tolerance = epsilon мы получим следующий ответ: (1,23.703) (3.557,28) (11.443,14.703) (14,19) 59.25 139.25 а при значении tolerance =0.1 мы получим ответ (1,23.702) (3.554,28) (11.446,14.702) (14,19) 59.28 139.25. Функция /, которая задается общим методом, часто может быть упрощена; например, в данном случае, удаляя ненужные детали, мы получим всего-навсего vardef f(expr theta) = save x,y; penposl(stem,theta); xll=a; ylr=b; angle(zlr-(c,d))<theta+phi enddef. Только что рассмотренную проблему можно назвать d-проблемой, так как, во-первых, она возникла в процессе разработки Н.Н.Биллавалой (N.N.Billawala) метадизайна готической буквы 4Ь', а во-вторых, она рассматривается в приложении D. 5. Нелинейная интерполяция. Допустим, разработчик шрифта определил опытным путем "хорошие" значения величины / для некоторых значений параметра х. Величина /(х) может представлять собой вес линии, длину засечки, величину перелета и т.п. Эти найденные эмпирически значения могут быть обобщены и включены в метаразработку, если возможно произвести интерполяцию между исходными значениями х, определяя значения величины f(x) в промежуточных точках. Допустим, что значения величины известны для x = xi<X2<-<xn. При помощи системы METAFONT мы можем построить примерный график изменения величины /, как путь вида F = (xi, /(xi)) .. (х2, /(х2)) .. (и т.д.) .. (хп, /(хп)) если только /(х) ведет себя как вычислимая функция. Поэтому мы можем произвести интерполяцию, используя операцию пересечения путей (!): vardef interpolate expr F of х = save t; t = if x < xpart point 0 of F: extrap_error 0 elseif x > xpart point infinity of F: extrap.error infinity else: xpart(F intersectiontimes verticalline x) fi; ypart point t of F enddef; def extrap.error = hide(errhelp "The extreme value will be used."; errmessage '"interpolate1 has been asked to extrapolate"; errhelp "") enddef; vardef verticalline primary x = (x,-infinity)—(x,infinity) enddef; Например, если /(1) = l,/(3) = 2 и /(15) = 4, то эта схема интерполяции дает значение, равное 'interpolate (1,1) .. (3,2) .. (15,4) of 7\ то есть 3.37.
Приложение D: Нестандартные приемы 307 6. Рисование оверлеями. Давайте теперь от операций с числовыми величинами обратимся к графике. Брюсом Лебаном (Bruce Leban) было предложено использовать обобщенные команды 'clearit/showit/shipit\ Используя эти обобщенные команды в операциях 'fill' и 'draw', мы получаем в распоряжение листы воображаемой прозрачной пленки. Модифицированная команда 'keepit' покрывает существующий рисунок слоем пленки, предохраняя его от последующего стирания. Реализовать команду keepit можно следующим образом: мы вводим новую переменную-рисунок totalpicture и булевы переменные totalnull, currentnull и определяем соответствующий макрос: def clearit = currentpicture:=totalpicture:=nullpicture; currentnull:=totalnull:=true; enddef; def keepit = cull currentpicture keeping (l,infinity); addto totalpicture also currentpicture; currentpicture:=nullpicture; totalnull:=currentnull; currentnull:=true; enddef; def addto.currentpicture = currentnull:=false; addto currentpicture enddef; def mergeit (text do) = if totalnull: do currentpicture elseif currentnull: do totalpicture else: begingroup save v; picture v; v:=currentpicture; cull v keeping (l,infinity); addto v also totalpicture; do v endgroup fi enddef; def shipit = mergeit(shipout) enddef; def showit. * mergeit(show.) enddef; def show, suffix v = display v inwindow currentvindov enddef; Строго говоря, вся эта бухгалтерия с totalnull и currentnull не является жизненно необходимой, но существенно влияет на эффективность данной схемы в том случае, когда дополнительные возможности команды keepit реально не используются. При вычислении переменной-рисунка 'г»' в подпрограмме mergeit аккумулированный рисунок копируется перед тем, как он будет выведен на кран или отправлен на вывод. Эта операция требует дополнительных затрат времени и требует почти вдвое больше памяти, поэтому по возможности ее лучше избегать. 7. Сохранение рисунков. Если нам нужно скопировать рисунок в файл, чтобы затем считать его в другом сеансе работы, то перед нами встают две проблемы. (1) При выполнении команды shipout происходит подравнивание значений, ассоциированных с пикселями, так что оставшиеся после нее данные имеют вид массива нулей и единиц. Различаются только положительные и неположительные значения пикселей, а вся остальная информация о них безвозвратно теряется. (2) Результат операции shipout может использоваться в другом задании для системы METAFONT лишь в том случае, если имеется вспомогательная программа, которая по данным в двоичном gf-формате восстанавливает исходную программу для METAFONT. Система METAFONT умеет записывать gf-файлы, но не умеет их читать. Эти проблемы можно решить, если в качестве выходного файла использовать не gf-файл, а файл-стенограмму, или log-файл. Рассмотрим, например, использование команды tracingedges. Допустим, мы дали системе METAFONT следующие инструкции: tracingedges := 1; (произвольная последовательность команд fill, draw или filldraw) message "Tracing edges completed."; tracingedges := 0; Тогда log-файл будет содержать такие строчки:
308 Приложение D: Нестандартные приемы Tracing edges at line 15: (weight 1) (1,5)(1,2)(2,2)(2,1)(3,1)(3,0)(8,0)(8,1)(9,1)(9,2)(10,2)(10,8)(9,8) (9,9) (8,9) (8,10) (3,10) (3,9) (2,9) (2,8) (1,8) (1,5) . Tracing edges at line 15: (weight -1) (3,5)(3,2)(4,2)(4,1)(7,1)(7,2)(8,2)(8,8)(7,8)(7,9)(4,9)(4,8)(3,8)(3,5). Tracing edges at line 18: (weight -1) (No new edges added.) Tracing edges completed. Определим макросы, при помощи которых эти строчки можно было бы использовать в качестве входной последовательности: def Tracing=begingroup save :,[,],Tracing,edges,at,weight,w; delimiters []; let Tracing = endfill; interim turningcheck := 0; vardef atfi#(expr wt) = save (,); w := wt; let ( = lp; let ) = rp; fill[gobble begingroup enddef; let edges = \; let weight = \; let : = \; enddef; def lp = [ enddef; def rp = ] — enddef; vardef No<3# = origin enddef; def endfill = cycle] withweight w endgroup; enddef; def completed = endgroup; enddef; Здесь учитывался тот факт, что результаты отслеживания границ записываются в log- файл в строго определенном виде, с использованием ограниченного набора слов и строго регламентированным использованием скобок и запятых. Внеся незначительные изменения в приведенные выше определения, мы можем получить необычные эффекты. Например, заменив определение макроса гр на '] . .tension 4..' и вставив перед лексемой 'withweight' команду 'scaled 5pt', мы получим "почти оцифрованный" символ: О (Бугорки слева обязаны своим происхождением повторяющимся точкам '(1,5)'и'(3,5)'в исходных данных. От них можно избавиться, добавив лишний переход и отследив границы линий в выходном рисунке при помощи немодифицированных макросов типа Tracing, отвечающих за процессы отслеживания.) Действия команд fill и draw могут быть отслежены специальными средствами системы METAFONT (tracingedges), но это возможно далеко не для всех операций. Например, операции подравнивания системой не отслеживаются. Поэтому рассмотрим более общее представление рисунков, которое генерирует система METAFONT в случае, когда задано положительное значение величины tracingoutput или когда вы просите систему отобразить рисунок на экране при помощи команды show (см. главу 13). Приведенные ниже макросы позволяют восстановить рисунок по входной последовательности вида: beginpicture row 1: 1+ -2- | 0+ 2- row 0: I 0+ 2++ 5 row -2: 0 2+ | endpicture
Приложение D: Нестандартные приемы 309 где три строки в середине — это дословная выдержка из файла-стенограммы. (Задача была бы проще, если бы лексема '-'не выполняла две различные функции!) let neg_ = -; let colon. = :; def beginpicture = begingroup save row, I, :, , —, +, ++, +++, v, xx, yy, done; picture v; v := nullpicture; interim turningcheck := 0; let" = mmm_; let -- — mm_; let + = p_; let ++ = pp.; let +++ = ppp_; let row = pic.row; let I = relax; let : = pic_colon; : enddef; def pic.row primary у = done; yy := y; enddef; def pic_colon primary x = if known x colon. ; xx := x; pic.edge fi enddef; def pic.edge = let - = m_; addto v contour unitsquare xscaled xx shifted(0,yy) enddef; def mmm_ = withweight 3; let - = neg_; : enddef; def mm_ = withweight 2; let - = neg_; : enddef; def m_ = withweight 1; let - = neg_; : enddef; def p_ = withweight neg_l; let - = neg_; : enddef; def pp_ = withweight neg_2; let - = neg_; : enddef; def ppp_ = withweight neg_3; let - = neg_; : enddef; transform xy_swap; xy_swap = identity rotated 90 xscaled -1; def endpicture = done; v transformed xy_swap transformed xy_swap endgroup enddef; Читателю будет полезно изучить эти макросы повнимательней. Появившись в первый раз, лексема 'done' представляет собой первичную формулу с неизвестным значением, поэтому команда pic.colon не пытается генерировать новую грань. Кроме того, при вводе каждой новой грани для сохранения баланса вставляется соответствующая грань в точке х = 0. Двукратное применение команды xy.swap в конце стирает все лишние грани. (Последняя операция действует немного быстрее, чем операция 'rotated-90 rotated 90', которая использовалась с этой же целью в главе 13.) 8. Увеличение пера. Перейдем теперь к другому аспекту системы METAFONT, а именно — рассмотрим специальную операцию над многоугольниками перьев. Пусть задано перо р, и требуется построить перо 'taller р\ которое на один пиксель выше заданного. Например, если р имеет кончик в форме ромба '(0.5,0) -- (0,0.5) -- (—0.5,0) -- (0,-0.5) -- cycle', то увеличенный кончик будет иметь вид (0.5,0.5) -- (0,1) -- (-0.5,0.5) -- (-0.5, -0.5) -- (0,-1) -- (0.5, -0.5) --cycle; если р — очень тонкое наклонное перо с кончиком вида '(—х, — у) -- (х, у) -- cycle', то увеличенное перо будет имеет вид: (-х, -у - 0.5) - - (х, у - 0.5) - - (х, у + 0.5) - - (-*, -у + 0.5) - - cycle. (Здесь предполагается, что х > 0.) Соответствующий макрос сам по себе довольно прост, но интересен как пример необычного использования операций над путями и перьями. Разобьем перо на две части — верхнюю и нижнюю. Нижнюю половину сдвинем на 0.5 пикселя вниз, а верхнюю — на столько же вверх. Выясняется, что точки раздела между половинами совпадают с крайней правой и крайней левой вершинами пера. Ммм-мм-м-да! Тут у нас может возникнуть проблема выбора, если крайняя левая или крайняя правая точка не является единственной. А что, если мы попробуем построить перо 'taller taller р' ? К счастью, METAFONT не смущает тот факт, что соответствующий многоугольник может иметь три последовательные вершины, лежащие на одной прямой. Поэтому мы можем совершенно свободно выбрать любую крайнюю правую и любую крайнюю правую точки.
310 Приложение D: Нестандартные приемы Возникает вопрос: как найти крайнюю правую и крайнюю левую точки? Конечно, чтобы определить все вершины многоугольника, мы можем использовать команду makepath, а затем найти среди этих вершин те, у которых координата х минимальна и максимальна. Однако макрос будет более эффективным (и интересным), если мы используем для этой цели команду 'directiontime' или команду 'penoffset'. Попробуем сначала вариант с 'directiontime': vardef taller primary p = save r, n, t, T; path r; r = tensepath makepath p; n = length r; t = round directiontime up of r; T = round directiontime down of r; if t>T: t := t-n; fi makepen(subpath(T-n,t) of r shifted .5down —subpath(t,T) of r shifted .5up — cycle) enddef; Путь, полученный применением команды makepath, имеет контрольные точки, совпадающие со смежными с ними вершинами. Такой путь не годится для использования в команде directiontime. (Дело в том, что если какая-либо ключевая точка совпадает с результатом применения к ней же команды 'precontrol' или 'postcontrol', то "скорость" в этой точке равна нулю. А команда directiontime предполагает, что в точке, где скорость равна нулю, путь имеет все направления сразу.) Поэтому мы использовали команду tensepath. Это почти решает задачу, в предположении, что значения t иТ иногда должны округляться до целых. Но этот метод не срабатывает в случае такого пера, как penspeck, в котором точки расположены очень близко друг к другу, так как в таких случаях команда tensepath работает не лучше команды makepath. Более того, даже если мы сможем получить из р подходящий путь (например, масштабировав его), то столкнемся с проблемой нестабильности вычислений в случае пера penrazor, у которого соответствующий многоугольник делает поворот на 180°. Операции с ультратонкими перьями иногда затруднительны, поскольку соответствующие многоугольники могут иметь более двух вершин. Например, вращение заготовок перьев вида 'makepen(/e/£ .. origin .. right .. cycle)' довольно проблематично. Использование команды 'penoffset' дает более стабильные результаты, поскольку в этой операции используется свойство выпуклости многоугольника. Самое эффективное с точки зрения скорости решение имеет вид: vardef taller primary р = save q, r, n, t, T; pen q; q = p; path r; r = makepath q; n = length r; t = round xpart(r intersectiontimes penoffset up of q); T = round xpart(r intersectiontimes penoffset down of q); if t>T: t := t-n; fi makepen(subpath(T-n,t) of r shifted .5down —subpath(t,T) of r shifted .5up — cycle) enddef; (Аргумент p копируется в q, если он является заготовкой пера; это значит, что превращение заготовки пера в готовое перо будет происходить не три раза, а всего один.) 9. Полиномы Бернштпейна. Чтобы выполнить последний трюк, мы попробуем расширить правила синтаксиса системы METAFONT так, чтобы можно было использовать обобщенную операцию помещения в промежуточное положение, записываемую как 4[ui,... ,ttn]', где п > 2. (Подобные обозначения для случая п = 3 и 4 были введены в главе 14, когда мы рассматривали подпути, соответствующие дробным значениям.) Если п > 2, то тождество t[ui,...,Un] = t[t[uiy.. .,Un-i],t[u2,...,Un]] есть рекурсивное определение функции t[ui,..., un]. Можно показать, что i[ui,...,un] = t[t[ui,u2],...<,t[un-.i,Un]]
Приложение D: Нестандартные приемы 311 — альтернативное определение, эквивалентное первому. (Действительно, имеем полином Бернштейна порядка п — I.) Задача состоит в том, чтобы изменить смысл квадратных скобок так, чтобы в соответствии с только что приведенными формулами значение такого выражения, как '1/2[а,6, с, d\\ было равным .125а + .3756 4- .375с + 125d. Но при этом смысл квадратных скобок в других примитивных конструкциях, таких как *х[п]' и 'path р[][]а', должен остаться прежним. Кроме того, нужно иметь возможность использовать скобки внутри скобок. Прежде, чем ознакомиться с предложенным ниже решением, читателю стоит попытаться решить эту задачу самостоятельно. Может быть, вам удастся найти более простое решение. def lbrack = hide(delimiters []) lookahead [ enddef; let [[[ = [; let ]]] = ]; let С = lbrack; def lookahead(text t) = hide(let [ = lbrack; for u=t, hide(n. := 0; let switch. = first.): switch, u; endfor) if n_<3: [[[t]]] else: Bernshtein n. fi enddef; def first, primary u = if numeric u: numeric u. [[[]]]; store, u elseif pair u: pair u_[[[]]]; store, u fi; let switch. = store, enddef; def store, primary u = u_[[[incr n_]]] := u enddef; primarydef t Bernshtein nn = begingroup for n=nn downto 2: for k-1 upto n-1: u.[[[k]]]:=t[[[u.[[[k]]],u.[[[k+l]]] ]]]; endfor endfor u_[[[l]]] endgroup enddef; Здесь самый тонкий момент — это использование пустого (списка цикла) с целью избавиться от от пустых текстовых аргументов. Поскольку METAFONT вычисляет значения всех выражений в цикле for до того, как считать текст цикла, и поскольку переменные 'п.' и 'и.' используются в отсутствие рекурсии, нет необходимости сохранять их значения, даже если скобки находятся внутри скобок. Используя этот прием, мы, безусловно, сильно замедляем работу системы METAFONT в тех местах, где используются квадратные скобки, так что он представляет для нас интерес с чисто познавательной точки зрения. Но, похоже, он работает во всех случаях, за исключением случаев с формулами, содержащими ']]' (две последовательные квадратные скобки). Последняя лексема, которую система METAFONT базового формата раскладывает в последовательность '] ]', не раскладывается, когда команда lookahead считывает свой текстовый аргумент. Поэтому пользователю нужно помнить о необходимости вставлять пробел между последовательными квадратными скобками. Их трюки и уловки свели меня с ума, Меня втащили внутрь — вот все, что помню я. — РОБЕРТ БЕРНС, Веселый нищий (1799) У каждый дом иметься сфой грязная угол. — АНДЕРСОН И КАНДАЛЛ, Пословицы и поговорки Ямайки (1927)
Примеры
Приложение Е: Примеры 313 Мы с вами видели множество отдельных примеров букв и их фрагментов. Теперь давайте соберем все эти примеры воедино. На следующих двух страницах полностью расписан файл 'logo.mf', который генерирует буквы логотипа METAFONT. Файл довольно короткий, поскольку в логотипе используется всего семь букв, и эти буквы специально разработаны так, чтобы системе, чье имя они составляют, было легче их обрабатывать. Тем не менее этот файл полон, и в нем в упрощенной форме отражены все основные свойства "больших" шрифтов. Здесь есть все: и перевод в пиксельные единицы специальных величин, и определение подпрограмм, и программы для отдельных букв, и специальные соглашения о межсимвольных промежутках и промежутках между словами. Более того, программы для конкретных символов написаны столь аккуратно, что рисуемые ими буквы хорошо ложатся на растр даже в том случае, когда пиксели выходного устройства — не квадратные. Начиная с главы 4, мы постоянно изучали буквы логотипа ' METAFONT' с различных точек зрения, постепенно усложняя наши примеры по мере того, как лучше узнавали язык системы. Наконец-то мы можем отбросить все условности и рассмотреть настоящий, профессионально подготовленный файл logo .mf, в котором соединились все лучшие приемы, описанные ранее либо непосредственно в тексте, либо в ответах к упражнениям. При помощи файла logo.mf легко генерируется шрифт — для этого достаточно выполнить процедуры, описанные в главе 11. Например, шрифт logo 10, используемый в логотипе 'METAFONT' размером 10 пунктов, для устройства печати с низкой разрешающей способностью можно получить, запустив METAFONT и подав на вход следующую командную строку: \mode=lowres; input logo10 Здесь logolO.mf — параметрический файл, описанный в указанной главе. Наклонный вариант 'METAFONT* генерируется посредством ввода параметрического файла logosllO.mf, в котором говорится всего лишь У, 10-point slanted METAFONT logo slant := 1/4; input logo10 Параметр slant влияет на значение currenttransform, как описано в главе 15. На самом деле в файле logo.mf широкие возможности метадизайна не используются в полной мере, поскольку требуется лишь несколько форм логотипа METAFONT. Но все же возможно получить интересные варианты этого логотипа. Например, при использовании параметрических файлов font.size 30pt#; ht#:=25pt#; xgap#:=1.5pt#; u#:=3/9pt#; s#:=l/3pt#; o#:=2/9pt#; px#:=lpt#; slant:=-1/9; получаем, соответственно font_size 10pt#; ht#:=6pt#; xgap#:=2pt#; u#:=4/3pt#; s#:=-2/3pt#; o#:=l/9pt#; px#:=l/3pt#; и r^xl "F=} F=CZ>r-r
314 Приложение Е: Примеры 7, Routines for the METAFONT logo, as found in The METAFONTbook 7, (logolO.mf is a typical parameter file) mode_setup; if unknown slant: slant:=0 else: currenttransform:= identity slanted slant yscaled aspect_ratio fi; ygap#:=(ht#/13.5u#)*xgap#; ho#:=o#; leftstemloc#:=2.5u#+s#; barheight#:=.45ht#; py#:=.9px#; define_pixels(s,u); define_whole_pixels(xgap) ; define_whole_vertical_pixels(ygap); define_blacker_pixels(px,py); pickup pencircle xscaled px yscaled py; logo_pen:=savepen; define_good_x_pixels(leftstemloc); define_good_y_pixels(barheight); define_corrected_pixels(o); define_horizontal_corrected_pixels(ho); def beginlogochar(expr code, unit_width) = beginchar(code,unit_width*u#+2s#,ht#,0); pickup logo_pen enddef; def super_half(suffix i,j,k) = draw z.i{0,y.j-y.i} (.8[x.j,x.i],.8[y.i,y.j]){z.j-z.i} z.j{x.k-x.i,0} (.8[x.j,x.k],.8[y.k,y.j]){z.k-z.j} z.k{0,y.k-y.j} enddef; beginlogochar("M",18); xl=x2=leftstemloc; x4=x5=w-xl; x3=w-x3; yl=y5; y2=y4; bot yl=-o; top y2=h+o; y3=yl+ygap; draw zl—z2—z3—z4—z5; labels(1,2,3,4,5); endchar; beginlogochar("E",14); xl=x2=x3=leftstemloc; x4=x6=w-xl+ho; x5=x4-xgap; yl=y6; y2=y5; уЗ=у4; bot у1=0; top y3=h; y2=barheight; 7» vertical adjustment 7% horizontal overshoot 7% position of left stem 7% height of bar lines 7. vertical pen thickness
Приложение Е: Примеры 315 draw z6—zl—z3—z4; draw z2—z5; labels(1,2,3,4,5,6); endchar; beginlogochar("T",13); italcorr ht#*slant + .5u#; if .5w<>good.x .5w: change_width; fi lft xl=-eps; x2=w-xl; x3=x4=.5w; yl=y2=y3; top yl=h; bot y4=-o; draw zl—z2; draw z3—z4; labels(1,2,3,4); endchar; beginlogochar("A",15) ; xl=.5w; x2=x4=leftstemloc; x3=x5=w-x2; top yl=h+o; y2=y3=barheight; bot y4=bot y5=-o; draw z4—z2—z3—z5; super_half(2,1,3); labels(l,2,3,4,5); endchar; beginlogochar("F",14); xl=x2=x3=leftstemloc; x4=w-xl+ho; x5=x4-xgap; y2=y5; y3=y4; bot yl=-o; top y3=h; y2=barheight; draw zl—z3—z4; draw z2—z5; labels(1,2,3,4,5); endchar; beginlogochar("0",15); xl=x4=.5w; top yl=h+o; bot y4=-o; x2=w-x3=good.x(1.5u+s); y2=y3=barheight; super_half(2,1,3); super_half(2,4,3); labels(1,2,3,4); endchar; beginlogochar("N",15); xl=x2=leftstemloc; x3=x4=x5=w-xl; bot yl=bot y4=-o; top y2=top y5=h+o; y3=y4+ygap; draw zl—z2—z3; draw z4—z5; labels(1,2,3,4,5); endchar; ligtable "T": "A" kern -.5u#; ligtable "F": "0" kern -u#; font_quad:=18u#+2s#; font_normal_space:=6u#+2s#; font_normal_stretch:=3u#; font_normal_shrink:=2u#; font_identifier:="MFL0G0" if slantoO: & "SL" fi; font_coding_scheme:="AEFMNOT only";
316 Приложение Е: Примеры В файле logo. mf новыми для нас являются только две последние строки, в которых определяются идентификатор шрифта — 'font-identifier' и его схема кодировки — 'font.coding-scheme\ Все остальное мы уже обсуждали ранее в этой книге. Идентификатор и схема кодировки дают дополнительную информацию о шрифте и рассматриваются в приложении F. Кроме того, для буквы 'Т' мы указали величину курсивной поправки, поскольку это последняя буква в логотипе %iETRFONT\ Программа для полноценного шрифта будет отличаться от программы для этого простенького логотипа главным образом количественно. В ней будет намного больше параметров, подпрограмм, символов, лигатур и кернений, а также множество мелких деталей. Но будут в ней и качественные отличия, призванные облегчить создание, тестирование и изменение символов, поскольку всякое большое дело требует хорошей организации. В оставшейся части этого приложения объясняется на конкретном примере, как эта программа может быть устроена. Мы рассмотрим ряд дополнительных подпрограмм, которые очень помогли автору при разработке семейства шрифтов Computer Modern. Целиком, без купюр программы для шрифтов Computer Modern приведены в томе Е серии Computers & Typesetting] но поскольку эти шрифты разрабатывались и улучшались на протяжении долгого времени, программы для них довольно сложны. Чтобы не отвлекаться на несущественные технические детали, эти программы приведены в упрощенном виде, что позволит нам сконцентрироваться на самых важных моментах. Рассмотренные выше простые шрифты, используемые для набора логотипов, генерируются файлами двух типов: параметрическими, такими как logolO.mf, и файлом- программой logo.mf. Шрифты Computer Modern, будучи более сложными, генерируются файлами четырех типов. Во-первых, это параметрические файлы, такие как 'cmrlO.mf', в которых задаются значения специальных величин, соответствующих тому или иному размеру и начертанию; во-вторых, это файлы-драйверы, такие как 'roman.mf', в которых содержатся команды, выполняемые непосредственно в процессе генерирования шрифта; в третьих, это файлы-программы, такие как 'punct.mf', содержащие программы для отдельных символов; и, наконец, имеется форматный файл 'cmbase.mf', содержащий подпрограммы и прочие макросы, которые используются системой повсеместно. Наш пример с логотипом можно подогнать под эту более общую схему, вынеся программы для символов в отдельный файл-программу 'METAFON.mf', а все вводные определения поместив в форматный файл 'logobase.mf', который будет иметь вид: У. Base file for the METAFONT logo logobase:=l; */, when logobase is known, this file has been input def font_setup = if unknown slant: slant :=0 else: cur rent transforms (определение точно такое, как и ранее) define_corrected_pixels(o); define_horizontal_corrected_pixels(ho); enddef; Кроме того, в этом файле должны быть определены макросы beginlogochar и super_half. Нам останется составить файл-драйвер logo.mf, который будет иметь следующий вид: У. Driver file for the METAFONT logo if unknown logobase: input logobase fi mode.setup; font.setup; input METAFON ligtable "T": "A" kern -.5u#; У, establish pixel-oriented units '/, generate the characters и так далее, в точности повторяя предыдущий вариант этого файла.
Приложение Е: Примеры 317 Вообще, параметрический файл вызывает файл-драйвер, который, в свою очередь, вызывает один или несколько файлов-программ. Форматный же файл содержит макросы, которые определяются в самом начале сеанса и могут использоваться во всех вышеупомянутых файлах. Можно составить несколько файлов-драйверов, в каждом из которых будет использоваться свой набор файлов-программ. Например, для семейства Computer Modern составлены файлы-драйверы 'roman.mf' и 'italic .mf'. Они используют один файл- программу punct.mf для генерирования знаков препинания, но разные файлы-программы для генерирования строчных букв. Символы разбиты на группы и помещены в отдельные файлы-программы так, что они могут использоваться разными драйверами. Параметрические файлы для семейства Computer Modern не всегда в точности следуют соглашениям, устанавливаемым файлом logo 10. mf. Вот, к примеру, как выглядят первые и последние строки файла cmrlO.mf: У, Computer Modern Roman 10 point if unknown cmbase: input cmbase fi font.identifier "CMR"; font.size 10pt#; u#:=20/36pt#; '/, unit width serif_fit: =0pt#; '/, extra sidebar near serifs letter_fit:=0pt#; '/, extra space added to all sidebars serifs:-true; '/« should serifs and bulbs be attached? monospace :=false; '/• should all characters have the same width? generate roman '/, switch to the driver file Основные отличия заключаются в следующем: (1) В начало файла вставлена специальная команда, проверяющая, был ли загружен файл cmbase.mf. Этот форматный файл содержит такие важные элементы, без которых правильное выполнение программы просто невозможно; например, в файле cmbase переменные 'serifs' и 'monospace1 объявляются как переменные типа boolean, так что такие присваивания, как 'serifs := true', будут корректными. (2) Идентификатор font-identifier определяется не в файле-драйвере, а в параметрическом файле. (3) В последней строке вместо 'input' указано 'generate'; команда generate определяется в форматном файле как синоним команды input, но ей придается несколько иной смысл подпрограммами, которые мы рассмотрим позже. (4) В параметрическом файле теперь отсутствует завершающая команда 'end'. Вот как выглядит (в сильно упрощенном виде) драйвер roman.mf: 7, The Computer Modern Roman family of fonts mode.setup; font.setup; input romanu; '/, upper case (majuscules) input romanl; */, lower case (minuscules) input romand; '/, numerals input punct; '/♦ punctuation marks font.slant slant; if monospace: font„quad 18u#; font_normal_space 9u#; '/, no stretching or shrinking else: font_quad 18u#+41etter_fit#; font_normal_space 6u#+21etter_fit#; '/, interword spacing font_normal_stretch 3u#; '/, with ''glue'* font_normal_shrink 2u#; input romlig; '/, f ligatures ligtable "f": "i" =: oct"014'\ "f" =: oct"013", "Iм =: oct"015", ,,,и kern u#, "?" kern u#, "!•• kern u#;
318 Приложение Е: Примеры ligtable oct"013": "i" =: oct"016\ "1" =: oct"017\ У. ffi and ffl ",M kern u#, ••?'• kern u#, "!" kern u#; ligtable "-": ••-" =: oct"173"; X en dash ligtable oct"173": "-" =: oct"174"; У. em dash ligtable "'": »'" =: oct"134"; У, open quotes ligtable "'": '■»•• =: oct"042", У. close quotes "?" kern 2u#, "!" kern 2u#; fi; bye. В таком шрифте, как cmttlO, где промежутки между буквами одинаковы, все символы имеют ширину ровно 9ti#. Для шрифтов cmrlO и cmttlO используется один и тот же драйвер roman. Но в случае, когда этот драйвер используется для генерирования шрифта с одинаковыми межбуквенными промежутками, он опускает все лигатуры и изменяет величину промежутка между словами. Файлы-программы для шрифтов семейства Computer Modern используют соглашения, несколько отличающиеся от соглашений базового формата METAFONT. Вот, к примеру, программы для двух простейших знаков препинания — точки и тире: cmchar "Period"; numeric dot_diam#; dot_diam# = if monospace: 5/4 fi dot_size#; define_whole_blacker_pixels(dot.diam); begincharC . " ,5u#,dot_diam#,0) ; adjust_fit(0,0); pickup fine.nib; posl(dot_diam,0); pos2(dot_diam,90); xll=good.x(xll+.5v-xl); bot y21=0; zl=z2; dot(l,2); У, dot penlabels(l,2); endchar; iff not monospace: cmchar "Em dash"; beginchar(oct"174",18u#,x_height#,0); italcorr .61803xjieight#*slant + .5u#; adjust.fit(letter.fit#,letter.fit#); pickup crisp.nib; posl(vair,90); pos2(vair,90); ylr=y2r=good.y(ylr+.61803h-yl); 1ft xl=-eps; rt x2=w+eps; filldraw stroke zle—z2e; У, crossbar penlabels(l,2); endchar;
Приложение Е: Примеры 319 Новыми структурными особенностями в этой программе являются: (1) 'cmchar' — команда, присутствующая в начале каждой программы для отдельного символа; (2) конструкция 'iff (булево выражение):', стоящая перед командой cmchar в случае, когда символ должен генерироваться только тогда, когда данное булево выражение истинно; (3) команда 'adjust.fit', которая может изменять величину промежутка слева и/или справа от данного символа; (4) перья, которые называются 'fine.nib' и 'crisp.nib'] (5) новые макросы 'роз\ 'doV и 'stroke', которые мы обсудим ниже. Форматный файл cmbase.mf начинается со следующих строк: У, The base file for Computer Modern (a supplement to plain.mf) cmbase:=l; '/, when cmbase is known, this file has been input let cmchar ■ relax; */, 'cmchar* should precede each character let generate e input; */, 'generate* should follow the parameters newinternal slant, superness, ••• '/, purely numeric parameters boolean serifs, monospace, ••• '/, boolean parameters Смысл этих строк достаточно прозрачен. Хотя команда cmchar определяется как синоним команды relax, которая не делает ничего, определение cmchar впоследствии изменяется вспомогательными подпрограммами-утилитами, которые будут приведены ниже; это оказывается удобным при разработке, тестировании и эксплуатации символов. Следующие несколько строк файла cmbase более замысловаты. В них используется условная конструкция с командой 'iff', которая позволяет "проскакивать" ненужные символы. let semi. = ;; let colon. = :; let endchar_ = endchar; def iff expr b = if b: let next. = use.it else: let next. = lose.it fi; next, enddef; def use.it = let : = restore.colon; enddef; def restore.colon = let : = colon.; enddef; def lose.it = let endchar = fi; inner cmchar; let ; = fix. semi. if false enddef; def fix. = let ; = semi.; let endchar = endchar.; outer cmchar; enddef; def always.iff = let : = endgroup; killboolean enddef; def killboolean text t = use.it enddef; outer cmchar; (В подпрограмме lose.it предполагается, что программа для каждого символа заканчивается на 'endchar;'.) Вероятно, самое интересное в формате cmbase — это метод, позволяющий регулировать величину отступа от краев каждого символа. Величину пустого пространства между символом и левым и правым краями его ограничивающей рамки можно менять, не сдвигая самого рисунка и не изменяя значение ширины, указанное в команде beginchar. Для этого после команды beginchar и необязательной команды italcorr в программе для символа из шрифта семейства Computer Modern достаточно указать: adjust.fit ((поправка к отступу слева), (поправка к отступу справа)); Поправки к отступам от краев задаются в "настоящих", точных единицах измерения. По сути, подпрограмма adjust.fit добавляет справа и слева дополнительные пробелы, соответствующие величине поправок. Кроме того, ко всем поправкам отступа неявно добавляется специальная внутренняя величина "letter-fit #". В программе для символа "." указано 'adjust.fit(0,0)'; это значит, что добавляется только величина letter-fit. А в программе для тире указано 'adjust_fit(fetter_/i*#, letter-fit #)\ поэтому величины отступа от краев увеличиваются на 2letter-fit с каждой
320 Приложение Е: Примеры стороны. Общая ширина символа "тире", таким образом, составляет 18u# + 4letter~fit# (что равно одному em — величине font _ quad, заданной в файле-драйвере roman). В программе для строчной буквы 'b' из файла romanl.mf используется команда 'adj ust .fit (seri/_/i$#, 0)'; она добавляет слева пробел, равный величине параметра serif-fit, чтобы компенсировать возможное наличие у этого символа засечки слева. В случае шрифта cmrlO величина serif-fit равна нулю, для рубленых шрифтов она имеет отрицательное значение, а для шрифтов с очень длинными засечками эта величина положительна. Команда adjust.fit удобна тем, что задает дополнительные свойства символа и на что больше не влияет. Программа может быть написана так, как если бы на левом краю отступ был равен 0, а на правом — iu; впоследствии отступы можно отрегулировать, не внося в программу изменений, касающихся собственно символа. Существует две версии подпрограммы adjust.fit. Одна из них предназначена для обычных шрифтов, а вторая — для шрифтов с равными промежутками между символами. Обе они несколько усложнены наличием элемента shrink-fit, смысл которого мы объясним позже; а пока давайте просто считать, что shrink.fit = 0. Вот как выглядит подпрограмма в случае обычного шрифта: def normal_adjust_fit(expr left_adjustment,right.adjustment) « 1 :- -hround(left_adjustment*hppp)-letter_fit; interim xoffset := -1; charwd := charwd+21etter_fit#+left_adjustment+right_adjustment; r := l+hround(charvd*hppp)-shrink_fit; w := r-hround(right_adjustment*hppp)-letter_fit; enddef; Переменным /иг присваиваются значения, равные текущим координатам границ символа, выраженным в пикселях. Таким образом, ограничивающая рамка в базовом формате METAFONT имеет координаты 0 < х < и>, а в формате Computer Modern — координаты I < х < г. Округление выполняется очень аккуратно, так что поправки к отступу всегда будут выражаться правильными соотношениями. Следует отметить, что величина w была пересчитана заново. Это значит, что adjust.fit может влиять на процесс оцифровки (мы надеемся) лучшим образом. В случае шрифта с равными промежутками между символами подпрограмма adjust.fit изменяет параметр и, определяющий единичную ширину так, чтобы суммарная ширина символа после внесения поправки была равна некоторой константе. Точно такие же поправки вносятся в такие параметры, как jut — номинальную длину засечки. Ширина всех символов в шрифте с равными промежутками будет равна mono-charwd# в точных единицах, и mono-charwd в пикселях. Курсивная поправка для всех символов будет равна mono-charic&. def mono_adjust_fit(expr left.adjustment,right.adjustment) = numeric expansion.factor; mono_charwd# = 21etter_fit# + expansion.factor*(charwd+left_adjustment+right_adjustment); forsuffixes $=u,jut, ••• : $ := $.#*expansion_factor*hppp; endfor 1 := -hround(left_adjustment*expansion_factor*hppp)-letter_fit; interim xoffset := -1; r := l+mono_charwd-shrink_fit; w := r-hround(right_adjustment*expansion_factor*hppp)-letter_fit; charwd := mono_charwd#; charic := mono_charic#; enddef; Автору пришлось немало потрудиться, чтобы найти правильный вид этой подпрограммы. Вычислений величины xoffset в подпрограмме adjust.fit вполне достаточно, чтобы при выводе символа он был сдвинут на нужное расстояние. Нам нужно только позабо-
Приложение Е: Примеры 321 титься о том, чтобы получить нужную ширину символа в пикселях, и в формате cmbase это достигается путем определения extra.endchar := extra_endchar&"r:=r+shrink_fit;w:=r-l;"; Больше никаких изменений в подпрограмму endchar базового формата М ETA- FONT вносить не требуется. Но нужно переопределить команды makebox и maketicks, чтобы они показывали уже поправленную ограничивающую рамку. Для наклонных шрифтов удобно изменить команду makebox так, чтобы она еще и наклоняла рамку и рисовала ряд вертикальных линий, отстоящих друг от друга на равные единичные промежутки. Эта своеобразная сетка очень помогает при разработке символа. Кроме того, рисуется несколько горизонтальных линий: def makebox(text rule) = for y=0, asc.height ,body_height, x_height,bar_height, -desc.depth,-body.depth: rule((l,y)t_, (r,y)t_) ; endfor 7, horizontals for x=l,r: rule((x,-body_depth)t_, (x,body_height)t_) ; endfor 7, verticals for x=u*(l+floor(l/u)) step u until r-1: rule((x,-body_depth)t_, (x,body_height)t..) ; endfor % more verticals if charicOO: rule((r+charic*pt,h.o_),(r+charic*pt,.5h.o_)); fi 7. italic correction enddef; def maketicks(text rule) = for y=0,h.o_,-d.o_: rule((l,y),(l+10,y)); rule((r-10,y) , (r,y)) ; endfor 7. horizontals for x=l,r: rule((x,10-d.o_),(x,-d.o_)); rule((x,h.o_-10), (x,h.o_)); endfor 7. verticals if charicOO: rule((r+charic*pt,h.o_-10), (r+charic*pt,h.o_)) ; fi 7. italic correction enddef; (Примерами использования обновленной подпрограммы makebox являются иллюстрации к символам "точка" и "тире", приведенные ранее в этом приложении, а также иллюстрации из главы 23.) Подпрограмма change.width базового формата METAFONT также должна быть обобщена: def change.vidth = if not monospace: if r+shrink_fit-1 = floor(charwd*hppp): w else: w := w-1; r := r-1; fi fi enddef; 7. change width by +1 or -1 := w+1; r := r+1; Подпрограмма font_setup, определенная в формате Computer Modern, вызывается в начале каждого файла-драйвера. Она переводит точные единицы измерения в пиксельные; кромо того, font.setup вычисляет дополнительные величины, важные для шрифта в целом. Определение очень длинное, поэтому приведем лишь основные моменты: def font_setup = define_pixels(u,jut, ■•• ); define_vhole_pixels(letter_fit,fine,crisp, ••• ); define_whele_vertical_pixels(body_height,cap_height, ••• ); define_whole_blacker_pixels(hair,stem,curve, ••• ); define_whole_vertical_blacker_pixels(vair,slab, • • • ); define_corrected_pixels(o, •••);
322 Приложение Е: Примеры if monospace: mono_charvd# := 9u#; define.vhole.pixels(mono.charvd); mono„charic# :« max(0,body_height#*slant); let adjust_fit * mono_adjust_fit; else: let adjust_fit s normal.adjust.fit; fi lowres.fix(stem,curve) 1.2; (Инициализация кончиков перьев, см. ниже) currenttransform:«identity slanted slant yscaled aspect.ratio scaled granularity; shrink.fit :« l+hround(21etter_fit#*hppp)-21etter_fit; if not string mode: if mode <= smoke: shrink.fit := 0; fi fi enddef; Если letter-fit# = 0, то величине 'shrink-fit' присваивается значение 1; в противном случае shrink-fit присваивается значение 0, 1 или 2, в зависимости от того, до какого целого будет округлена величина letter-fit. Эта величина вычитается из величины w перед тем, как рисуется каждый символ шрифта. Опыт показывает, что за счет этого значительно улучшается читабельность текста, набранного этим шрифтом при среднем и низком разрешении. Большинство символов в шрифтах семейства Computer Modern нарисованы при помощи команды filldraw, которая представляет собой сочетание заливки контура с рисованием фиксированным пером. Для облегчения такого рисования-заливки в формат cmbase включено несколько специальных макросов, в частности, lpos' и 'stroke': vardef pos€#(expr b,d) ■ (xC#r-xC#l,yC#r-yC#l)=(b-currentbreadth,0) rotated d; xC#=.5(x«#l+x«#r); y«#=.5(yC#l+yC#r) enddef; vardef stroke text t = forsuffixes e-l,r: path_.e:=t; endfor path_.l — reverse path_.r — cycle enddef; Макрос pos похож на penpos; единственное отличие его состоит в том, что величина currentbreadth вычитается из общей ширины пера. (Ср. с программой для левой скобки из главы 12.) Подпрограмма stroke — это упрощенная версия подпрограммы penstroke, такая, что penstroke эквивалентно 'fill stroke \ если указанный путь — не циклический. Величина currentbreadth получается добавлением в макрос 'numeric-pickupj базового формата METAFONT новой строчки вида: if known breadth.[q]: currentbreadth:=breadth_[q]; fi Макрос clear.pen.memory переопределяется так, что его вторая строчка теперь имеет следующий вид: numeric pen_lft_[] ,pen_rt_[] ,pen_top_[] ,pen_bot_[] .breadth. [] ; Как мы вскоре увидим, соответствующие значения breadth., устанавливаются подпрограммой font.setup. В программах для точки и тире использовались команды 'pickup fine.nib* и 'pickup crisp.nib\ Указанные в них перья определяются подпрограммой font.setup следующим образом: clear_pen_memory; forsuffixes $ = fine,crisp, ••• : $.breadth :s $; pickup if $=0: nullpen else: pencircle scaled $; $ := $-eps fi; $.nib :=» savepen; breadth_[$.nib] := $; forsuffixes $$ = lft,rt,top,bot: shiftdef($.$$,$$ 0); endfor endfor
Приложение Е: Примеры 323 Например, если fine = 4, то подпрограмма устанавливает fine.breadth := 4, fine.nib := 1, fine := 4- eps, и 6readt/i-[l] := 4— eps. (Вычитаемое eps настолько мало, что, как правило, Ъ — currentbreadth > 0.) Кроме того, определяются четыре макроса fine.lft, fine.rt, fine.top и fine.bot, обеспечивающие доступ к границпам пера fine.nib в том случае, когда оно не является текущим пером. Эти четыре подпрограммы используют довольно замысловатый макрос shiftdef: def shiftdef(suffix $)(expr delta) = vardef $ primary x = x+delta enddef enddef; Ну вот, мы почти что разобрались с форматом cmbase, который отличается дополнительной организационной сложностью, присущей любой крупномасштабной разработке. Оставшаяся часть форматного файла содержит только определения таких подпрограмм, как serif и dot, которые отвечают за часто повторяющиеся элементы самих символов. Нет нужды приводить их здесь. Для того чтобы создать файл cm.base, содержащий информацию в бинарном виде, можно использовать элементарный по содержанию файл 'cm.mf': '/. This file creates 'cm.base', assuming that plain.base is preloaded input cmbase; dump. Кроме параметрических файлов, файлов-драйверов, файлов-программ и форматного файла, среди подпрограмм для шрифтов Computer Modern имеется несколько так называемых файлов-утилит, которые обеспечивают удобство при разработке новых и улучшении старых символов. В завершение мы рассмотрим содержимое этих файлов. Допустим, рассмотрев пробные оттиски, мы обнаружили дефекты в символах 'к' и 'S' и хотим их исправить. Вместо того чтобы работать со всем шрифтом, мы можем скопировать программы для этих двух символов (и только для них) во временный файл 'test.mf'. Затем мы запускаем METAFONT и подаем на ввод файл 'rtest.mf', в котором говорится следующее: У, try all characters on 'test.mf' using the parameters of cmrlO if unknown cmbase: input cmbase fi mode.setup; def generate suffix t = enddef; input cmrlO; font_setup; let echar = endchar; def endchar = echar; stop "done with char "ftdecimal charcodeft". " enddef; let iff ■ always_iff; input test; bye В результате мы получим пробные оттиски символов (k' and 'S', в которых используется набор параметров, соответствующий шрифту cmrlO. Обратите внимание на нехитрый прием, благодаря которому rtest может оставаться в нашем распоряжении и после ввода файла cmrlO, не позволяя драйверу roman вступить в права: команда 'generate' переопределяется так, что становится "безвредной". Более того, в rtest команда endchar переопределяется так, что METAFONT будет останавливаться и выводить каждый символ на экран перед тем, как перейти к следующему. Условие 'iff заменено на 'always.iff', так что проверка на истинность булева выражения будет производиться для каждого символа, даже если это выражение не определено; это облегчает копирование из файла-программы в тестовый файл и обратно, поскольку условная конструкция с iff не затрагивается. Если в самом начале сеанса работы вы укажете 'Nmode^lowres; input rtest', то получите шрифт rtest с низким разрешением и такими же параметрами, как у шрифта cmrlO, но содержащий лишь те символы, которые находятся в тестовом файле. Выйдя из этого режима работы, вы, как обычно, попадете в режим proof.
324 Приложение Е: Примеры Точно так же, имеются псевдодрайверы ttest.mf (для шрифта cmttlO), btest.mf (для cmbxlO) и т.п.; они дают возможность проверять подозрительные символы с разными установками параметров. Существует также псевдодрайвер ztest.mf, который вводит набор параметров из временного файла 'z.mf', содержащего необходимые специальные параметры. (Если файл z.mf не существует, то у вас будет возможность указать другой параметрический файл в интерактивном режиме.) Более сложно устроенный псевдодрайвер '6test.mf' позволяет проверять одновременно до шести наборов параметров и просматривать результаты на экране, как было проиллюстрировано в главе 23. Вот программа, которая дает нам эту удивительную возможность: 7, try all characters on 'test.mf' using six different sets of parameters if unknown cmbase: input cmbase fi mag=.5; */, the user can override this equation mode_setup; let mode_setup=\; boolean running; def abort = hide(scrollmode; running := false) enddef; def pause = stop "done with char "ftdecimal charcodeft". " enddef; let iff = always_iff; def ligtable text t=enddef; def charlist text t=enddef; def extensible text t-enddef; string currenttitle; let semi = ;; let echar = endchar; let endchar = enddef; def cmchar expr s = currenttitle := s; let ; = testchar semi quote def chartext = enddef; def testchar = semi let ; = semi; running := true; errorstopmode; for k=l upto 6: if running: if known params[k]: scantokens params[k]; font.setup; currentwindow:=k; currenttitle ft ", "ft fontname[k]; chartext echar; fi fi endfor pause; enddef; string params[] ,fontname[] ; params[l] = "roman_params"; fontname[l] = "cmrlO"; params[2] = "sans.params"; fontname[2] = "cmssbxlO"; params[3] = "ital.params"; fontname[3] = "cmtilO"; params[4] = "tt_params"; fontname[4] = "cmttlO"; params[5] = "bold_params"; fontname[5] = "cmblO"; params[6] = "quote_params"; fontname[6] = "cmssqi8"; w_rows = floor 1/2 screen.rows; w.cols = floor 1/3 screen.cols; def open(expr k,i,j)= openwindow k from ((i-l)*w_rows,(j-l)*w_cols) to (i*w_rows,j*w_cols) at (-10,140) enddef; def openit = open(l,l,l); open(2,l,2); open(3,l,3); open(4,2,l); open(5,2,2); open(6,2,3); enddef; begingroup delimiters begintext generate; def makedef(expr s)(text t) = expandafter def scantokens s = t enddef; flushtext enddef; def flushtext suffix t = enddef; for k=l upto 6: if known params[k]:
Приложение Е: Примеры 325 makedef(params[к]) expandafter expandafter expandafter begintext scam tokens ("input "&fontname[k]); fi endfor endgroup; input test; bye Параметры переносятся из параметрических файлов в макросы при помощи приема, который обсуждался в начале приложения D. Затем команда cmchar переопределяется так, что весь текст программы для символа, подлежащего проверке, помещается в макрос chartext. Каждый из экземпляров chartext последовательно применяется к каждой из шести установок шрифта. Уже при использовании первого или второго набора параметров может обнаружиться настолько серьезная ошибка, что вам даже не захочется видеть, что будет в случае третьего, четвертого, пятого и шестого наборов. Например, когда файл test.mf содержит только что написанные программы для символов, некоторые уравнения могут быть пропущены или набраны неверно, так что результаты окажутся плачевными. В таком случае вы можете прервать работу программы и напечатать фразу 'I abort'. В подпрограмме 6test имеется макрос abort, который прерывает работу по окончании обработки текущего набора параметров и переходит непосредственно к следующему символу, пропуская оставшиеся непроверенными комбинации параметров. В файл test.mf может быть включен материал, который не является частью программы для какого-либо символа. Например, вы можете переопределить здесь подпрограммы форматного файла. Но только программы для самих символов (т.е. последовательности лексем ограниченные лексемами 'cmchar' и 'endchar;') являются объектами шестикратного повторения. Некоторые особенно крупные символы могут выводится на экран лишь частично, поскольку для их вывода при заданном увеличении попросту не хватает места. Вы можете уменьшить размеры всех символов, если в начале сеанса работы с METfiFONT напечатаете, скажем, 4\mag=l/3; input 6test'. Компьютер выдаст вам сообщение об ошибке, в котором будет говориться, что уравнение 'mag=. 5' несовместно. Но вы можете спокойно продолжить работу, так как получите именно то увеличение, которое вам нужно. Пример, поданый деянием, предпочтительней чтения моралей. — ДЖОН МЕРК, Праздник (С. 1400) Старые люди любят давать добрые советы, чтобы убедить самих себя в том, что они уже не могут подавать дурной пример. — ЛАРОШФУКО, Максимы (1665)
F Метрики шрифтов
Приложение F: Метрики шрифтов 327 Издательская система TeX предполагает, что используемые ею шрифты обладают определенным встроенным "интеллектом", то есть имеется дополнительная информация о шрифтах, которая серьезным образом влияет на процесс набора. Отсюда вытекают два следствия, (а) Процесс допечатной подготовки документов отличается гибкостью, поскольку число жестких соглашений самой системы TJrjX относительно невелико. (Ь) При разработке шрифта приходится выполнять дополнительную работу, чтобы объяснить системе TeX, что она должна делать. В этом приложении мы объясним, как справиться с задачей, упомянутой в пункте (Ь), чтобы затем использовать возможности, заявленные в пункте (а). Информация, используемая системой Т]еХ, заключена в компактных бинарных файлах, называемых метриками шрифтов (TJEJX Font Metric), или tf m-файлами. Наличием буквы V расширение 'tfm' обязано системе IfejX, но это всего лишь исторически сложившееся недоразумение, так как работать с tfm-файлами могут и другие программы компьютерной верстки. На самом деле эти файлы правильнее было бы называть просто "f m-файлами", но теперь уже поздно что-либо менять. METAFONT генерирует два вида бинарных файлов. Во-первых, это файлы с расширением *gf \ которые содержат оцифрованные изображения символов и дополнительную информацию, которая необходима драйверу печатающего устройства; они будут рассмотрены в приложении G. Второй тип — это tfm-файлы, которые содержат информацию о шрифте, используемую программами верстки, такими как TeX; эти файлы и являются предметом нашего рассмотрения. Такой файл генерируется, если на момент завершения выполнения задания внутренняя величина ifontmaking'> имеет положительное значение. (Обычно подпрограмма mode.setup базового формата METAFONT автоматически устанавливает нужное значение fontmaking.) В tfm-файле содержится информация о каждом символе, информация об определенных комбинациях символов и информация, касающаяся шрифта в целом. Мы рассмотрим все эти виды информации. Метрические данные о шрифте, в которых фигурируют физические единицы измерения, должны быть выражены в аппаратно-независимых, "точных" единицах. Если шрифт генерируется при разных разрешениях и разном увеличении, то все соответствующие tfm-файлы должны быть идентичными. Программа верстки, например, TeX, должна знать размеры ограничивающей рамки каждого символа. Например, если система TeX печатает слово 'box', она помещает первую букву 'Ь' внутрь маленькой рамки так, что пиксель, левый нижний угол которого в системе METAFONT имеет координаты (0,0), будет располагаться на базовой линии набираемой в данный момент строки, на левой границе рамки. (Для простоты мы предполагаем, что в момент, когда символ 'Ь' отправлялся на вывод, значения xoffset и yoffset были нулевыми.) Вторая буква, 'о', помещается во вторую маленькую рамку, смежную с первой. Поэтому мы должны объяснить системе TeX, какова должна быть ширина рамки для 'Ь\ Системе ТфС нужно также знать высоту и глубину рамки каждого символа. От них будет зависеть расположение диакритических знаков, если вы захотите набрать нечто вроде 'boxy'. Кроме того, эта информация позволяет предотвратить наложение символов в тех случаях, когда соседние строки содержат очень "высокие" или очень "глубокие" рамки. Всего для каждого символа приводится информация о четырех размерностях, выраженная в точных единицах (пунктах устройства печати): ■ charwd, ширина ограничивающей рамки. ■ charht, высота (над базовой линией) ограничивающей рамки. ■ chardp, глубина (вниз от базовой линии) ограничивающей рамки. Она выражается полжительным числом, если часть символа располагается ниже базовой линии, хотя соответствующие координаты у имеют отрицательные значения.
328 Приложение F: Метрики шрифтов ш charic, курсивная поправка для данного символа. Система TeX прибавляет эту величину к ширине рамки (с правой стороны) в двух случаях: (а) если пользователь прямо указывает на необходимость внесения курсивной поправки, печатая \/ непосредственно после символа; (Ь) если обособленный символ используется в математическом режиме работы, за исключением случая, когда он имеет нижний индекс и не имеет верхнего индекса. Так, курсивная поправка применяется к символу 'Р' в формулах 'Р(х)' и 'Р2', но не применяется в формуле 'Рп'; в формуле 'Р2' с ее помощью корректируется положение верхнего (но не нижнего) индекса. В программах для системы METAFONT базового формата вы указываете значения величин charwd, charht и chardp в команде beginchar, а значение величины charic (если оно положительное) — в команде italcorr. Но и beginchar, и italcorr являются макросами, а не примитивами системы METAFONT. Система METAFONT записывает значения внутренних величин charwd, charht, chardp и charic в тот момент, когда выполняется команда shipout. Абсолютные значения этих величин (а равно и других размерностей, которые будут упомянуты ниже) не должны превышать 2048pt#. В шрифте может содержаться до 256 кодов символов; при помощи оператора 'charexists' вы можете узнать, какие коды символов уже задействованы. Если два и более символов отправляются на вывод с одинаковыми кодами (но, возможно, с разными значениями расширения charext), то значения charwd, charht, chardp и charic последнего символа применяются ко всем символам. В одном шрифте допускается до 15 различных ненулевых высот символов, до 15 различных ненулевых глубин и до 63 различных ненулевых курсивных поправок. Если эти ограничения превышены, METAFONT заменит одно или несколько значений, изменив их как можно меньше, пока указанные условия не будут выполнены. В случае таких изменений выводиться соответствующее предупреждение; например, сообщение '(some charht values had to be adjusted by as much as 0.12pt)' означает, что вы указали слишком много разных ненулевых высот, но система METAFONT нашла возможность сократить их число до 15 или менее, изменив некоторые из них; при этом ни одна из величин не была изменена более чем на 0.12 пункта. Если максимальная величина такой поправки не превышает j$ pt, то предупреждение вообще не выводится. Следующий вид информации, необходимой системе TeX, касается пар смежных символов, которые набираются одинаковым шрифтом. Например, в слове 'box' система TeX сдвигает символ 'х' чуть ближе к символу 'о', а символ 'о' немного отодвигает от символа 'Ь', согласно информации из tf m-файла для шрифта, которым набран текст. Такая поправка к расположению символов называется кернингом. В противном случае (если бы эти символы располагались просто друг за другом в соответствии с их значениями charwd), то слово имело бы вид 'box', что выглядит несколько хуже. Точно так же, имеется разница между 'difference' и 'difference',* поскольку tfm-файл предписывает системе TeX заменять две следующие друг за другом буквы 'f' лигатурой 'ft7'. Информация о лигатурах и кернинге указывается в коротких и чрезвычайно простых программках, которые называются программами лигатур. Вот пример, который иллюстрирует большинство их особенностей (хотя и не является практическим примером): ligtable "f": "f" =: oct"013", "i" l=: octM020M, skipto 1; ligtable "o": "b": "p": "e" kern .5u#, "o" kern .5u#, "x" kern-.5u#, 1:: "!" kern u#; Эту последовательность инструкций можно расшифровать следующим образом: Дорогая система TeX! Когда ты будешь набирать букву 'Г данным шрифтом и следующий символ также будет принадлежать этому шрифту, пожалуйста, будь * Слово "difference" значит "разница", так что, как видите, иногда есть разница между разницей и разницей! — Прим. перге.
Приложение F: Метрики шрифтов 329 внимательна, так как тебе, возможно, придется выполнить специальные инструкции. А именно: если следующим символом будет еще одна буква Т, замени эти две буквы f символом с кодом oct"013" [т.е. символом 'ff']; если следующий символ — буква 4', то оставь Т, но замени эту Т на символ с кодом oct"020" [символ без точки V]; в противном случае отправляйся за дальнейшими инструкциями к метке '1::'. Когда будешь набирать символы 'о', ЧУ и 'р', то, если следующим входным символом будет 'е' или 'о', добавь между буквами полпробела; если же это будет буква 'х', то полпробела нужно удалить; если следующий символ — восклицательный знак, добавь целый пробел. Последняя инструкция относится также к восклицательному знаку после буквы Т (т.к. перед ней стоит метка '1::'). Двоеточие, следующее за кодом символа, "метит" начало программы лигатур и кернинга для этого символа, которая продолжается вплоть до конца утверждения. Двойное двоеточие обозначает "локальную метку"; команда skipto обозначает переход к следующей соответствующей локальной метке, которая должна встретиться до того, как будет описано еще 128 "лигатурных подстановок". Специальная метка 11: может использоваться для обозначения начала программы лигатур для невидимого "символа левой границы", который намеренно вставляется перед каждым словом; невидимый "символ правой границы", равный boundarychar, также намеренно вставляется после каждого слова, если значение boundarychar лежит между 0 и 255. Общие правила синтаксиса для программ лигатур имеют очень простой и предсказуемый вид, но мы все же должны привести их для полноты изложения: (команда ligtable) —У ligtable (программа лигатур) (необязательный переход) (программа лигатур) —> (лигатурная подстановка) | (программа лигатур) , (лигатурная подстановка) (необязательный переход) —> , skipto (код) | (пустое) (лигатурная подстановка) —> (код) (оператор лигатуры) (код) | (код) kern (числовое выражение) | (метка) (лигатурная подстановка) (оператор лигатуры) —У =: | I =: | | = :> | =:| | =: I > | 1 = :| | 1 = :1> | |-:|» (метка) —> (код) : | (код) :: | II: (код) —> (числовое выражение) | (выражение-цепочка) После округления до ближайшего целого (код) должен представлять собой число, заключенное между 0 и 255, включительно; или же он должен представлять цепочку длины 1, которая в этом случае выражает соответствующий ASCII-код (см. приложение С). Например, как "А", так и 64.61 указывают значение кода, равное 65. Вертикальные черточки справа или слева от '»:' означают, что система IfejX должна оставлять неизменным правый и/или левый символ в лигатуре. Дополнительные знаки '>' говорят системе TeX, что она должна перенести внимание на следующий символ и не производить более никаких операций, связанных с лигатурами и кернингом, с символом в текущей позиции. Предупреждение: Новички часто злоупотребляют кернингом. Лучше кернить не более половины всего, что сразу хочется. Заметным должно быть не наличие кернинга, а только его отсутствие. Кернинг, который выглядит хорошо в логотипах или заглавиях, часто ухудшает восприятие обычного текста, нарушая ритм чтения. Система TeX будет работать более эффективно, если вы упорядочите лигатурные подстановки в (программе лигатур) так, чтобы наиболее часто встречающиеся комбинации стояли в числе первых. Встретив первый такой "хит", TeX прекратит считывание программы лигатур. Несколько символов одного шрифта можно связать в устойчивую последовательность посредством команды char list. Например, в шрифте cmexlO посредством команды charlist ocf'OOO": oct"020": oct"022": octM040": oct"060"
330 Приложение F: Метрики шрифтов указывается последовательность левых скобок возрастающего размера, которые TeX использует в выделенных математических формулах. Следуя заданному списку, T$i подбирает подходящие по размеру разделители и диакритические знаки, а также устанавливает соответствие между знаками операций обычного размера, например, 'J^', и теми же зна" ками большего размера — 'V', используемыми в выделенных формулах. TJtjX строит большие разделители при помощи "растяжимых" символов, которые задаются указанием в команде extensible верхней, средней, нижней частей и повторяющегося символа. Так, растяжимая левая скобка шрифта cmexlO определяется командой extensible oct"060": oct"060", 0, ocflOO", oct"102"; она означает, что символ с кодом octM060M представляет собой растяжимый разделитель, верхняя часть которого есть сам этот символ, нижняя часть — символ номер octM100", а оставшаяся часть получается повторением символа номер oct"102" нужное число раз, пока не будет достигнут подходящий размер. В приведенном примере средняя часть отсутствует, но в случае с таких символов, как фигурные скобки, без средней части никак не обойтись. Нулевое значение в позиции верхней, средней или нижней части означает, что это место в конструкции не должен занимать никакой символ; но если нулевое значение стоит в последней позиции, то это значит, что повторяющимся будет символ номер ноль. Ширина растяжимого символа принимается равной ширине повторяющегося символа. Вот, например, как выглядят круглые скобки первых восьми размеров в шрифте cmexlO, которые система TeX выдает, если пользователь использует команду '\left(': ■«((((( Согласно приведенным выше примерам команд charlist и extensible, первые четыре скобки — это символы с кодами ocf'OOO", octH020", oct"022" и oct"040M. Остальные четыре скобки в верхней части имеют символ oct"060", в нижней — символ oct"100" и, соответственно, нуль, один, два и три экземпляра символа oct"102" посередине. Вот формальные правила синтаксиса: (команда charlist) —> charlist (помеченный код) (помеченный код) —У (код) | (метка) (помеченный код) (команда extensible) —> extensible (метка)(четыре кода) (четыре кода) —► (код) , (код) , (код) , (код) Заметим, что (метка) может присутствовать в командах ligtable, charlist и extensible. При этом присутствие метки в одной из этих команд исключает возможность присутствия ее в двух других: каждый код может использоваться в качестве метки только один раз. Таким образом, символ, имеющий программу лигатур/кернинга, уже не может быть ни растяжимым, ни присутствовать в списке команды charlist где-либо, за исключением последней позиции. Последний тип информации, содержащейся в tfm-файле, относится к шрифту в целом. Она состоит из данных двух видов — числовых данных и байтов, которые указываются в командах fontdimen и headerbyte, согласно следующим правилам синтаксиса: (команда headerbyte) —У headerbyte (числовое выражение) : (список байтов) (команда fontdimen) —У fontdimen (числовое выражение) : (список чисел) (список байтов) —У (код) | (список байтов) , (код) (список чисел) —У (числовое выражение) | (список чисел) , (числовое выражение)
Приложение F: Метрики шрифтов 331 Рассмотрение команд headerbyte мы отложим на потом, так как обычно можно обойтись и без них. Но команды fontdimen очень важны. Числовые параметры шрифта можно указать, например, посредством команды вида fontdimen 3: 2.5, 6.5, 0, 4х которая означает, что параметры с 3 по 6 имеют значения 2.5, 6.5, 0 и 4х, соответственно. Это те параметры, которые в TeX указываются как \f ontdimen3 thru \f ontdimen6. (Нумерация параметров ведется на старый лад, так что параметра \f ontdimenO не существует.) Первые семь параметров типа 'fontdimen' особенно важны, поэтому в базовом формате METAFONT имеется семь соответствующих макросов, которые позволяют задавать их по одному в виде следующих величин: ■ font.slant (\f ontdimenl) — величина наклона в расчете на один пункт; система 1ЁХ использует эту информацию для определения положения диакритических знаков. ■ font.normal.space (\f ontdimen2) — величина промежутка между словами. Если эта величина имеет нулевое значение, то все символы шрифта в математическом режиме будут рассматриваться как "изолированные", поэтому чаще будет вноситься курсивная поправка. ■ font_normal.stretch (\f ontdimen3) — растяжимость промежутка между словами, подробно описанная в книге Все про TeX. ■ font.normal.shrink (\fontdimen4) — сжимаемость промежутка между словами, подробно описанная в книге Все про TeX. ■ font.x.height (\f ontdimen5) — высота символов, для которых диакритические знаки размещаются правильным образом. Диакритический знак будет приподнят над символом на величину разности между значением charht этого символа и данным значением. Эта х-высота совпадает с единицей высоты, которая в системе TfcjX называется 'ex'. ■ font.quad (\f ontdimen6) — единица ширины, которая в TeX называется 'em'. ■ font_extra_space (\f ontdimen7) — величина дополнительного пробела, добавляемого к обычному пробелу мел^лу словами в конце предложений, зависящая от величины \spacef actor (см. книгу Все про TeX). По умолчанию все эти параметры имеют нулевое значение. В математических символьных шрифтах, используемых системой TeX, число параметров достигает 22 вместо обычных семи, а шрифты с математическим расширением требуют указания как минимум 13 параметров. В приложении G книги Все про TeX объясняется, почему так важены эти дополнительные параметры, которые отвечают за такие моменты, как, например, правильное размещение верхних и нижних индексов. Номинальный размер шрифта не является параметром, указываемым в команде 'fontdimen'. Он представляет собой внутреннюю величину системы METAFONT и записывается в выходной файл в числе первых байтов, как объясняется ниже. Когда пользователь системы TeX, используя команду 'at', запрашивает шрифт определенного размера, этот шрифт масштабируется на величину отношения заданного и номинального размеров. Например, номинальный размер шрифта cmrlO равен 10 pt; если пользователь системы ГЩХ запрашивает шрифт 'cmrlO at 15pt', то в результате он получает то же, что 'cmrlO scaled 1500' (или, в терминах базового формата METAFONT, шрифт cmrlO с увеличением mag=l .5). Что же реально обозначает номинальный размер шрифта? Эта величина не является строго определенной, поскольку между номинальным размером шрифта и любыми другими размерами шрифта, вообще говоря, может не быть никакой связи. Печатных дел мастера всегда оставляют место неопределенности, говоря о шрифте размера "10 пунктов", так как некоторые шрифты зрительно воспринимаются как большие по сравнению
332 Приложение F: Метрики шрифтов с другими, даже если горизонтальные и вертикальные размеры тех и других совпадают. Ситуация здесь примерно такая же, как с размерами одежды и обуви. Вообще, номинальный размер шрифта — это приблизительный размер элемента набора. Шрифт, имеющий больший номинальный размер, как правило, выглядит крупнее, чем шрифт с меньшим номинальным размером. Два шрифта одинакового номинального размера по идее должны хорошо сочетаться. К примеру, шрифты cmr9 и cmtt9 имеют номинальный размер 9 pt, хотя заглавные буквы шрифта cmtt9 чуточку меньше, как можно заметить, сравнив 'А' с СА\ Номинальный размер шрифта определяется величиной designsize, значение которой должно быть не меньше 1р£#. И, как и все размеры, указываемые в tfm-файле, значение designsize не должно превышать 2048pt#. Все другие значения автоматически заменяются на 128р£#. Система METAFONT учитывает значение величины designsize только в самом конце выполнения задания, так что вам не нужно устанавливать его перед выводом каждого символа. В конце задания, когда записывается tfm-файл, METAFONT проверяет, не превосходит ли абсолютное значение какой-либо размерности шрифта его номинального размера в 16 или более раз, поскольку это условие предусмотрено форматом tfm-файла. Так, например, если номинальный размер шрифта равен 10 pt, вы не можете получить символ шириной 160 pt и более. Если какие-то из метрических параметров шрифта окажутся слишком большими, METAFONT сообщит, сколько параметров вам нужно изменить. Команда header byte похожа на команду fontdimen, но в отличие от последней задает не числовые данные, а 8-битовые (коды). Например, команда headerbyte 33: 0, 214, 0, "с" означает, что байты с 33 по 36 во вводной части tfm-файла будут содержать значения 0, 214, 0 и 99. В первые четыре байта вводной части (с номерами 1-4) автоматически записывается значение контрольной суммы, если только вы специально не укажете другое значение по крайней мере для одного из этих байтов. (Это значение контрольной суммы будет соответствовать значению контрольной суммы в gf-файле, так что с помощью этой информации любая издательская система сможет определить, согласованы ли между собой используемые ею файлы.) Точно так же, в следующие четыре байта вводной части (с номерами 5-8) автоматически заносится значение номинального размера шрифта, умноженное на 220, если не задано иное значение. TeX просматривает только первые восемь байтов во вводной части tfm-файла, поэтому не нужно использовать команду headerbyte, если вы просто генерируете шрифт для системы Т)й}Х стандартного формата. Однако другим программам, считывающим tfm-файлы, может понадобиться больше вводной информации. Так, первоначально tfm- формат [разработанный Лайлом Рамшо (Lyle Ramshaw) в исследовательском центре Xerox Palo Alto] предполагал, что байты с 9 по 48 вводной части содержат информацию о кодировке шрифта (font_coding«scheme), а байты с 49 по 68 — информацию об идентификаторе шрифта (font>identifier). Номинальный размер шрифта также заносился в 72-ой байт. В "мире Xerox" каждый шрифт единственным образом определяется не именем соответсвующего файла, а идентификатором и номинальным размером. Схема кодировки — это просто комментарий, который дает дополнительную информацию о больших коллекциях шрифтов; обычно эта информация оказывается весьма полезной. Вот некоторые наиболее распространенные схемы кодировок: ТеХ text ТеХ math italic ТеХ typewriter text ТеХ math symbols XEROX text TeX math extension ASCII TeX extended ASCII PI GRAPHIC Цепочка, обозначающая кодировку шрифта, не должна содержать скобок.
Приложение F: Метрики шрифтов 333 Следующие макросы в случае необходимости можно использовать для перевода величин font.identifier и font-coding.scheme базового формата METAFONT в формат, предусматриваемый tf m-файлами Рамшо: def BCPL.string(expr s,n) = 7, цепочка s превращается в п-байтовую BCPL-цепочку for l:=if length(s)>=n: n-1 else: length(s) fi: 1 for k:=l upto 1: , substring (k-l,k) of s endfor for k:=l+2 upto n: , 0 endfor endfor enddef; inner end; def bye = if fontmaking>0: headerbyte 9: BCPL_string(font_coding_scheme_,40); special "codingscheme " ft font_coding_scheme_; headerbyte 49: BCPL_string(font_identifier_,20); special "identifier " ft font.identifier_; headerbyte 72: max(0, 254 - round 2designsize); fi end enddef; outer bye,end; Эти макросы можно включить в файл local.mf, приспосабливающий базовый формат plain.mf к конкретным локальным условиям. Тогда, если пользователь в конце сеанса будет использовать команду 'bye' вместо 'end', то дополнительная информация будет автоматически вноситься во вводную часть tfm-файла. Давайте подведем итоги, перечислив все, чему мы здесь научились в этом приложении. Используя команды описания метрики, программист может задавать разного рода дополнительную информацию о том, как осуществлять набор данным шрифтом. Простые версии этих команд, которых вполне достаточно для разработки простых шрифтов, представляют собой стандартные операции системы METAFONT в базовом формате; примеры их использования были рассмотрены в главе 11 и в приложении Е. В общем случае мы можем использовать команды описания метрики следующих пяти типов: (команда описания метрики) —> (команда ligtable) | (команда charlist) | (команда extensible) | (команда fontdimen) | (команда headerbyte) Этим завершается описание синтаксиса METAFONT, которое было несколько неполным в главе 26. Этим я разрушил хаос, введя порядок там, где прежде его никогда не было. Я думаю, что изобретенные мною типографские пункты помогут добиться предельной точности и четкости. — ПЬЕР ФУРНЬЕ, Учебник по типографии (1764) Следует впитывать в себя все краски бытия, не обращая внимания на мелочи. Мелочи всегда вульгарны. — ОСКАР УАЙЛЬД, Портрет Дориана Грея (1890)
G Файлы шрифтов общего формата
Приложение G: Файлы шрифтов общего формата 335 Основная часть выходных данных, генерируемых системой METAFONT, отправляется в gf-файл, который называется файлом шрифта общего формата, так как легко может быть переведен в любой другой шрифтовой формат, хотя сам не соответствует никакому "патентованному" шрифтовому формату. Цель данного приложения — объяснить, какая именно информация отправляется в gf-файл, и при каких условиях система METAFONT производит запись в этот файл. Выходной gf-файл представляет собой компактное двоичное представление оцифрованного шрифта. В нем содержится вся информация, необходимая драйверу устройства — программе, посредством которой из dvi-файла, генерируемого системой TeX, получается готовый печатный документ. Точная схема внутреннего устройства gf-файла нас не интересует, но нам нужно знать, какого рода данные зашифрованы в этом файле. Первая строчка gf-файла объясняет его происхождение. Система METAFONT записывает следующую строку METAFONT output 1986.06.24:1635 исходя из текущих значений внутренних величин day (число), month (месяц), year (год) и time (время) на момент начала записи gf-файла. (В данном случае day = 24, month = 6, year = 1986 и time = 16 х 60 + 35 = 995.) После вводной строки в gf-файле следует последовательность (команд special), перемежаемых шифрованными изображениями символов. Эти "специальные" команды резервируют место, куда впоследствии будут вставлены последовательности примитивных команд, которые определяются в дополнение к примитивам METAFONT и саму систему не затрагивают. Некоторые такие команды определены изначально, другие же, несомненно, будут разработаны и реализованы в ближайшие годы. (В системе TeX имеется аналогичная команда \special, которая вставляет произвольную строку в dvi-файл.) В gf-файл (команда special) попадает, если вы указываете 'special (цепочка)' или 'numspecial (числовое)' в тот момент, когда установлено значение proofing > 0. Цепочка, фигурирующая в команде special, должна стоять перед командой numspecial и быть либо ключевым словом, либо последовательностью из ключевого слова, пробела и некой дополнительной информации. Если ключевое слово задает операцию, в которой используется один или несколько числовых аргументов, то за ней должны следовать соответствующие числа, записываемые командами numspecial. Например, макрос 'proofrule' (см. приложение В) раскладывается в последовательность special "rule"; numspecial х\\ numspecial y\\ numspecial X2\ numspecial уг; которая представляет прямую линию на пробном оттиске, проходящую от точки (xi, j/i) до точки (х2,У2). Если вы скажете 'grayfont gray5', то макрос grayfont, описанный в приложении В, будет разлагаться в последовательность 'special "grayfont gray5"\ Программа, которая работает с gf-файлами, вначале считывает все цепочки в командах special до тех пор, пока не встретит пробел или конец цепочки. Если считанное ключевое слово программе неизвестно, то цепочка будет проигнорирована, как и все следующие за ней команды numspecial вместе с аргументами. Но если ключевое слово принадлежит к числу известных, то программа сможет определить соответствующие аргументы. Например, программа GFtoDVI, описанная в приложении Н, умеет работать с ключевыми словами 'rule' и 'grayfont' из базового формата METAFONT. METAFONT может генерировать команды special и по собственной инициативе, но только в том случае, когда значение величины proofing больше нуля. Возможны два случая: (1) если имеется утверждение типа (заголовок), то выводится особая строка '"title 11 & (заголовок)' (вот как фраза 'The letter 0' попадала на пробные оттиски в экспериментах, проводимых в главе 5); (2) непосредственно перед тем, как изображение символа
336 Приложение G: Файлы шрифтов общего формата отправляется на вывод, система METAFONT выполняет следующую последовательность инструкций: if round xoffset ф 0: special "xoffset"; numspecial round xoffset; fi if round yoffset ф 0: special "yoff set"; numspecial round yoffset; fi Команда shipout отправляет оцифрованный рисунок в gf-файл, если значение величины proofing больше или равно нулю, но ничего не выводится, если proofing < 0. Более того, текущие значения величин charwd, charht, chardp, charic, chardx и chardy сохраняются как соответствующие текущему значению charcode; значения этих величин сохраняются в любом случае, независимо от значения proofing. С этого момента символ с текущим кодом объявляется "существующим", т.е. булева величина charexists будет иметь значение true. При выводе рисунка, пиксели с положительными значениями считаются "черными", а все остальные — "белыми". Черно-белый массив пикселей шифруется таким образом, что при удвоении разрешения размер выходного gf-файла в большинстве случаев увеличивается приблизительно вдвое. Система METAFONT докладывает о текущем состоянии дел, выводя на терминал последовательность ' [с]' всякий раз, когда отправляется на вывод символ с кодом с. (Символ '[' печатается перед тем, как начинается процесс шифрования, а символ ']' — после его завершения; таким образом, вы можете проследить за тем, как долго продолжается процесс вывода того или иного символа.) Если после округления величина charext имеет ненулевое значение, то сообщение будет иметь вид '[с.х]'; например, сообщение '[65.3]' указывает на символ с кодом 65 и расширением кода 3. Система ЧЩХ допускает наличие в шрифте до 256 символов, но версии, разработанные для восточных языков, могут задействовать возможность введения дополнительных символов посредством использования расширения charext. Все символы с одинаковым кодом имеют одинаковую ширину, высоту и глубину ограничивающей рамки, но для разным расширениям кода могут соответствовать совершенно разные символы. Вообще говоря, команда special относится к рисунку, следующему за ней, а не к тому, что ей предшествует. Однако команды special, стоящие до первого оцифрованного рисунка, могут давать инструкции, относящиеся ко всему шрифту в целом. Команды special, следующие за последним рисунком, всегда относятся ко всему шрифту в целом. (Например, макрос 'bye', определенный в конце приложения F, генерирует две "специальные" цепочки, которые вставляются после последнего символа шрифта.) Запись в gf-файл производится только в том случае, если выводится по крайней мере один символ или команда special выполняется в момент, когда proofing > 0, либо если система встретила утверждение типа (заголовок) в момент, когда proofing > 0. То, какое из этих событий случится первым, и определяет имя, присваиваемое gf-файлу. Если на этот момент не была выполнена ни одна команда input, METAFONT установит 'mf put' в качестве названия выполняемого задания; в противном случае название задания будет уже определено. Полное имя gf-файла будет иметь вид: '(название задания). (разрешение) gf', где (разрешение) определяется текущим значением величины hppp. (Если hppp < 0, то разрешение будет опущено; в противном случае значение hppp переводится в эквивалентное число пикселей на дюйм по горизонтали.) Последующие операции ввода input и изменения значения величины hppp не изменяют имени gf-файла. В конце gf-файл а содержится некоторое количество числовых данных, необходимых при печати данным шрифтом. Первыми по порядку указываются номинальный размер шрифта и контрольная сумма; они в точности соответствуют данным, заносимым в tfm-файл, за тем исключением, что первые несколько байтов tfm-файла специально устанавливаются иными. Затем следуют значения величин hppp и vppp. (Эти значения устанавливаются в конце выполнения задания, так что hppp может не соответствовать значению (разрешения), указанному в имени gf-файла.)
Приложение G: Файлы шрифтов общего формата 337 Наконец, в gf-файл заносятся значения величин charwd, chardx и chardy для каждого "существующего" символа. Величины chardx и chardy представляют величины сдвига по горизонтали и вертикали, желательного при выводе символов на печать с помощью конкретного устройства (см. главу 12). Значения величин charwd идентичны значениям ширины символов, указанным в tfm-файле. Контрольная сумма полностью определяется данными о величинах charwd] два шрифта с символами одинаковой ширины имеют одинаковую контрольную сумму, но два шрифта с символами разной ширины почти никогда не будут иметь одинаковые контрольные суммы. Для чего нужна контрольная сумма можно понять, рассмотрев следующий сценарий. Шрифт с именем cmrlO может генерироваться системой METAFONT когда угодно. При этом мы получаем tfm-файл с именем cmrlO.tfm и gf-файл с именем, например, cmrl0.300gf. Документ с именем doc, в котором используется шрифт cmrlO, может генерироваться системой TeX когда угодно. При этом мы получаем dvi-файл с именем doc.dvi; для того чтобы создать этот dvi-файл, системе TeX нужно считать файл cmrlO.tfm. По прошествии некоторого времени для вывода файла doc.dvi на печать с использованием шрифта cmrl0.300gf применяется специальная программа — драйвер устройства. За это время шрифт мог быть изменен. Если имеющийся на данный момент gf-файл не соответствует своему tf m-файлу, как предполагает система TeX, в печатном документе могут появиться непонятные "глюки", так как информация в dvi-файле хранится в кратком виде, но при этом предполагается, что драйвер устройства черпает сведения о ширине символов из tfm-файла. Риск возникновения проблем сводится к минимуму благодаря тому, что система TeX помещает в генерируемый ею dvi-файл предполагаемые собственные размеры используемых шрифтов и значения их контрольных сумм. Таким образом, если gf-файл не соответствует предположениям системы TeX, то драйвер устройства может определить ошибку и выдать сообщение о ней. Но даже если слесарь-виртуоз работает безупречно, нам все равно необходимо регулярно снабжать его тисками, ручными тисками, молоточками, маленькими напильника мй^й Надфилями (называемыми также часовыми напильничками) по мере того, как они изнашиваются. ДЖОЗЕФ МОКСОН, Упражнения по механике (1683) В определении должны быть перечислены все общие свойства объекта. — ЛИННЕЙ, Философия ботаники (1751)
н Пробные оттиски
Приложение Н: Пробные оттиски 339 Шрифт — не теорема, и его "правильность" нельзя строго обосновать, а можно лишь почувствовать. Более того, если в шрифте имеются неправильные символы, то ошибки легче выявить визуально, рассматривая их увеличенные изображения. Таким образом, система METAFONT не является самодостаточной; требуются дополнительные программы, которые бы позволяли переводить ее выходные данные в графический вид. В данном приложении мы обсудим две такие вспомогательные программы, служащие примером множества других подобных программ, которые могут быть разработаны. Первая называется GFtoDVI; она переводит gf-файлы в dvi-файлы, которые можно распечатывать точно так же, как выходные файлы системы TeX. Изображение каждого символа из gf-файла печатается на отдельной странице с обозначением помеченных точек и ограничивающей рамки, как на иллюстрациях, которые встречались в этой книге. (Эти иллюстрации были подготовлены при помощи GFtoDVI.) Вторая программа — это сама система TeX; мы рассмотрим набор макросов системы TeX, разработанных специально для тестирования шрифтов. 1. Увеличенные изображения. Файлы с расширением gf генерируются системой METAFONT в базовом формате, когда она работает в режиме proof или smoke. Из экспериментов в главе 5 мы знаем, что прогнав эти файлы через программу GFtoDVI, мы получим изображения символов в увеличенном виде. Кроме того, программа GFtoDVI позволяет изучать символы с низким разрешением; при этом дополнительные преимущества имеют те символы, при создании которых использовался макрос lgf corners' из базового формата METAFONT. Давайте проведем ревизию возможностей программы GFtoDVI. Все контакты между системой METAFONT и программой GFtoDVI происходят через gf-файл, а также посредством специальных подстановок, возможность осуществить которые предоставляется непосредственно в процессе работы GFtoDVI. Если gf-файл не содержит команд special, описанных в приложении G, то выходной файл программы GFtoDVI будет содержать изображения символов, по одному на каждой странице, состоящие только из их "черных" пикселей. В верхней части каждой страницы помещается строка-заголовок с указанием даты и времени начала сеанса работы с METAFONT, в котором был получен соответствующий gf-файл, а также код данного символа и его расширенный код (если он не равен нулю). Черные пиксели на печати заменяются символами так называемого "серого шрифта", который мы детально рассмотрим позже; вы можете получить множество различных выходных dvi-файлов из одного и того же gf-файла, подставляя разные серые шрифты в процессе работы программы GFtoDVI. Для того чтобы на пробных оттисках появились дополнительные элементы, в gf-файле должны присутствовать команды special. Например, если величина proofing имеет положительное значение, то, как объяснялось в приложении G, система METAFONT автоматически вставит в gf-файл команду title; программа GFtoDVI поместит соответствующую цепочку в строку-заголовок страницы с изображением следующего за командой символа. Если имеется несколько утверждений с командой 'title', то будут напечатаны все соответствующие заголовки, если, конечно, они поместятся в одной строке. Наиболее важные команды типа 'special' — это команды, отдающие программе GFtoDVI распоряжение пропечатать на диаграмме символа помеченные точки. Например, если в программе для системы METAFONT в базовом формате команда (labels(l, 2)' встречается в тот момент, когда proofing > 1, то макросы (см. приложение В) преобразуют ее в "специальные" команды вида special" 01"; numspecial xi; numspecial t/i; special " 02"; numspecial хг\ numspecial уг\ Программа GFtoDVI затем помещает в положение (a?i,yi) на пробном оттиске точку, помеченную цифрой '!', а точку, помеченную цифрой '2', — в положение (хг,2/2).
340 Приложение Н: Пробные оттиски Метки размещаются относительно точек в одной из четырех позиций: сверху, слева, справа или снизу. Программа GFtoDVI старается разместить все метки так, чтобы они не накладывались одна на другую и на точки, которые они метят. Но если вы хотите сами контролировать размещение каждой конкретной точки, то можете, например, указать 'labels, top (la, 2a)'; в этом случае метки будут печататься сверху от своих точек независимо от того, приведет ли это к наложению на другие метки и/или точки. В данном случае gf- файл будет содержать такие строки: special " 11а"; numspecial Х\а; numspecial t/ia; special " 12а"; numspecial Х2а\ numspecial у2а- По символу, который следует за первым пробелом, программа GFtoDVI определяет, как в данном случае желательно разместить метку; последующие символы представляют собой текст метки. В базовом формате METAFONT команда 'labels, top (1а, 2а)' есть краткая форма записи команд ' makelabel. top (MlaM,zia); makelabel. fop (м2ам, z2a)' ПРИ условии, что proofing > 1. Макрос makelabel является одним из фундаментальных макросов, и использовать его непосредственно следует лишь в случаях, когда необходимо добиться необычных эффектов. Предположим, вы хотите поставить точку без метки в позиции zy, для этого вы можете использовать команду 'makelabel(|,и, zs)'. Предположим, далее, что вы хотите разместить слева от гъ метку, но не ставить точку; в этом случае вы можете использовать команду 'makelabel.Ift.nodot ("5м, zs)'. Более того, при помощи команды 'makelabel.Ift.nodot("5", z$ — (2,3))' вы можете передвинуть метку на два пикселя влево и на три пикселя вниз, получив таким образом метку, расположенную по диагонали от точки, которую она метит. Метки без точек, кроме прочего, позволяют печатать на диаграммах слова-комментарии. Всего программа GFtoDVI распознает девять видов меток, исходя из первых двух символов в цепочке команды 'special': ■ makelabel (special " 0"): расположение метки выбирается автоматически. ■ makelabel.fop (special " Iм): метка центрируется относительно точки и располагается прямо над ней. ■ makelabel.Ift (special " 2м): метка располагается слева от точки. ■ makelabel.rt (special " 3м): метка располагается справа от точки. ■ makelabel.hot (special " 4м): метка центрируется относительно точки и располагается прямо под ней. ■ makelabel.top.nodot (special " 5й): то же, что и с top, но без точки. ■ makelabel.Ift.nodot (special " 6м): то же, что и с Ift, но без точки. ■ makelabel.rt.nodot (special " 7м): то же, что и с rt, но без точки. ■ makelabel.hot.nodot (special " 8м): то же, что и с bot, но без точки. В первом случае мы имеем дело с так называемыми автометками; это стандартный вид команды. При таком способе расстановки точка печатается всегда, независимо от того, накладывается ли она на другие точки, но метка может и не печататься. Автометки расставляются только после того, как будут расставлены метки, способ расположения которых точно указан; затем программа GFtoDVI пытается разместить как можно больше оставшихся меток. Если для размещения автометки недостаточно места, то в правом верхнем углу пробного оттиска помещается "уравнение переполнения". Например, уравнение переполнения вида '5 s 5г + (-4.9,0)' означает, что не хватило места для метки 5, чья точка расположена на 4.9 пикселя левее точки, соответствующей метке 5г (которая имеется на пробном оттиске). От уравнений переполнения можно избавиться, указав для GFtoDVI в команде 'special' цепочку " /" вместо " 0м; при этом автометки расставляются как обычно с той лишь разницей, что в том случае, когда для меток недостаточно места, программа о
Приложение Н: Пробные оттиски 341 них просто "забывает". В базовом формате METAFONT для этого достаточно установить 4code~ := " /'"в начале программы; величина Icode. — это цепочка, которую команда makelabel использует в автометках. Следующий важный вид информации — описание прямых линий на пробных оттисках, которым соответствует термин "rule". В базовом формате METAFONT для этого имеется команда 'proofrule(zi,Z2)\ разложение которой имеет вид: special "rule"; numspecial х\\ numspecial yi; numspecial X25 numspecial y2- Программа GFtoDVI не очень хорошо воспроизводит диагональные линии, поскольку стандартный dvi-формат не содержит встроенных средств для изображения прямых линий, если они не являются вертикальными или горизонтальными. Поэтому, если линия не является вертикальной (xi = хг) или горизонтальной (у\ = уг), то вы можете получить сообщение об ошибке. Однако, частично эту проблему можно решить за счет так называемого "наклонного шрифта" — slantf ont, который позволяет программе GFtoDVI строить наклонные линии как последовательности символов. При этом в каждом задании мы можем использовать лишь один угол наклона, но это все же лучше, чем ничего (см. ниже). В программах для системы METAFONT в базовом формате вес линий на пробных оттисках может быть задан командой 'proofrulethickness 1.5mra#', которая раскладывается в последовательность special "rulethickness"; numspecial 1.5mm#. Все горизонтальные и вертикальные линии проводятся пером толщины, равной текущему значению величины rulethickness, поэтому на одной диаграмме могут присутствовать линии разной толщины. Если текущее значение rulethickness отрицательно, то линия не изображается; если эта величина равна нулю, то используется значение rulethickness, устанавливаемое по умолчанию, исходя из соответствующего параметра серого шрифта; если значение положительно, то указанная толщина будет в случае необходимости увеличена до целого числа пикселей, и полученное значение будет использоваться при изображении прямых линий. В начале каждого символа текущее значение rulethickness равно нулю. Вы можете изменить расположение всей диаграммы на странице при помощи команды 'proofoffset (х,у)', которая раскладывается в последовательность special "offset"; numspecial х; numspecial у Эта команда предписывает программе GFtoDVI сдвинуть все изображение следующего символа (кроме заголовка) на х пикселей вправо и на у пикселей вверх. В выходных файлах программы GFtoDVI используются шрифты четырех видов: (1) title font — используется в верхней строке каждой страницы, содержащей заголовок; (2) labelfont — для печати меток; (3) grayfont — "серый шрифт" для печати точек и черных пикселей; (4) slantfont — для печати наклонных прямых линий. Вы можете указать, какие именно шрифты должна использовать для этих целей программа GFtoDVI, задав их в "специальных" командах title font, labelfont, grayfont и slantfont. Если вы этого не сделаете, то будут подставлены соответствующие шрифты, используемые по умолчанию. Кроме того, программа GFtoDVI понимает такие команды, как '"gray font are а /usr/dek"', при помощи которых вы можете указать нестандартное местонахождение или имя директории, в которой расположен файл серого шрифта. Более того, если вы хотите, чтобы шрифт 'labelfont' загружался не в собственном размере, а в размере 20 pt, то можете вставить в gf-файл такую команду: special "labelfontat"; numspecial 20 Местонахождение и желаемый размер серого шрифта указываются после того, как задано его имя, т.е. команда '"grayfont"' отменяет предыдущую команду '"grayfontarea"'.
Приложение Н: Пробные оттиски Четыре шрифта, используемые программой GFtoDVI, должны быть установлены до того, как из gf-файла будет считано изображение первого символа. Это значит, что специальные команды, устанавливающие шрифты, должны стоять в вашей программе до первой команды shipout или endchar; но они не должны стоять до команды mode.setup, если вы хотите, чтобы ваш gf-файл получил правильное имя. Бели вы находите такой способ определения шрифтов неудобным, то можете изменить их непосредственно во время сеанса работы с GFtoDVI: просто наберите V после имени вводимого gf-файла, и программа предложит ввести нужные специальные команды в интерактивном режиме. Вот, например, как может выглядеть диалог с машиной в процессе работы с GFtoDVI: This is GFtoDVI, Version 2.0 GF file name: io.2602gf/ Special font substitution: labelfont cmbzlO QK; any more? grayfont black OK; any more? После нажатия клавиши (return), программа GFtoDVI продолжит работу как обычно, но будет игнорировать все установки шрифтов во входном файле, противоречащие заданным. 2. Серые шрифты. Пробный оттиск, генерируемый программой GFtoDVI, можно рассматривать как двумерный массив из прямоугольников, где каждый многоугольник либо пуст, либо заполнен некоторым специальным символом, который мы будем обозначать через V. Каждый пустой прямоугольник представляет белый пиксель, в то время как каждый символ * представляет черный пиксель. На этот массив из прямоугольников часто накладываются дополнительные метки и поясняющий текст; поэтому, как правило, лучше всего для отображения черных пикселей подходит символ «, который создает сероватый фон, хотя в принципе может использоваться любой другой символ. Для построения таких изображений программе GFtoDVI необходимо работать со специальным типом шрифта, называемым "серым". Используя различные серые шрифты, можно получить множество различных пробных оттисков. В следующих нескольких абзацах подробно объясняется, из чего именно должен состоять серый шрифт, на тот случай, если вы захотите разработать такой шрифт самостоятельно. Простейший серый шрифт содержит всего два символа, а именно — символ к и символ, который будет использоваться для изображения точек, отмечающих ключевые позиции на ваших пробных оттисках. Если вы намерены получать изображения с пикселями относительно крупных размеров, то для этой цели вполне достаточно серого шрифта с двумя символами. Однако, если размеры пикселей относительно малы, то двух символов в сером шрифте явно недостаточно, так как иначе изображение будет состоять из десятков тысяч крошечных символов, а устройства печати очень редко успешно справляются с задачей вывода данных, столь сильно отличающихся от обычного текста. Поэтому серый шрифт с маленькими пикселями обычно содержит символы, представляющие собой готовые наборы из повторяющихся к и на самом деле изображение строится из небольшого числа символов. Поскольку многие устройства печати не позволяют выводить сколь угодно большие или сложные по структуре символы, то для работы с разными устройствами не может одинаково хорошо подходить один и тот же серый шрифт. Действительно, ширина символа « должна быть равной целому числу горизонтальных и вертикальных единиц длины, соответствующих используемому устройству, так как в противном случае округление до целых может привести к появлению на пробных оттисках некрасивых полос. Поэтому невозможно сделать серый шрифт aim ар атно независимым, как это возможно для обычных шрифтов. Поняв это, можно теперь взглянуть на серый шрифт глазами программы GFtoDVI и выяснить, из чего же он состоит. В позиции номер 1 серого шрифта всегда стоит символ к. Он должен иметь положительную высоту h и ширину w\ глубина и курсивная поправка этого символа игнорируются.
Приложение Н: Пробные оттиски 343 Позиции 2-120 серого шрифта резервируются под специальные комбинации, состоящие из символов м и пробелов, составленных друг над другом. Присутствие в любой из этих позиций какого-либо символа не является обязательным. Но если все же в этих позициях содержатся символы, то они должны иметь ширину w и представлять собой вполне определенные конфигурации из ж и пробелов, вид которых зависит только от номера позиции. Например, в позиции номер 3 серого шрифта может либо вовсе не быть никакого символа, либо в ней должен находиться символ, состоящий из двух символов ш, расположенных один над другим; один из этих символов к должен располагаться над базовой линией, а второй — непосредственно под ней. В дальнейшем нам будет удобно обозначать вертикальные сочетания из символов к и пробелов соответствующими горизонтальными наборами тех же символов, такими, например, как '* ж \ Условимся, что набор строится от основания к вершине, и самый верхний прямоугольник "сидит" на базовой линии. Таким образом, набор (м ** ' фактически обозначает символ высотой h и глубиной 4Л, имеющий вид: g«— базовая линия м Мы используем здесь горизонтальные, а не вертикальные обозначения, поскольку, во- первых, векторы-столбцы занимают слишком много места и, во-вторых, таким горизонтальным наборам естественным образом соответствуют двоичные числа. Позиции 1-63 серого шрифта резервируются под наборы *, м , **, * , * * и так далее вплоть до ******, соответствующие стандартным двоичным представлениям чисел 1-63, где символ к заменяет цифру 1, а пробел — цифру 0. Позиции 64-70 резервируются под специальные наборы к , ** , *** , **** , ***** , ****** , ******* по семь символов в каждом. Точно так же, позиции 71-78 резервируются под восьмисимвольные наборы, начиная с набора * и заканчивая набором ********. Девятисимвольные наборы с * по ********* приписываются позициям 79-87, десятисимвольные — позициям 88-97, одиннадцатисим- вольные — позициям 98-108, а двенадцатисимвольные — позициям 109-120. Позиция 0 серого шрифта резервируется под символ "точка", который должен иметь положительную высоту h! и положительную ширину w . Когда программе GFtoDVI требуется отметить точкой положение (х, у) на диаграмме, она размещает символ точки так, чтобы координаты его точки отсчета совпадали с (х, у). Считается, что точка занимает прямоугольник, углы которого имеют координаты (ж ± ti/,y ± Л'); прямоугольная рамка, содержащая метку, вплотную подгоняется к прямоугольнику, содержащему точку. Все остальные позиции серого шрифта (позиции 121-255) остаются незарезервированными в том смысле, что изначально им не приписывается какое-либо значение. Но программа GFtoDVI позволяет получить к ним доступ, указав их в tfm-файле в списке команды char list, где первый элемент может быть произвольным символом в позиции с 1 по 120. В таком случае каждый последующий символ в списке должен быть эквивалентен двум копиям предшествующего, составленным горизонтально. Например, в случае charlist 53: 121: 122: 123 символ номер 121 будет состоять из двух символов номер 53, символ номер 122 — из двух символов номер 121 (т.е. из четырех символов номер 53), а символ номер 123 — из двух символов номер 122 (т.е. из восьми символов номер 53). Поскольку в позиции 53 находится набор к* * *, то символ номер 123 в данном примере будет иметь высоту /i, глубину 5/i и ширину 8tu, и будет представлять собою набор вида: *******Ч— базовая линия Конечно, маловероятно, что такая таблица пикселей встретится в gf-файле, но если это случится, то программа GFtoDVI сможет использовать для ее отображения данный набор. Чтобы введение таких удвоенных наборов себя оправдывало, разработчик серого шрифта
344 Приложение Н: Пробные оттиски должен продумать, какие таблицы пикселей будут повторяться достаточно часто. Например, символ номер 120 (к***********) или другой символ, который является максимальным по высоте набором из символов * без пробелов, является естественным претендентом на последовательное удвоение. Вот алгоритм, согласно которому программа GFtoDVI решает, какие символы серого шрифта должны использоваться для отображения данной конфигурации из белых и черных пикселей: Если черных пикселей нет вообще, то завершить сеанс. Иначе, рассмотреть верхнюю строку, содержащую по крайней мере один черный пиксель, и одиннадцать следующих строк. Для каждого такого столбца пикселей найти наибольшее такое к} что 1 < к < 120, серый шрифт содержит символ номер к и таблица пикселей, соответствующая позиции /:, присутствует в данном столбце. Вставить символ номер к (если он еще не вставлен) и стереть соответствующие черные пиксели; если нужно вставить два или более одинаковых последовательных символов, то использовать двойные символы, если таковые присутствуют в сером шрифте. Повторять этот процесс с оставшейся конфигурацией до тех пор, пока не будут стерты все черные пиксели. Если серый шрифт содержит все символы в позициях 1-63, то на каждом шаге этого алгоритма обрабатывается как минимум шесть строк конфигурации; а если имеются также символы в позициях 64-120, то обрабатывается как минимум двенадцать строк, поскольку для этого имеются все необходимые комбинации из символов * и пробелов. Некоторые из параметров типа fontdimen, которые обсуждались в приложении F, являются важными для серых шрифтов. Если значение з величины font_slant не равно нулю, то выводимые программой GFtoDVI символы будет скошены; в этом случае символ » может представлять собой не обычный прямоугольник, а параллелограмм с соответствующим наклоном. Координаты (х,у) системы METAFONT будут соответствовать "физической" позиции (xw + yhs,yh). (Таким образом можно получать наклонные изображения прямых символов, искусственно наклоняя их.) Параметр fontdimen 8 в сером шрифте определяет толщину прямых линий на пробных оттисках. Если он имеет нулевое значение, то толщина прямых выбирается равной принимаемой по умолчанию в системе TeX (0.4 pt). Остальные параметры серого шрифта программой GFtoDVI игнорируются, но при этом значения величин font_normal.space и font_quad принимаются равными ги, а значение font_x_height — равным h. Для получения лучших результатов разработчику серого шрифта нужно выбрать такие значения w и Л, чтобы используемая пользователем программа печати (которая обеспечивает получение готового печатного продукта из dvi-файла) не делала никаких ошибок округления. Более того, символ "точка" должен иметь в диаметре четное число (2т) пикселей, а толщина прямых линий должна составлять четное число (2п) пикселей. Тогда точки и прямые на пробных оттисках будут правильно центрироваться в общем случае, когда координаты выражаются целыми числами. Серые шрифты почти всегда разрабатываются под определенные устройства вывода, несмотря на то, что расширение 'dvi' расшифровывается как "аппаратно-независимый". Мы используем dvi-файлы для вывода пробных оттисков потому, что программы, обеспечивающие печать dvi-файлов, у нас уже имеются. Следующие несколько страниц содержат текст программы 'grayf .mf' для системы METAFONT, которая позволяет генерировать множество различных серых шрифтов. Программа должна вызываться из параметрического файла, в котором устанавливаются значения определенных величин. ■ Если величина large.pixels имеет тип boolean, то будет генерироваться всего 15 символов; в противном случае их будет 123. ■ Если величина pix.picture имеет тип picture, то соответствующий рисунок должен изображать желаемый вид символа V, а величины pix.wd и pixM — выражать его ширину и высоту в пикселях. В противном случае используется стандартный символ серого пикселя.
Приложение Н: Пробные оттиски 345 ■ Если величина rep известна, то она должна быть целым положительным числом; стандартный символ пикселя будет размножен так, что пробные оттиски будут в rep раз больше, чем обычно, а полученная таким образом таблица пикселей будет немного срезана по краям, так что дискретные пиксели будут четко просматриваться на пробном оттиске. ■ Если величина lightweight имеет тип boolean, то стандартная таблица пикселей будет вполовину светлее, чем обычно. ■ Если величина dotsize известна, то она должна представлять собой диаметр специального символа "точка", выраженный в пикселях. ■ Должен быть указан идентификатор шрифта — font-identifier. (Значения величин rep и lightweight игнорируются, если указан рисунок pix-picture.) Поскольку серые шрифты являются по сути аппаратно-зависимыми, то, в отличие от обычных шрифтов, мы не задаем в них "точные" единицы измерения с самого начала, а вычисляем их позже по заданным значениям пиксельных единиц. Имя серого шрифта должно включать в себя имя того устройства, для которого этот шрифт разрабатывался. ("Излюбленное" устройство вывода пробных оттисков можно также выбрать при установке системы; для него можно использовать шрифты с именами 'gray' и 'black'; эти шрифты, зависящие от конкретной инсталяции, выбираются по умолчанию для режимов proof и smoke, соответственно.) Вот, например, как может выглядеть параметрический файл 'graycheap.mf', генерирующий серый шрифт с "ванильным привкусом" для гипотетического принтера cheapo: У, Gray font for Cheapo with proofsheet resolution 50 pixels per inch if modeOcheapo: errmessage "This file is for cheapo only"; fi font.identifier "GRAYCHEAP"; input grayf (Пробные оттиски будут иметь разрешение 50 пикселей на дюйм, так как разрешающая способность принтера cheapo равна 200 пикселям на дюйм, а площадь стандартного символа серого пикселя pix-picture в шрифте grayf в данном случае будет равна четырем квадратным пикселям.) Если таблица из стандартных пикселей окажется настолько темной, что на ее фоне не будут выделяться точки и прямые линии, то в параметрический файл нужно будет вставить объявление 'boolean lightweight'. Следующий файл 'blackcheap.mf' генерирует аспидно-черный шрифт, который дает изображение с чуть более высоким разрешением: У, Black font for Cheapo with proofsheet resolution 66.7 pixels per inch if modeOcheapo: errmessage "This file is for cheapo only"; fi picture pix.picture; pix.wd := pix_ht := 3; pix.picture := unitpixel scaled 3; font.identifier "BLACKCHEAP"; input grayf А следующий файл 'graycheapS.mf' генерирует серый шрифт, подходящий для использования в пробных оттисках символов с малым разрешением: '/• Gray font for Cheapo with proofsheet resolution 10 pixels per inch if modeOcheapo: errmessage "This file is for cheapo only"; fi rep=5; boolean large.pixels; font.identifier "GRAYCHEAP"; input grayf Давайте рассмотрим сам файл-программу 'grayf .mf'. Он начинается с простой проверки того, являются ли значения величин тад и rep, если они известны, положительными целыми числами; затем следует менее очевидная часть программы, в которой производятся достаточно необычные операции с увеличением:
346 Приложение Н: Пробные оттиски У, More-or-less general gray font generator 7, See Appendix H of The METAFONTbook for how to use it forsuffixes m = mag,rep: if unknown m: m := 1; elseif (m<l) or (m<>floor m): errmessage "Sorry, " ft str m ft " must be a positive integer"; m := 1; fi endfor mg := mag; mag := 1; mode.setup; if mg>l: hppp := hppp*mg; vppp := vppp*mg; extra.endchar:= "if charcode>0:currentpicture:«currentpicture scaled mg;fi" ft extra.endchar; fi; Эти простейший способ гарантировать, что увеличение не повлияет на tf m-файл. В следующей части программы grayf определяется величина pix-picture — символ, изображающий пиксель. if picture pix_picture: rep :s 1; cull pix_picture keeping (l,infinity); else: for z=(0,2),(1,0),(2,3),(3,1): fill unitsquare shifted z; endfor if not boolean lightweight: addto currentpicture also currentpicture rotated 90 xscaled -1; fi if unknown scale: scale := max(1,round(pixels.per.inch/300)); fi pix_wd := pix.ht := 4scale; if rep>l: picture pix; currentpicture := currentpicture shifted-(l,l); pix := currentpicture; for r=l upto rep-1: addto currentpicture also pix shifted(4r,0); endfor cullit; pix := currentpicture; for r=l upto rep-1: addto currentpicture also pix shifted(0,4r); endfor unfill unitsquare xscaled 4rep yscaled 2 shifted-(l,l); unfill unitsquare yscaled 4rep xscaled 2 shifted-(1,1); cullit; fi picture pix.picture; pix_picture :» currentpicture scaled scale; pix.wd := pix.ht := 4scale*rep; fi Если величина lightweight объявлена как булева переменная, то мы получим осветленное изображение, так как в таблице пикселей лишь 4 из каждых 16-ти пикселей будут "включены"; в обычной таблице таких пикселей вдвое больше. Символ номер 0 — это точка, которая определяется довольно просто: def # = *72.27/pixels_per_inch enddef; if unknown dotsize: dotsize := 2.5pix_wd/rep; fi beginchar(0,1.2dotsize#,1.2dotsize#,0); fill fullcircle scaled dotsize scaled mg; endchar; Затем задается специальная схема кодировки серого шрифта: numeric а[]; newinternal b,k; def next.binary « k := 0; forever: if k>b: a[incr b] := 0; fi exitif a[k]=0; a[k] := 0; k := k+1; endfor a[k] := 1 enddef; def next.special.binary =
Приложение Н: Пробные оттиски 347 if а[0]=1: for к=0 upto Ъ: а [к] := 0; endfor a[incr b] else: к :■ 0; forever: exitif a[incr k]=l; endfor a[k-l] fi :« 1 enddef; def make.char = clearit; next.binary; for k=0 upto b: if a[k]=l: addto currentpicture also pix.picture shifted(0,-k*pix.ht); fi endfor chareode := charcode+1; chardp := b*charht; scantokens extra.endchar; shipout currentpicture enddef; Теперь мы готовы к тому, чтобы генерировать все символы шрифта, изображающие комбинации пикселей и пробелов. charvd :« pix.vd#; charht := pix.ht#; chardx := pix_vd*mg; b :« -1; if boolean large.pixels: for k*l upto 7: make.char; charlist k:k+120; endfor charcode := 120; b := -1; addto pix.picture also pix.picture shifted (chardx,0); charvd :■ 2charwd; chardx := 2chardx; for ksl upto 7: make.char; endfor else: for k=l upto 63: make.char; endfor let next.binary * next.special.binary; for k*64 upto 120: make.char; endfor for k=121,122: charcode := k; addto currentpicture also currentpicture shifted (chardx,0); charvd :s 2charvd; chardx := 2chardx; scantokens extra.endchar; shipout currentpicture; endfor charlist 120:121:122; fi В завершение задаются параметры, относящиеся к шрифту в целом: font.coding.scheme "GFGRAY"; font.size 8(pix.vd#); font.normal.space pix.vd#; font.x.height pix.ht#; font.quad pix.vd#; fontdimen 8: if knovn rulethickness: rulethickness else: pix.vd#/(2rep) fi; bye. (Такие параметры, как aspect-ratio или наклон, в данном случае являются лишними, поэтому мы к ним не обращаемся.) 3. Наклонные шрифты. Если программе GFtoDVI требуется провести на пробном оттиске наклонные линии, она использует для этой цели шрифты особого типа. Формат так называемых "наклонных шрифтов" чуть проще формата серых шрифтов. Наклонный шрифт содержит п символов, которые находятся в позициях с 1 по п, где п — целое положительное число. Символ в позиции А; представляет наклонную линию высоты к единиц, считая от базовой линии. Все эти линии имеют фиксированный тангенс угла наклона, равный s. Вертикальная единица измерения обычно выбирается равной целому числу пикселей, достаточно малому, чтобы можно было рисовать прямые линии, имеющие в высоту целое число таких единиц; эта единица, очевидно, не должна превосходить ширины изображаемых прямых линий.
348 Приложение Н: Пробные оттиски Прямая высотой т единиц набирается в соответствии со следующим простым алгоритмом: вычислить q = \т/п\\ затем набрать q символов примерно одинакового размера, а именно: (т mod q) копий символа номер \m/q\ и q — (т mod q) копий символа номер \rn/q\. Например, если п = 15 и т = 100, то имеем g = 7; прямая высоты 100 единиц будет составлена из 7 кусочков, с использованием символов 14, 14, 14, 14, 14, 15, 15. Программа GFtoDVI принимает во внимание лишь значение charht символа номер п, так что tfm-файл не обязательно должен содержать информацию о высоте других символов. (Это весьма кстати, так как формат tf m предусматривает не более 15 различных ненулевых значений высоты символов в одном шрифте.) Значение величины -charwd символа номер к должно равняться отношению &/п, помноженному на s и на значение charht символа номер п. Параметр font_slant должен быть равен s. Параметр fontdimen 8 принято устанавливать равным толщине наклонных прямых линий, хотя программа GFtoDVI этот параметр игнорирует. Вот пример параметрического файла 'slantcheap6', соответствующего наклонному шрифту с наклоном 1/6 для принтера cheapo: У, Slant font for Cheapo with slope 1/6 if modeOcheapo: errmessage "This file is for cheapo only"; fi s=l/6; 7, the slant ratio n=30; У. the number of characters r#=.4pt#; У, thickness of the rules u=l; У. vertical unit font.identifier "SLANTCHEAP6"; input slant Соответствующий файл-программа 'slant.mf' имеет вид: У, More-or-less general slant font generator for GFtoDVI У, The calling file should set the font_identifier and У, n = number of characters У, s = slant ratio У, r# = rule thickness (in sharp units) У, u = vertical unit (in pixels) if unknown mag: mag := 1; elseif (mag<l) or (magOfloor mag): errmessage "Sorry, mag must be a positive integer"; mag := 1; fi mg :~ mag; mag := 1; mode_setup; u# := u*72.27/pixels_per_inch; pixels_per_inch := pixels_per_inch*mg; fix.units; define_whole_pixels(u); define_blacker_pixels(r); pickup pencircle scaled r; ruler := savepen; for k=l upto n: beginchar(k,k*u#*s,n*u#,0); pickup ruler; draw origin—(k*u*s,k*u); У, draw the line unfill (lft-l.bot -l) — (rt l,bot -1) — (rt 1,0) — (lft-1,0)— cycle; У, clip the ends unfill ((lft -1,0) —(rt 1,0) — (rt l,top 1) —(lft -l,top 1)—cycle) shifted (k*u*s,k*u); endchar; endfor font.size 16pt#; font.slant s; fontdimen 8: r#; font_coding_scheme "GFSLANT"; bye.
Приложение Н: Пробные оттиски 349 4- Тесты для шрифтов. Реальной проверкой для шрифта является использование его в окончательном виде для набора печатной продукции. Испытания нового шрифта можно проводить при помощи издательской системы ТЕХ, с использованием (помимо базового форматного файла) макрофайла 'testfont.tex', который рассматривается ниже. Рассматривая отдельные части файла testf ont, мы будем приводить примеры их использования. В начале файла отключаются некоторые стандартные средства системы. 7. A test bed for font evaluation \tracinglostchars=0 7, missing characters are OK \tolerance=1000 '/, and so are loose lines \raggedbottom '/, pages can be short \nopagenumbers '/, and they won't be numbered \parindent=Opt 7. nor will paragraphs be indented \hyphenpenalty=200 7. hyphens are discouraged \doublehyphendemerits=30000 7. and two in a row are terrible \nevlinechar='(3 7, we want to type multiline messages \chardef\other=12 7. and redefine "catcodes" \newcount\m \newcount\n \newcount\p \newdimen\dim 7. temporary variables Затем следуют макросы, печатающие время и дату, наличие которых на пробных оттисках очень важно. \def\today{\ifcase\month\or January\or February\or March\or April\or May\or June\or July\or August\or September\or OctoberXor NovemberXor DecemberXfi \space\number\day, \number\year} \def\hours{\n=\time \divide\n 60 \ms-\n \multiply\m 60 \advance\m \time \twodigits\n\twodigits\m} \def\twodigits#l{\ifnum #1<10 0\fi \number#l} Если вам потребуется справка, то, набрав \help, вы получите список имеющихся в распоряжении подпрограмм тестирования с комментариями об их назначении. {\catcode'\| =0 \catcode'\\=\other 7. use I as the escape, temporarily I gdef I help{ I messaged/. \init switches to another font;07. \end or \bye finishes the run;<37. \table prints the font layout in tabular format ;Ф7. \text prints a sample text, assuming TeX text font conventions;07. \sample combines \table and \text;67* \mixture mixes a background character with a series of others ;®7. \alternation interleaves a background character with a series;(37. \alphabet prints all lowercase letters within a given background;®7. \ALPHABET prints all uppercase letters within a given background; ®7. \series prints a series of letters within a given background;€% Mowers prints a comprehensive test of lowercase ;©7. \uppers prints a comprehensive test of uppercase ;®7. \digits prints a comprehensive test of numerals ;<D7. \math prints a comprehensive test of TeX math italic ;®7. \names prints a text that mixes upper and lower case; (37. \punct prints a punctuation test;€7. \bigtest combines many of the above routines;€% \help repeats this message;(37. and you can use ordinary TeX commands (e.g., to \input a file).}}}
350 Приложение Н: Пробные оттиски Программа запрашивает имя шрифта, подлежащего тестированию. Если этот шрифт находится не в системной, а в локальной директории, то вам, возможно, понадобится указать полное имя шрифта, включающее имя соответствующей директории. Кроме того, вы должны будете указать масштаб, если шрифт был увеличен, как в примере из главы 5. Если до команды '\end' вы используете команду '\init', то сможете за один сеанс работы протестировать несколько шрифтов. \def\init{\message{eName of the font to test * } \read-l to\fontname \startfont \message{Now type a test command (\string\help\space for help):}} \def\startfont{\font\testfont=\fontname \spaceskip=0pt \leftline{\sevenrm Test of \fontname\unskip\ on \today\ at \hours} \medskip \testfont \setbaselineskip \ifdim\fontdimen6\testfont<10pt \rightskip=0pt plus 20pt \else\rightskip-Opt plus 2em \fi \spaceskips\fontdimen2\testfont 7, space between words (\raggedright) \xspaceskip=\fontdimen2\testfont \advance\xspaceskip by\fontdimen7\testfont} Указанный шрифт будет называться \testfont. Как только вы его укажете, команда \init вызовет подпрограмму \startf ont, которая помещает на странице заголовок; затем она выбирает подходящее, по ее мнению, расстояние между базовыми линиями строк, и приходит в состояние готовности набрать текст с выравниванием по правому краю. (В приведенном выше определении макроса фигурирует команда базового формата TeX \raggedright.) Межстроковое расстояние — \baselineskip — принимается равным 6pt плюс высота самого "высокого" и глубина самого "глубокого" символа. Это расстояние между базовыми линиями используется при тестировании последовательностей символов (\series), но при печати образца текста (\text) оно уменьшается на 4pt. Если вы желаете изменить расстояние между базовыми линиями, выбранное в testf ont, то можете просто указать, например, <\baselineskip=llpt'. \def\setbaselineskip{\setboxO=\hbox{\n=0 *\loop\char\n \ifnum \n<255 \advance\n 1 \repeat} % 256 chars in \box0 \baselineskip=6pt \advance\baselineskip\htO \advance\baselineskip\dpO } Когда программа testf ont просит указать фоновый символ (background character), начальный символ (starting character) или конечный символ (ending character), вы можете задать какой угодно символ (введя соответствующий ASCII-код); или ввести, например, '#35', чтобы получить символ, стоящий в позиции номер 35. В обычных реализациях системы TeX с приставкой '#' указываются символы, стоящие в позициях 0-32 и 127-255, а также символ номер 35 (который в кодировке ASCII соответствует самому символу '#'). \def\setchar#l{{\escapechar-l\message{\string#l character ■ }У, \def\do##l{\catcode'##l=\other}\dospecials \read-l to\next \expandafter\finsetchar\next\next#l}} \def\finsetchar#l#2\next#3{\global\chardef#3='#l \ifnum #3='\# \global\chardef#3=#2 \fi} \def\promptthree{\setchar\background \setchar\starting \setchar\ending}
Приложение Н: Пробные оттиски 351 (Здесь система TeX должна действовать тонко, так как специальные символы, такие как 'V и '$', временно теряют свой особый важный смысл.) Допустим, фоновым символом является 'о', а начальным и конечным — 'р' и 'q\ соответственно. Тогда операция \mixture даст нам "смеси" символов вида 'орооррооорррор' и 'oqooqqoooqqqoq', а операция \alternation даст последовательности чередующихся символов вида 'ороророророророро' и 'oqoqoqoqoqoqoqoqo'. Точно так же можно получить последовательности, состоящие из других символов. \def\mixture{\promptthree \domix\mixpattern} \def\alternation{\promptthree \domix\altpattern} \def\mixpattern{\0\l\0\0\l\l\0\0\0\l\l\l\0\l} \def\altpattern{\0\l\0\l\0\l\0\l\0\l\0\l\0\l\0\l\0} \def\domix#l{\par\chardef\Os\background \n=\starting \loop \chardef\l=\n #l\endgraf \ifnum \n<\ending \advance\n 1 \repeat} Операция \series вставляет фоновый символ между всеми остальными (например, 'opoqo'). При этом допускаются специальные последовательности, содержащие строчные буквы обычных текстовых шрифтов системы TeX (включая символы 'В', 'ае', 'се' и о') и заглавные буквы (включая символы 'Ж\ ССЕ' и (0'). Операция \series, в отличие от \mixture и \alternation, отключает лигатуры и кернение. \def \! {\discretionary{\background}{\background}{\background}} \def\series{\promptthree \!\doseries\starting\ending\par} \def\doseries#l#2{\n=#l\loop\char\n\! \if num\n<#2\ advance \n 1 \repeat} \def \complower{\! \doseries{' a}{' z}\doseries{»31}{ • 34}\par} \def\compupper{\!\doseries{' A}{' Z}\doseries{'35}{'37}\par} \def\compdigs{\!\doseries{'0}{c 9}\par} \def\alphabet{\setchar\backgroimd\complover} \def\ALPHABET{\setchar\background\compupper} (Длинная последовательность может занять несколько строк; в таких случаях используется операция разрыва строки \discretionary системы l^X, так что фоновый символ будет последним в разрываемой строке, и будет воспроизведен еще раз в начале следующей.) В "исчерпывающем" тесте — \docomprehensive — в качестве фонового символа последовательно используются все элементы определенной последовательности символов; фоновый символ перемежается с остальными элементами последовательности. Эта последовательность может состоять из строчных букв ('Mowers'), заглавных букв ('\uppers') или цифр ('\digits'). \def \lovers{\docomprehensive\complover{' а}{' z}{' 31}{' 34}} \def \uppers{\docomprehensive\compupper{' А}{' Z}{ * 35}{' 37}} \def \digits{\docomprehensive\compdigs{' 0}{' 4}{' 5}{' 9}} \def\docomprehensive#l#2#3#4#5{\par\chardef\backgro\mds#2 \loop{#l} \ifnum\background<#3\me\background\advance\m 1 \chardef\backgro\mds\m \repeat \chardef\background3#4 \loop{#l} \ifnmn\background<#5\ms\background\advance\m 1 \chardef\background=\m \repeat} Тест \names составляет имена, начинающиеся с заглавной буквы, и содержащие кроме строчных букв еще и диакритические знаки. Эти знаки могут смотреться забавно, если в тестируемом шрифте они находятся не в тех позициях, где их ожидает встретить система ТфС базового формата.
352 Приложение Н: Пробные оттиски \def\names{ {\AA}ngel\aa\ Beatrice Claire Diana VErica Fran\c{c}oise Ginette H\'el\'ene Iris Jackie K\=aren {\L}au\.ra Mar{\'\i}a N\H{a}ta{\l}{\u\i}e {\0}ctave Pauline Qu\~eneau Roxanne Sabine T\~a{\'\j}a Ur\v{s}ula Vivian Wendy Xanthippe Yv{\o}nne Z\"azilie\par} Знаки препинания тестируются в сочетаниях с разными буквами при помощи макроса Apunct': \def\punct{\par\dopunct{min}\dopunct{pig}\dopunct{hid} \dopunct{HIE}\dopxinct{TIP}\dopunct{fluff} \$1,234.56 + 7/8 = 9V/. в \#0\par} \def\dopunct#l{#l,\ #1:\ #1;\ '#1'\ '#1?\ ! '#1!\ (#1)\ [#1]\ #1»\ #l.\par> Операции \mixture, \alternations и \series позволяют обнаружить недостатки букв; буквы могут оказаться слишком темными, слишком светлыми или располагаться слишком близко друг к другу. Но, кроме всего прочего, шрифт должен быть "читабельным" — это самая важная из его характеристик. Поэтому в test font имеется образец текста, который вызывается командой l\text\ Одно из предложений в нем является необязательным, так как содержит множество диакритических знаков и необычных букв; вы можете выбросить его из текста при помощи команды '\omitaccents'. Более того, при желании вы можете ввести свой собственный текст с клавиатуры или из файла. \def\text{{\advance\baselineskip-4pt \setboxO=\hbox{abcdefghijklmnopqrstuvwxyz} \ifdim\hsize>2\wd0 \ifdim 15pc>2\wd0 \hsize=15pc \else\hsize=2\vd0 \fi\fi On November 14, 1885, Senator \fe Mrs.~Leland Stanford called together at their San Francisco mansion the 24~prominent men vho had been chosen as the first trustees of The Leland Stanford Junior University. They handed to the board the Founding Grant of the University, which they had executed three days before. This document with various amendments, legislative acts, and court decrees remains as the University's charter. In bold, sweeping language it stipulates that the objectives of the University are ''to qualify students for personal success and direct usefulness in life; and to promote the publick welfare by exercising an influence in behalf of humanity and civilization, teaching the blessings of liberty regulated by law, and inculcating love and reverence for the great principles of government as derived from the inalienable rights of man to life, liberty, and the pursuit of happiness.'' \moretext (!'THE DAZED BROWN FOX QUICKLY GAVE 12345—67890 JUMPS!)\par}} \def\moretext{?'But aren't Kafka's Schlo{\ss} and {\AE}sop's {\0E}uvres often na{\"\i}ve vis-\'a-vis the d{\ae}monic ph{\oe}nix's official r\~ole in fluffy souffl\'es? } \def\omitaccents{\let\moretext=s\relax} Далее следует одна из наиболее сложных, с точки зрения системы TeX, часть файла: макрос \table печатает таблицу шрифта, в которой могут быть опущены группы по шестнадцать символов, если все эти символы в шрифте отсутствуют. Формат этой таблицы такой же, как формат таблиц из приложения F книги Все про TeX. Если шрифт содержит необычно большие символы, которые необходимо центрировать по вертикали, то перед командой '\table' вы должны использовать команду '\centerlargechars'. (Последняя
Приложение Н: Пробные оттиски 353 применяется, как правило, при получении таблиц математических символьных шрифтов и расширенных математических шрифтов, используемых системой TeX.) \def\oct#l{\hbox{\ra\4}\kern-.2em\it#l\/\kern.05em}} У. octal constant \def\hex#l{\hbox{\rm\H{}\tt#l}} У, hexadecimal constant \def\setdigs#l"#2{\gdef\h{#2}7, \h=hex prefix; \0\l=corresponding octal \m=\n \divide\m by 64 \xdef\0{\the\m}7. \multiply\m by-64 \advance\m by\n \divide\m by 8 \xdef \H\the\m}} \def\testrow{\setboxO=\hbox{\penalty l\def\\{\char"\h}y. \\0\\l\\2\\3\\4\\5\\6\\7\\8\\9\\A\\B\\C\\D\\E\\Fy. \global\p=\lastpenalty}} '/, \p=l if none of the characters exist \def\oddline{\cr \noalign{\nointerlineskip} \multispan{19}\hrulefill& \setboxO=\hbox{\lower 2.3pt\hbox{\hex{\h x}}}\smash{\boxO}\cr \noalign{\nointerlineskip}} \nevif\ifskipping \def\evenline{\loop\skippingfalse \ifnum\n<256 \m=\n \divide\m 16 \chardef\next=\m \expandafter\setdigs\meaning\next \testrow \ifnum\p=l \skippingtrue \fi\fi \ifskipping \global\advance\n 16 \repeat \ifnum\n=256 \let\next=\endchart\else\let\next=\morechart\fi \next} \def\morechart{\cr\noalign{\hrule\penalty5000} \chartline \oddline \m=\l \advance\m 1 \xdef\l{\the\m} \chartline \evenline} \def\chartline{&\oct{\0\lx}&&\:kk\:kk\:&&\:&&\:&&\:&&\:&&\:&&} \def\chartstrut{\lower4.5pt\vbox tol4pt{}} \def\table{$$\global\n=0 \halign to\hsize\bgroup \chartstrut##\tabskipOpt pluslOptft ft\hfil##\hfil&\vrule##\cr \lower6.5pt\null ftft&\oct0&&\octl&&\oct2&&\oct3&&\oct4&&\oct5&fc\oct6&&\oct7fc\evenline} \def\endchart{\cr\noalign{\hrule} \raisell.5pt\nullft&&\hex 8&&\hex 9&ft\hex A&&\hex B& ft\hex C&fc\hex D&&\hex E&ft\hex F&\cr\egroup$$\par} \def \: {\setboxO=\hbox{\noboundary\char\n\noboundary}'/, \ifdim\ht0>7.5pt\reposition \else\ifdim\dp0>2.5pt\reposition\fi\fi \boxO\global\advance\n 1 } \def\reposition{\setboxO=\vbox{\kern2pt\boxO}\dim=\dpO \advance\dim 2pt \dpO=\dim} \def\centerlargechars{ \def\reposition{\setbox0=\hbox{$\vcenter{\kern2pt\box0\kern2pt}$}}} Затем определяются две наиболее важные комбинации тестов: \sample представляет собой комбинацию команд \table и \text и печатает, соответственно, таблицу и текст; \bigtest включает в себя все виды тестов и, кроме того, выдает загадочное слово hamburgef onstiv, которое является стандартным образцом набора. \def\sample{\table\text} \def\bigtest{\sample hamburgefonstiv HAMBURGEFONSTIV\par \names \punct Mowers \uppers \digits}
354 Приложение Н: Пробные оттиски Наконец, имеется подпрограмма \math, полезная для проверки распределения пустого пространства между символами метематических курсивных шрифтов, используемых системой ЧЩХ в базовом формате; подпрограмма \mathsy делает то же, что и \math, но для заглавных букв математических символьных шрифтов. \def\math{\textfontl=\testfont \skewchar\testfont=\skevtrial \mathchardef\Gamma=M100 \mathchardef\Delta="101 \mathchardef\Theta="102 \mathchardef\Lambda="103 \mathchardef \Xi=f,104 \mathchardef\Pi=M105 \mathchardef\Sigma="106 \mathchardef\Upsilon="107 \mathchardef\Phi="108 \mathchardef\Psi="109 \mathchardef\0mega="10A \def\ii{i} \def\jj{j} \def\\##1{|##1|+}\mathtrial \def\\##l{##l„2+}\mathtrial \defW##H##l~2+}\mathtrial \def\\##l{##l/2+}\mathtrial \def\\##l{2/##l+}\mathtrial \defW##H##l,0+}\mathtrial \defW##Hd##l+}\mathtrial \let\ii=\imath \let\jj=\jmath \defW##U\hat##l+}\mathtrial} \newcount\skewtrial \skewtrial=,177 \def\mathtrial{$\\A \\B \\C \\D \\E \\F \\G \\H \\I \\J \\K \\L \\M \\N WO \\P \\Q \\R \\S \\T \\U \\V WW \\X \\Y \\Z \\a \\b \\c \\d We \\f \\g \\h \\\ii \\\jj \\k \\1 \\m \\n \\o \\p \\q \\r \\s \\t \\u \\v \\w \\x \\y \\z \Walpha \\\beta \\\gamma \\\delta \\\epsilon W\zeta \\\eta \\\theta \\\iota \\\kappa WUambda \\\mu \\\nu W\xi \\\pi \Wrho \\\sigma \\\tau \\\upsilon \\\phi \\\chi \\\psi \\\omega \Wvartheta \\\varpi \\\varphi \\\Ganima \\\Delta WYTheta \\\Lambda \\\Xi \\\Pi \WSigma \WUpsilon \\\Phi \\\Psi \W0mega \Wpartial \\\ell \\\wp$\par} \def\mathsy{\begingroup\skewtrial=,060 7, for math symbol font tests \def\mathtrial{$WA \\B \\C WD WE \\F \\G \\H \\I \\J \\K \\L WM \\N WO \\P \\Q \\R \\S WT \\U WV WW \\X \\Y WZ$\par} \math\endgroup} Последняя строчка файла testf ont имеет вид: \ifx\noinit!\else\init\fi Это значит: "Автоматически вызывать команду '\init', если только лексема '\noinit' не означает то же, что и восклицательный знак". С чего бы это? Дело в том, у вы можете записать собственный тестовый файл и использовать в нем подпрограммы файла testf ont, не набирая соответствующие команды в интерактивном режиме. Если в вашем файле указано '\let\noinit! \input testf ont', то TeX загрузит файл testf ont, но при этом подпрограмма не попросит вас ввести имя файла. Далее в вашем файле могут идти команды тестирования для одного или нескольких шрифтов. Например, в вашем файле может быть указано: \def\fontname{cmbxlO }\startfont\sample\vfill\eject \def\fontname{cmtilO scaled \magstep3}\startfont\sample\vfill\eject Здесь инициализация производится не командой \init, а путем непосредственного задания имени тестируемого шрифта в команде \f ontname и последующего применения команды \startfont.
Приложение Н: Пробные оттиски 355 В заключение этого приложения давайте рассмотрим следующий файл, который можно использовать для тестирования специальных конструкций в математических шрифтах, находясь в рамках соглашений базового формата IfeJC: \raggedright \rightskip=2em plus 5em minus 2em $\hbar \not\equiv B$, but $\sqrt С \mapsto \sqrt x$, $Z \hookrightarrow W$, $Z \hookleftarrow W$, $Z Mongmapsto W$, $Z \bowtie W$, $Z \models W$, $Z \Longrightarrow W$, $Z \longrightarrow W$, $Z Mongleftarrov W$, $Z \Longleftarrow W$, $Z \longleftrightarrow W$, $Z \Longleftrightarrow W$, $\overbrace{\hbox{very long things for testing}}$, $\underbrace{\hbox{very long things for testing}}$, $Z \choose W$, $Z \brack V$9 $Z \brace W$, $Z \sqrt W$, $Z \cong W$, $Z \notin W$, $Z \rightleftharpoons W$, $\widehat Z$, $\widehat{ZW}$, $\widehat{Z+V}$, $\widetilde Z$, $\widetilde{ZV}$, $\widetilde{Z+W}$. \def\sizetest#l#2{$$ \Bigggl{#l}\bigggl{#l}\Biggl{#l}\biggl{#l}\Bigl{#l}\bigl{#l}\left#l \bullet \right#2\bigr{#2}\Bigr{#2}\biggr{#2}\Biggr{#2}\bigggr{#2}\Bigggr{#2}$$} \def\biggg#l{{\hbox{$\left#l\vbox to20.5pt{}\right.$}}} \def\bigggl{\mathopen\biggg} \def\bigggr{\mathclose\biggg} \def\Biggg#l{{\hbox{$\left#l\vbox to23.5pt{}\right.$}}} \def\Bigggl{\mathopen\Biggg} \def\Bigggr{\mathclose\Biggg} \sizetest () \sizetest [] \sizetest \lgroup\rgroup \sizetest \lmoustache\rmoustache \sizetest \vert\Vert \sizetest \arrowvert\Arrowvert \sizetest \uparrow\downarrow \sizetest \updownarrow\Updownarrow \sizetest \Uparrow\Downarrow \sizetest \bracevert{\delimiter"342} \sizetest \backslash/ \sizetest \langle\rangle \sizetest \lbrace\rbrace \sizetest \lceil\rceil \sizetest \lfloor\rfloor $$\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{-l}}}}}}}}}$$ \def\dobig{\do\bigvee \do\bigwedge \do\bigotimes \do\bigoplus \do\bigodot \do\bigcap \do\bigcup \do\biguplus \do\bigsqcup \do\int \do\ointop \do\smallint \do\prod \do\coprod \do\sum} \def\do#l{#l_a~b A} $\dobig$ $$\dobig$$ \bye Если хотите, чтобы я поверил, представьте мне убедительное доказательство. — ВИЛЬЯМ ШЕКСПИР, Отелло (1604) Чертеж в данном случае является очень важным уточняющим элементом. В эвклидовом представлении мы не можем до конца проследить за ходом рассуждения, не обращаясь к чертежу, и, если воображение наше развито не настолько, чтобы мысленно нарисовать фигуру, то мы будем вынуждены начертить ее, если автор не сделал этого за нас. Язык доказательств отличается формальностью и сухостью. Он не похож ни на язык истории, ни на язык драмы, ни на обиходный язык. Это лаконичный, строгий язык, который служит для точного описания точных, но ограниченных объектов. — Ф. Я. ДЭВИС и Р. ХЕРШ, Доказательство (1981)
Предметно-именной указатель
Приложение I: Предметно - именной указатель 357 Автор постарался составить как можно более полный предметный указатель, чтобы можно было отыскать такие вещи, которые запрятались в самых темных уголках этой длинной книги. Поэтому и сам предметный указатель довольно длинный. Краткий обзор простейших элементов системы METAFONT приведен в начале приложения В; краткий обзор стандартных классов символьных лексем приведен в конце главы 6; описания остальных элементов системы вы найдете по ссылкам, приведенным ниже в данном приложении. Номера страниц в предметном указателе выделены подчеркиванием, если на этих страницах содержатся определения или основные источники информации о предметах, на которые идет ссылка. (Информация, которая находится на подчеркнутых страницах, наиболее полна, но не всегда понятна для новичков.) Номер страницы выделен курсивом (например, '125'), если на этой странице приводится содержательный пример использования того, о чем идет речь. В некоторых случаях оказывается оправданным выделение как курсивом, так и подчеркиванием. Когда предметный указатель отсылает вас к странице, на которой содержится упражнение с интересующим вас понятием, иногда дополнительная информация может содержаться в ответе (см. приложение А) к этому упражнению; номера страниц с ответами указываются только в том случае, если в ответе описывается нечто, что не входит в формулировку соответствующего упражнения. Если в пункте указателя содержится ссылка на какой-либо символ, например 'Т', то на указанной странице приводится пример программы, которая рисует этот символ. Символьные лексемы, отмеченные звездочкой (*), являются примитивами системы METAFONT, т.е. встроены в систему. Переопределять их не рекомендуется, так как это не всегда безопасно. 6test . mf , 324 - 325 . # (диез) , см . точные единицы длины . '#' , 212 - 213 . ## (отслеженное уравнение) , 92 - 95 , 251 . ### (уничтоженная независимая переменная) , 96 . #### (выведенное уравнение) , 92 - 93 . *#С (префикс переменной) , 190 , 263 . У , (знак процента) , 55 , 62 . *к (амперсанд) , 225 - 226 , см . сцепление , при именах форматных файлов , 47 , 291 . ' (апостроф или штрих) , 37 , 67 , 93 . " (знак двойной кавычки) , 62 - 63 . "" (пустая цепочка) , 200 , 248 , 266 , 288 , 306 , 340 . '(' , 115 - 117 , 140 , 340 . ( (левая круглая скобка) , 71 , 72 , 73 , 74 - 75 , 83 - 85 , 177 , 222 - 227 . (( , 63 . ) (правая круглая скобка) , 71 , 72 , 73 , 74 - 75 , 83 - 85 , 177 , 222 - 227 . )) , 63 . *[ (левая квадратная скобка) , 21 - 22 , 66 , 67 , 72 , 84 , 92 , 223 - 224 , 310 - 311 , 336 . [[ , 73 . [] (общий нижний индекс) , 68 , 189 , 285 . [1] (отчет о выполненной работе) , 49 , 336 . *] (правая квадратная скобка) , 21 - 22 , 66 , 67 , 72 , 84 , 92 , 223 - 224 , 310 - 311 , 336 . ]] , 73 , 174 , 274 , 311 . *{ (левая фигурная скобка) , 28 - 30 , 72 , 141 , 225 . {{ , 73 , 301 . *} (правая фигурная скобка) , 28 - 30 , 72 , 141 , 225 . }} , 73 , 301 . *+ (знак плюс) , 74 , 75 , 84 , 92 , 223 . *++ (пифагоров плюс) , 78 , 79 , 84 , 223 . (двойная грань) , 129 , 308 - 309 . +++ (тройная грань) , 308 - 309 . *+ - + (пифагоров минус) , 78 , 84 , 223 , 250 . * - (знак минус) , 74 , 75 , 84 , 92 , 223 , 309 . — (связка прямой) , 36 - 38 , 139 - 141 , 246 , 274 . (двойная грань) , 129 , 308 - 309 . (связка с натяжением) , 119 , 139 - 141 , 274 . (тройная грань) , 308 - 309 . - > (разложение макроса) , 56 , 172 , 261 , 263 . *—' (длинное тире) , 318 . _ (подчеркивание) , 61 , 63 , 185 , 277 , 282 . * (звездочка) , 297 - 298 . как приглашение , 43 , 49 , 291 . *как знак умножения , 71 , 74 - 76 , 84 , 85 , 92 , 223 - 224 . ** , как приглашение в командной строке , 43 - 44 , 47 - 52 , 199 , 281 , 291 . как знак возведения в степень , 71 , 76 , 84 , 249 , 263 , 277 .
358 Приложение I: Предметно - именной указатель / (косая черта) , 340 , 341 . ♦как знак деления , 71 , 74 , 75 , 84 , 92 , 94 , 222 - 223 . I (вертикальная черта) , 129 , 309 . *\ (обратная косая черта) , 191 , 248 , 274 . как начало командной строки , 43 , 50 , 52 . W 274 . ♦< (знак меньше) , 76 , 77 , 182 , 222 , 249 . ♦<= (знак меньше или равно) , 76 , 77 , 182 , 222 , 294 . < - (значение аргумента) , 172 . ♦О (не равно) , 76 , 77 , 182 , 222 , 294 . () (угловые скобки) , 61 - 62 . ♦= (знак равно) , 17 , 18 , 35 , 76 , 87 - 97 , 100 , 109 , 177 , 179 , 182 , 183 , 222 , 230 . == , 304 . ♦=: (лигатурная подстановка) , 317 , 318 , 328 , 329 . ♦| = : , 328 , 329 . *| = :> , 329 . ♦=:| , 329 . ♦=:|> , 329 . *|«:| , 329 . *| = :|> , 329 . ♦| = :|» , 329 . 7* (знак не равно) , 294 . ♦> (знак больше) , 76 , 182 , 222 , 249 . >> (выводимое значение) , 53 , 74 . *>= (знак больше или равно) , 76 , 77 , 182 , 222 , 294 . ♦ , (запятая) , 69 , 84 , 85 , 141 , 167 , 177 - 179 , 183 , 329 , 330 . , , , 63 . . (точка) , 55 , 62 , 63 . V , 318 . * . . (свободная связка) , 19 , 27 - 31 , 36 , 139 - 145 , 225 . . . . (связка с ограничением) , 30 - 31 , 56 , 149 , 260 , 274 . ♦; (точка с запятой) , 167 , 181 , 183 , 184 , 199 , 229 , 235 - 236 , 275 , 324 . ;; , 63 . *: (двоеточие) , 181 , 329 - 331 . *: : (локальная метка) , 329 . *| I : (метка левой границы) , 329 . *:= (присваивание) , 40 , 45 , 99 , 100 , 109 , 110 , 167 - 168 , 171 , 177 , 179 , 183 , 188 , 230 , 294 . ? , 53 , 54 - 55 - ??? , 236 , 274 . ! (восклицательный знак) , 53 , 201 . ♦в (конец переменной) , 189 , 263 . ♦в# (суффикс переменной) , 188 , 189 , 190 , 263 , 285 - 286 . А 'а' , 204 . 'А' , 175 , 176 , 260 , 314 - 315 . abort , 324 - 325 . abs (абсолютное значение) , 78 , 94 , 250 , 276 . ♦addto , 130 - 131 , 156 , 163 , 254 - 256 . adjust_fit , 318 - 320 . ♦also , 130 , 232 , 254 - 256 . \altemation , 351 . alvays . iff , 319 , 323 - 324 . ♦and , 76 , 141 , 182 , 222 , 225 , 300 - 301 . ♦angle , 41 , 79 , 83 , 119 , 147 , 223 . ASCII , 61 , 200 , 293 - 295 , 329 . ♦ASCII , 84 , 200 , 223 . aspect_ratio , 106 , 157 , 216 , 281 , 347 . ♦at , 203 , 232 , 264 , 289 , 324 . ♦atleast , 141 , 143 , 225 , 274 . ♦autorounding , 139 , 207 , 216 - 217 , 218 , 223 , 274 , 276 , 283 - 284 . В 'b\ 320 . badio . mf , 53 , 235 . barheight , 108 , 173 , 211 , 314 - 315 . ♦batchmode , 231 , 237 . BCPL - цепочки , 333 . beginchar , 47 , 88 , 108 , 114 - 115 , 119 , 127 , 160 , 168 , 209 , 211 , 216 , 286 , 328 . ♦begingroup , 167 - 169 , 187 , 190 , 222 - 227 , 229 , 248 , 255 , 287 , 301 . beginlogochar , 172 , 314 . \bigtest , 353 . blacker , 105 - 106 , 279 , 282 - 283 . blankpicture , 204 , 275 . ♦boolean , 67 , 68 . bot , 35 , 92 , 159 , 163 , 216 , 284 - ♦boundarychar , 224 , 329 . bp (большой пункт) , 104 , 279 , 280 . buffer size , 238 , 298 . bye , 290 , 290 , 318 , 333 , 336 . byte , 276 , 286 . С с - код , 118 , 336 . с and , 300 - 301 . CAPSULE , 251 . capsule_def , 275 . ее (цицеро) , 104 , 279 , 280 . ceiling , 77 , 78 , 84 , 276 . \centerlargechars , 352 , 353 . change . width , 211 , 287 , 321 . ♦char , 199 , 200 , 226 , 275 . ♦charcode , 118 , 222 , 224 , 232 , 286 , 336 . ♦chardp , 118 , 224 , 232 , 286 , 327 - 328 , 336 .
Приложение I: Предметно - именной указатель 359 ♦chardx , 118 , 224 , 232 , 287 , 336 , 347 . ♦chardy , 118 , 224 , 336 , 337 . ♦charexists , 118 , 222 , 328 , 336 . ♦charext , 118 , 224 , 232 , 328 , 336 . *charht , 118 , 224 , 232 , 286 , 327 - 328 , 336 , 346 , 347 . ♦charic , 118 , 224 , 232 , 287 , 328 , 336 . ♦charlist , 329 , 330 , 343 , 347 . ♦charwd , 118 , 224 , 232 , 286 , 327 - 328 . 336 , 346 , 347 . cheapo , 103 - 105 , 111 , 289 - 290 , 345 . clear_pen_memory , 159 , 284 , 289 , 322 . clearit , 127 , 254 , 286 , 288 , 307 . clearpen , 284 , 286 . clearxy , 286 , 288 . cm (сантиметр) , 30 , 104 , 279 , 280 . cm . base , 47 , 290 , 323 . cmchar , 318 , 319 , 319 , 324 - 325 . cmexlO , 329 - 330 . cmmf , 47 , 291 . cmr9 , 215 , 332 . cmrlO , 113 , 316 - 317 , 331 . cmrlO . mf , 316 - 317 . cmsllO , 113 . cmttlO , 316 - 317 . Computer Modern , семейство шрифтов , 47 , 115 - 117 , 215 , 218 , 290 , 316 - 325 . ♦contour , 130 - 131 , 232 . ♦controls , 31 , 82 - 83 , 141 - 143 , 145 , 164 , 225 . cor , 300 - 301 . ♦cosd , 78 , 84 , 223 . counterclockwise , 276 . craziness , 196 - 197 . ♦cull , 130 , 131 , 163 , 255 - 257 . culldraw , 283 . cullit , 125 , 132 , 254 , 255 , 288 - ♦curl , 29 , 140 - 143 , 225 , 246 . currentbreadth , 322 - 323 . * currentnull , 307 . currentpen , 130 , 159 , 163 , 216 , 282 . currentpicture , 126 , 127 , 128 , 130 , 132 , 203 , 282 - 283 , 307 . currenttransf orm , 107 , 156 , 216 , 280 , 281 , 313 , 322 . currentwindow , 204 , 325 . cut draw , 163 , 283 . cutoff , 163 , 284 - ♦cycle , 27 , 28 , 36 - 40 , 81 , 141 - 143 , 182 , 183 , 222 , 225 . D d , 47 , 88 , 114 , 216 , 286 . 'd' , 306 . ♦day , 224 , 230 , 335 . dd (дидот) , 104 , 279 , 280 . ♦decimal , 199 - 200 , 226 . deer , 277 . ♦def , 48 , 171 - 174 , 177 - 179 . def ault_wt_ , 283 . define . blacker . pixels , 45 , 105 , 118 , 279 , 314 . define_corrected_pixels , 105 , 209 , 279 . 314 . def ine_good . . x_pixels , 211 , 279 , 314 . def ine_good_y_pixels , 211 , 279 , 314 . define_horizontal_corrected_pixels , 216 , 279 , 314 . def ine_pixels , 45 , 104 , 118 , 211 , 279 , 314 . define_whole_blacker_pixels , 214 , 279 . define_whole_pixels , 211 , 279 , 314 . define_whole_vertical_blacker_pixels , 279 . define_whole_vertical_pixels , 216 , 279 , 314 . ♦delimiters , 73 , 192 , 222 , 230 , 233 , 273 , 308 , 311 , 325 . ♦designsize , 224 , 332 . \digits , 351 . dir , 30 , 79 , 80 , 95 - 96 , 147 , 175 - 176 , 187 , 245 , 276 . direction , 81 , 82 , 147 , 247 , 277 . directionpoint , 147 , 277 . ♦directiontime , 147 , 148 , 223 , 257 , 277 , 307 . dishing , 164 , 176 . ♦display , 203 - 204 , 232 . displaying , 280 , 288 , 290 . ditto , 199 , 275 . div , 276 . dot , 318 , 323 . dotprod , 80 , 190 , 250 , 276 . dotsize , 345 , 346 . ♦doublepath , 130 , 131 , 163 , 232 . down , 44 , 275 . downto , 184 , 274 . draw , 19 , 27 - 31 , 33 , 124 , 130 - 132 , 156 , 159 , 164 , 210 , 242 , 282 , 307 . применяемая к одной точке , 34 , 162 , 212 , 265 . drawdot , 43 , 125 , 159 , 162 , 246 , 282 . ♦dropping , 130 , 132 , 232 . ♦dump , 229 , 233 , 273 , 290 , 323 . . dvi , 44 , 52 , 115 , 118 , 335 , 339 , 340 . Б e , 39 - 41 , 285 . 'E' , 108 - 109 , 216 , 314 - 315 . ♦else , 181 - 182 . 191 . ♦elseif , 181 - 182 , 191 . ♦end , 43 , 49 , 167 , 179 , 229 , 233 , 238 , 289 , 299 , 317 , 333 .
360 Приложение I: Предметно - именной указатель endchar , 48 , 114 , 168 , 203 , 286 , 321 , 323 , 342 . ♦enddef , 106 , 171 - 176 , 177 , 187 - 190 . *endf or , 30 , 51 , 183 - 184 , 185 , 262 , 302 . ENDFOR , 57 , 298 , 302 . ♦endgroup , 167 - 169 , 179 , 187 , 190 , 222 - 227 , 229 , 248 , 255 , 288 , 301 , 302 . ♦endinput , 191 , 299 - 300 . ENE , 131 , 218 - 219 , 240 . enormous number , 74 , см . также слишком большое число . eps , 105 , 211 - 212 , 241 , 275 , 322 - 323 . epsilon , 74 - 81 , 127 , 147 , 164 , 241 , 275 . equally_spaced , 302 . erase , 125 , 132 , 179 , 283 , 284 . ♦errhelp , 201 , 231 , 306 . ♦errmessage , 190 , 201 , 231 , 306 . ♦errorstopmode , 231 , 239 , 325 . ESE , 218 - 219 , 240 - 241 . ♦everyjob , 192 , 231 . ♦exitif , 183 , 185 , 188 , 191 , 274 . exitunless , 185 , 274 . ♦expandafter , 191 , 192 , 281 , 298 - 302 , 325 . ♦expr , 172 , 174 , 177 , 178 , 179 , 188 , 222 . (EXPR„) , 56 , 172 , 261 , 263 . expr . mf , 73 , 74 - 83 , 128 - 129 , 144 , 147 - 149 , 154 - 155 , 162 , 185 . ♦extensible , 330 . extra_beginchar , 287 , 289 . extra . endchar , 287 , 289 , 321 . extra . setup , 280 , 282 , 290 . ! Extra tokens will be flushed , 55 - 56 , 236 - 237 . F 'F\ 109 , 216 , 314 - 315 . ♦false , 67 , 76 , 182 , 222 . Fatal base file error , 238 . ♦f i , 181 - 182 , 191 . ! File ended . . . , 299 . fill , 36 - 39 , 121 - 123 , 128 , 130 - 133 , 157 , 179 , 282 , 307 . filldraw , 115 - 117 , 124 - 125 , 130 - 131 , 159 , 160 , 164 , 176 , 242 , 283 , 318 , 322 . ♦Mllin , 106 , 162 , 224 , 259 , 279 , 289 - 290 . fine , 115 - 116 , 318 - 319 , 322 - 323 . fine . lft , 323 . fix . units , 279 . flex , 136 - 137 , 139 , 164 , 185 , 240 - 241 , 279 . ♦floor , 77 , 78 , 75 , 94 , 223 , 264 . f ont . coding . scheme , 288 , 315 , 316 , 332 - 333 . font . extra . space , 288 , 331 . font . identifier , 288 , 315 , 316 , 317 , 332 , 345 . font . normal . shrink , 109 , 287 , 317 , 331 . font . normal , space , 109 , 287 , 317 , 331 , 344 . font . normal . stretch , 109 , 287 , 317 , 331 . f ont . quad , 109 , 287 , 320 , 331 , 344 . f ont . setup , 215 , 317 , 321 - 324 . f ont . size , 107 , 108 , 287 . font . slant , 287 , 317 , 331 , 344 , 347 - 348 . font . x . height , 287 , 331 , 344 . ♦fontdimen , 287 , 330 - 331 , 343 - 344 , 347 . ♦fontmaking , 66 , 106 , 223 , 282 , 327 . \f ontname , 354 . ♦for , 30 , 51 , 125 , 183 - 185 , 191 , 240 , 297 - 303 , 311 . ♦forever , 73 , 183 - 185 , 188 , 191 . ♦forsuffixes , 183 - 184 . ♦from 203 , 232 , 264 , 289 , 324 . fullcircle , 126 , 135 - 136 , 139 , 147 - 149 , 275 , 277 . G generate , 317 , 319 , 323 , 325 . gf , 43 , 253 , 307 , 335 - 337 . gf corners , 288 , 290 , 339 . GFtoDVI , 44 , 49 , 199 , 339 - 348 . gimme , 74 . gobble , 179 , 274 , 301 . gobbled , 274 , 301 - 303 . good . bot , 216 , 284 . good . lft , 216 , 284 . good . rt , 216 , 284 - good . top , 216 , 284 . good . x , 210 , 279 , 284 - good . y , 210 , 216 , 279 , 284 . ♦granularity , 217 , 224 , 274 , 322 . gray , 34? . grayf . mf , 344 - 347 . grayfont , 282 , 286 , 335 , 341 . grayf ontarea , 341 . H h , 34 - 37 , 47 - 48 , 88 - 90 , 114 , 216 , 286 . 'H\ 175 , 177 . half circle , 135 , 148 , 275 . hamburgefonstiv , 353 . ♦headerbyte , 330 , 332 - 333 . ♦hex , 200 , 223 , 293 . hide , 128 , 155 , 179 , 185 , 239 , 274 . ♦hppp , 104 - 105 , 224 , 279 , 280 , 336 . hround , 276 , 280 .
Прилоэюение I: Предметно - именной указатель 361 I Т , 40 , 44 , 51 , 175 - 176 . ! I can't go on , 238 . IBM Corporation , корпорация , 11 . identity , 153 - 157 , 226 , 275 . ♦if , 182 , 191 , 301 . iff , 318 , 319 , 323 . imagerules , 288 , 290 . in (дюйм) , 104 , 279 , 280 . incr , 51 , 188 - 189 , 277 . infinity , 75 - 81 , 275 , 278 . INIMF , 233 , 273 , 290 . \init , 354 . ♦inner , 192 , 230 - 231 . 298 - 299 , 319 , 333 . inorder , 302 . ♦input , 191» 192 , 280 , 299 - 300 , 336 . input stack size , 238 , см . также размер стека . interact , 243 , 274 . ♦interim , 167 - 168 , 242 , 255 , 256 , 283 , 281 interpath , 146 , 278 . intersectionpoint , 119 , 149 , 150 , 190 , 277 . ♦intersectiontimes , 148 , 190 , 225 , 277 , 306 , 310 . inverse , 155 , 276 . ♦inwindow , 203 , 232 , 288 . ! Isolated expression , 235 . italcorr , 115 - 117 , 286 , 315 , 318 , 328 . J ♦jobname , 199 , 226 , 336 . join_radius , 278 . Journal of Algorithms , журнал , 149 - 151 . jut , 174 , 320 . К ♦keeping , 130 , 132 , 232 . keepit , 307 . ♦kern , 109 , 328 , 329 . killtext , 274 , 281 ♦known , 77 , 91 - 94 , 155 , 182 , 222 . L 1 , 320 - 321 . labelfont , 286 , 341 . labelfontarea , 341 . labelfontat , 341 . labels , 119 , 286 - 287 , 339 - 340 . labels . top , 340 . large . pixels , 344 . lcode_ , 285 , 341 . left , 28 , 275 . leftstemloc , 108 , 211 , 314 . ♦length , 78 , 81 , 83 , 223 , 250 . ♦let , 65 , 192 , 230 , 299 - 301 , 311 , 323 . letter . fit , 319 - 320 . lft , 35 , 89 , 92 , 159 , 163 , 284 - lightweight , 345 . ♦ligtable , 109 , 317 , 328 - 329 . local . mf , 290 , 333 . localfont , 51 , 283 , 289 , 290 . log - файл , 54 , 58 , 74 , 242 , 307 - 309 . loggingall , 243 , 274 . logo . mf , 107 - 110 , 210 , 314 - 315 . logolO . mf , 107 , 299 , 313 , 316 . Mowers , 351 . lowres , 208 , 213 , 242 , 282 . lowres . f ix , 215 , 280 , 322 . luxo , 103 - 106 , 111 , 207 , 290 - 291 . M 'M\ 35 , 109 , 212 , 314 - 315 . mag , 51 , 104 - 105 , 110 , 181 , 242 , 281 , 290 , 345 - 346 . magstep , 110 , 281 . makebox , 282 , 287 , 321 . makegrid , 286 . makelabel , 285 , 340 . ♦makepath , 162 , 225 , 259 , 310 . ♦makepen , 159 - 160 , 225 , 276 . maketicks , 282 , 287 , 321 . \math , 353 . max , 77 , 278 , 302 - 303 . ♦message , 73 , 201 , 274 . METAFONT , логотип , 2 - 3 , 34 - 35 , 107 - 111 , 172 - 173 , 196 - 197 , 211 - 212 , 216 , 313 - 316 , 375 . название , 13 - 15 . METAF0NT capacity exceeded , 238 . METAFONT79 , 11 . ♦mexp , 79 , 84 , 223 , 277 , 281 . mf , 43 , 47 . . mf , 48 . mfput , 43 , 199 , 336 . MFT , 274 . min , 77 , 278 , 302 - 303 . ! Missing ')' has been inserted , 265 . \mixture , 52 , 351 . ♦mlog , 79 , 84 , 223 , 277 . mm (миллиметр) , 88 , 103 - 104 , 279 , 280 . mod , 78 , 276 . mode , 50 - 51 , 87 , 103 - 106 , 281 , 289 . mode . def , 106 , 201 , 282 , 290 - 291 . mode_name , 280 . mode . setup , 44 - 46 , 87 , 88 , 103 - 106 , 108 , 127 , 180 , 281 , 290 , 316 , 317 , 342 . mono_charwd , 320 . monospace , 317 - 320 . ♦month , 224 , 335 .
362 Прилооюение I: Предметно - именной указатель N V , 213 - 215 . 'N' , 196 - 197 , 315 . \names , 351 . new . window , 205 . ♦newinternal , 192 , 230 . NNE , 131 , 240 . NNW , 131 , 240 - 241 . nodisplays , 288 , 289 . nodot , 286 , 340 . ♦nonstopmode , 231 , 237 . ♦normaldeviate , 19 , 84 , 195 - 197 , 222 . *not , 76 , 182 , 222 . notransforms , 288 , 289 . ♦nullpen , 160 , 225 , 284 . ♦nullpicture , 127 , 204 , 226 , 284 , 289 . ♦numeric , 67 , 68 , 77 , 100 . numeric_pickup_ , 284 , 322 . ♦numspecial , 232 , 285 , 335 - 336 , 339 - 341 . numtok , 285 . О о , 35 , 46 , 105 , 209 , 212 , 216 , 314 . 'о' , 215 . 'О' , 44 - 49 , 211 , 315 . o_correction , 105 - 106 , 279 . *oct , 200 , 223 , 293 . ♦odd , 182 , 222 , 262 . ♦of , 84 , 141 , 177 - 179 , 199 , 223 - 226 . ! OK , 231 , 236 . \omitaccents , 352 . openit , 288 , 325 . ♦openwindow , 203 - 205 , 232 , 289 , 325 . ♦or , 76 , 182 , 222 , 249 , 300 - 301 . origin , 89 - 90 , 255 , 263 , 275 . ♦outer , 192 , 230 - 231 , 233 , 298 - 299 , 319 , 333 . overdraw , 126 , 255 . P 'P\ 219 . ♦pair , 67 , 68 , 77 . ♦path , 67 , 68 , 183 . ♦pausing , 223 , 243 . pc (пика) , 104 , 279 , 280 . ♦pen , 67 , 68 , 77 , 182 . pen . bot , 163 , 283 . pen . lft , 163 , 283 . pen . rt , 163 , 283 . pen_top , 163 , 283 . ♦pencircle , 33 - 35 , 40 - 41 , 159 - 161 , 162 - 164 , 210 , 212 , 225 . penlabels , 48 , 285 . ♦penoffset , 162 , 224 , 242 , 310 . penpos , 38 - 41 , 49 , 115 , 174 , 284 , 322 . penrazor , 119 , 124 , 159 , 162 , 275 , 309 . penspeck , 276 , 283 . pensquare , 159 , 164 , 275 , 286 . penstroke , 39 - 41 , 150 , 284 . pickup , 33 - 35 , 157 , 159 , 284 - ♦picture , 67 , 68 , 126 . pix . ht , 344 , 345 . pix . picture , 344 , 345 . pix . wd , 344 , 345 . pixels . per . inch , 279 , 280 . plain . mf , 273 - 290 . ♦point , 81 - 82 , 84 , 126 , 145 , 224 , 279 . pool size , 238 , 298 . pos , 322 . ♦postcontrol , 146 , 224 , 279 . posttension , 148 . ♦precontrol , 146 , 224 , 279 . pretension , 148 . ♦primary , 177 , 179 . ♦primarydef , 177 , 190 . ♦proofing , 106 , 199 , 223 , 232 , 282 , 285 , 335 - 336 , 339 . proof off set , 286 , 341 . proofrule , 285 , 335 , 341 . proofrulethickness , 286 , 341 . pt (типографский пункт) , 33 - 35 , 45 , 103 - 104 , 279 , 280 . \punct , 351 . Q <Q\ 219 . quartercircle , 135 , 275 . ♦quote , 178 , 184 , 281 , 299 , 325 . R r , 320 - 321 . 'R\ 219 . \raggedright , 350 . ♦randomseed , 197 , 230 . range , 119 , 150 , 212 , 285 . ♦readstring , 73 , 199 - 200 , 226 . ref lectedabout , 149 , 153 , 154 , 172 , 277 - relax , 43 , 274 , 319 . rep , 344 , 347 . ♦reverse , 141 , 144 , 225 . right , 38 , 80 , Г7Ъ . ♦rotated , 33 - 34 , 37 , 39 , 56 , 80 , 85 , 119 , 126 , 129 , 153 , 224 , 250 . rotatedabout , 278 . rotatedaround , 149 , 154 , 156 , 171 - 172 , 278 . round , 77 , 208 , 214 , 276 , 285 . rt , 35 , 89 , 92 , 115 , 159 , 163 , 284 - rtest . mf , 323 . rulepen , 286 , 287 . runaway , 299 .
Приложение I: Предметно - именной указатель 363 S •S\ 52 , 126 , 247 . safefill , 132 . \sample , 353 . *save , 167 - 168 , 172 , 185 , 190 , 230 , 248 , 256 , 308 , 311 . savepen , 108 , 159 , 283 , 322 . ♦scaled , 33 - 35 , 80 , 85 , 153 , 224 , 255 , 303 . *scantokens , 73 , 191 , 192 , 201 , 263 , 281 , 282 , 298 - 300 , 325 . screen . cols , 205 , 288 , 289 . screen . rows , 288 , 289 . screenchars , 203 , 288 . screenrule , 285 , 288 . screenstrokes , 288 . screentokens , 203 . ♦scrollmode , 73 , 231 , 325 . ♦secondary , 177 , 179 . ♦secondarydef , 177 , 190 . serif . fit , 320 . setu_ , 278 , 303 . shiftdef , 323 . ♦shifted , 80 , 85 , 129 , 153 , 224 . shipit , 43 , 287 , 288 , 307 . ♦shipout , 118 , 222 , 232 , 288 , 307 , 328 , 336 . ♦show , 154 , 231 , 239 , 243 , 262 , 308 . ♦showdependencies , 92 - 94 , 231 , 274 . showit , 43 , 203 , 288 , 288 , 307 . ♦showstats , 231 . ♦showstopping , 223 , 240 , 243 , 274 . ♦showtoken , 192 , 231 , 233 . ♦showvariable , 187 , 189 , 192 , 231 . shrink . fit , 320 - 322 . ♦sind , 78 , 84 , 223 . ♦skipto , 328 , 329 . slant , 117 , 218 , 313 - 315 , 322 , 331 . ♦slanted , 80 , 85 , 117 , 153 , 224 . slantfont , 287 , 341 . slantfontarea , 341 . slantfontat , 341 . smode , 280 . ♦smoothing , 66 , 207 , 217 - 218 , 223 , 274 . softjoin , 270 , 277 . solve , 188 - 189 , 278 , 304 - 306 . (some charht values . . . ) , 328 . ♦special , 232 , 252 - 253 , 285 , 335 - 336 . 339 - 341 . ♦sqrt , 71 , 76 , 84 , 223 . SSE , 218 - 219 , 240 - 241 . SSW , 131 , 240 - 241 . \startfont , 349 , 350 , 354 . ♦step , 30 , 183 . stop , 274 , 323 - 324 . ♦str , 199 - 200 . 226 , 262 , 263 . ♦string , 67 , 68 , 81 . stroke , 318 , 322 . ♦subpath , 82 , 83 , 126 , 141 , 145 , 146 , 200 , 225 , 310 . ♦substring , 81 , 199 , 200 , 226 , 333 . ♦suffix , 173 , 177 , 188 . (SUFFIXn) , 56 , 263 . superellipse , 138 , 149 , 278 . superness , 138 . T "Г , 34 - 35 , 109 , 163 , 211 - 212 , 314 - 315 . takepower , 276 . tensepath , 140 , 276 , 310 . ♦tension , 27 - 28 , 126 , 141 - 144 , 148 , 308 . ♦tertiary , 177 , 179 . ♦tertiarydef , 178 , 190 , 277 . test . mf , 323 - 324 . testfont . tex , 52 , 349 - 354 . TeX , 13 , 46 , 52 , 103 , 108 , 110 , 113 - 114 , 327 , 348 - 355 , 373 . ♦text , 173 , 177 - 179 . \text , 352 . (TEXT„) , 57 , 261 , 263 . . tfm , 51 , 327 - 329 , 345 , 347 . ! This can't happen , 238 . thru , 119 , 150 , 212 , 285 . ♦time , 224 , 230 , 335 . titlefont , 286 , 341 . titlef ontarea , 341 . titlefontat , 341 . ♦to , 203 , 232 , 264 , 289 , 324 . <to be read again> , 235 . tolerance , 188 , 262 , 279 , 305 . top , 35 , 89 , 92 , 115 , 159 , 163 , 216 , 284 . totalnull , 307 . ♦totalweight , 127 , 223 , 304 . tracingall , 242 , 274 , 300 . ♦tracingcapsules , 223 , 231 , 251 . ♦tracingchoices , 223 , 242 . ♦tracingcommands , 223 , 242 . ♦tracingedges , 223 , 242 , 307 - 308 . ♦tracingequations , 92 - 95 , 223 , 242 . ♦tracingmacros , 172 , 223 , 242 . tracingnone , 243 , 274 . ♦tracingonline , 73 , 92 , 223 , 231 , 242 . ♦tracingoutput , 223 , 242 , 308 . ♦tracingpens , 223 , 242 . ♦tracingrestores , 168 , 223 , 242 . ♦tracingspecs , 218 - 219 , 223 , 241 . ♦tracingstats , 223 , 238 - 239 , 241 . ♦tracingtitles , 66 , 106 , 199 , 223 , 242 . ♦transform , 67 , 68 , 69 , 153 - 155 , 172 , 278 . ♦transformed , 85 , 153 - 157 , 224 . transum , 190 . ♦true , 67 , 76 , 182 , 222 . TUG , общество , 11 , 375 .
364 Прилоэюение I: Предметно - именной указатель TUGboat , журнал , 11 , 373 . ♦turningcheck , 124 , 131 , 223 , 241 , 256 , 274 , 308 . ♦turningnumber , 123 , 223 , 269 , 276 . и u , 115 - 116 , 317 - 320 . ! Undefined coordinate , 236 . undelimited suffix parameters , 179 , 188 , 277 , 282 . undraw , 125 , 130 , 132 , 254 , 282 . undrawdot , 125 , 282 . unf ill , 37 , 39 , 121 - 122 , 130 , 138 , 282 . unf illdraw , 125 , 130 , 282 . *unif ormdeviate , 79 , 84 , 195 , 197 , 223 . Union Jack , эмблема , 19 . unitpixel , 275 , 345 . unitsquare , 128 , 140 , 144 , 148 , 275 . unitvector , 250 , 276 . ♦unknown , 91 - 94 , 155 , 182 , 222 . ♦until , 30 , 183 . up , 44 , 141 , 275 . \uppers , 351 . upto , 51 , 184 , 274 . V ♦vardef , 177 , 187 - 190 , 301 . *vppp , 224 , 279 , 336 . vround , 216 , 276 , 280 . w w , 34 - 37 , 47 - 48 , 88 - 90 , 114 - 115 , 118 , 287 - 288 , 320 - 322 . 'w\ 214 . ♦varningcheck , 223 , 281 , 282 . whatever , 95 - 96 , 149 , 169 , 245 , 251 , 275 , 302 . ♦withpen , 130 , 232 , 254 . ♦vithveight , 130 , 232 , 254 , 309 . VNW , 131 , 240 - 241 . WSW , 131 , 240 - 241 . X х - высота , 331 . Xerox Corporation , компания , 332 . xgap , 107 - 109 , 211 . ♦xoffset , 224 , 232 , 320 , 327 , 336 . ♦xpart , 80 , 83 , 150 , 154 , 223 . ♦xscaled , 33 - 35 , 80 , 85 , 153 , 224 , 255 , 303 . ♦xxpart , 84 , 154 , 172 , 223 . xy_swap , 309 . ♦xypart , 154 , 172 , 223 . Y ♦year , 224 , 335 . ygap , 108 , 211 . ♦yoffset , 224 , 232 , 327 , 336 . ♦ypart , 80 , 83 , 154 , 223 , 250 . ♦yscaled , 33 - 35 , 80 , 85 , 153 , 224 , 255 , 303 . ♦yxpart , 154 , 172 , 223 . ♦yypart , 154 , 172 , 223 . z z convention , 19 , 80 , 81 , 263 , 289 . ♦zscaled , 80 - 81 , 85 , 153 , 224 . ztest . mf , 324 . A автоокругление , 140 , 270 . Адаме , Джон (Adams , John) , 371 . аддитивные операторы , 72 . алгебраические операции , 71 - 85 , 221 - 227 , 242 . Алгол , 69 , 101 . Аллен , Фред (Allen , Fred = Sullivan , John Florence) , 97 . альтернативы , 181 - 182 . Американское математическое общество (AMS) , 4 , П . Андерсон , Айзет (Anderson , Izett William) , 311 . аргументы , 171 - 172 , 178 - 179 , 222 , 300 . арифметические операции , 71 - 75 . аффинные преобразования , 258 . Б базовая линия , 87 - 89 , 113 . базовый формат (plain METAFONT) , 46 , 269 - 291 . Баркитт , Уильям (Burkitt , William) , 111 . Безье , Пьер (Bezier , Pierre Etienne) , 26 . Белл , Эрик Темпл (Bell , Eric Temple) , 23 . Берне , Роберт (Burns , Robert) , 311 . Бернштейн , Сергей Натанович , 26 . бесконечные циклы , 184 , 238 - 239 . Бетховен , Людвиг ван (Beethoven , Ludwig van) , 197 . Бибби , Двейн (Bibby , Duane Robert) , 2 - 3 . Биллавала , Назнин (Billawala , Nazneen Noorudin) , 277 , 306 . бинарно перемешанные числа , 149 . бинарный поиск , 188 - 189 , 305 - 306 . Бирс , Амброз (Bierce , Ambrose Gwinnett) , 11 . больше или равно , 77 . Брак , Ричард (Bruck , Richard Hubert) , 41 . Бронте , Эмели (Bronte , Emily Jane) , 85 . (булево выражение) , 182 , 222 . булевы выражения , 182 , 269 .
Пргыосисение I: Предметно - именной указатель 365 Буль , Джордж (Boole , George) , 182 . Бэкас , Джон (Backus , John Warner) , 61 . В валентинка , 146 . ввод текста в интерактивном режиме , 54 , 57 , 73 , 200 , 235 - 237 . вводная часть tfm - файла , 332 . Вебстер , Hoa (Webster , Noah) , 179 . векторы , 20 - 21 , 89 . (величина натяжения) , 141 , 225 . Венецки , Ричард (Venezky , Richard Lawrence) , 205 . внешние ярлыки , 67 , 230 . внутренние величины , 66 - 67 , 100 , 230 , 274 . таблица , 223 - 224 . (внутренняя величина) , 168 , 230 , 277 . Военно - морское исследовательское бюро (Office of Naval Research) , 11 . волосяные линии , 116 - 117 . восьмеричная система счисления , 200 . время в путях , 131 , 144 - 149 . встроенные символы , 330 . (вторичная заготовка пера) , 160 , 225 . (вторичная пара) , 85 , 224 . (вторичная формула) , 83 , 221 . (вторичная цепочка) , 199 , 226 . (вторичное а) , 83 . (вторичное булево) , 182 , 222 . (вторичное вырожденное) , 227 . (вторичное перо) , 160 , 225 . (вторичное преобразование) , 226 . (вторичное числовое) , 84 , 190 , 223 . (вторичный путь) , 141 , 225 . (вторичный рисунок) , 127 , 226 . вывод системы METAFONT , 51 , 54 , 327 - 337 . (выделенные параметры) , 177 . вызовы , 65 - 67 , 168 , 187 , 227 , 231 , 301 . выпуклые многоугольники , 131 , 159 , 309 - 310 . (выражение) , 179 , 221 . (выражение - а) , 83 . (выражение - пара) , 85 , 225 . (выражение - перо) , 159 , 160 , 226 . (выражение - преобразование) , 226 . (выражение - путь) , 141 , 225 . (выражение - рисунок) , 127 , 226 . (выражение - цепочка) , 85 , 199 , 226 . выражения , 71 - 85 , 221 - 227 . выражения - группы , 169 , 172 . выражения - пары , 85 , 183 , 270 . выражения - перья , 159 - 160 , 270 , 310 . выражения - преобразования , 153 - 155 , 182 , 190 , 270 . выражения - пути , 141 - 146 , 270 . выражения - рисунки , 127 , 270 . трансформирование , 156 , 309 . выражения - цепочки , 199 - 201 , 270 , 298 . (вырожденное выражение) , 227 . вырожденные выражения , 221 , 227 , 262 , 274 , 301 , 304 . высота , 113 . вычислимая функция , 306 . вычитание , рисунков , 127 , 256 . векторов , 21 . Г Гарднер , Мартин (Gardner , Martin) , 138 . Гейн , Пит (Hein , Piet) , 138 , 243 . гекс , 19 - 20 , 40 - 41 . гистограмма , 263 . глубина , 113 . Голанд , Филемон (Holland , Philemon) , 63 . Голсуорси , Джон (Galsworthy , John) , 227 . Гомер (Homerus) , 63 . готическая буква , 306 . грамматика , правила , 61 - 62 . грани , 128 . границы пикселей см . грани . границы , 36 - 41 , 135 - 137 . Гримм , Вильгельм (Grimm , Wilhelm Karl) , 85 . Гримм , Якоб (Grimm , Jakob Ludwig Karl) , 85 . группы , 167 - 169 , 179 . Гу Гуоянь (Gu Guoan) , 15 . Гуди , Фредерик (Goudy , FVederic William) , 31 . д да Винчи , Леонардо (da Vinci , Leonardo) , 31 . Дарвин , Чарльз (Darwin , Charles Robert) , 69 . дважды залитые пиксели , 122 - 124 . движение против часовой стрелки , 123 , 131 , 241 , 267 . двойная кавычка , 62 - 63 , 199 . де Кастелье , Поль (de Casteljau , Paul de Faget) , 26 . Декарт , Рене (Descartes , Rene) , 18 , 23 , 31 . декартовы координаты , 17 - 18 , 203 . декларативный против императивного , 99 . деление , 71 , 74 , 75 , 92 , 94 . числовых лексем , 73 , 85 . ' Дерек , Бо (Derek , Во) , 299 . десятичная точка , 62 - 63 . (десятичная цифра) , 62 . десятичное представление , 200 . деформированный эллипс , 138 , 173 . Джонсон , Самюэль (Johnson , Samuel) , 179 . Джонстон , Эдвард (Johnston , Edward) , 41 . Джотто (Giotto de Bondone) , 151 . диагностика , 241 - 243 , 271 , 298 .
366 Приложение I: Предметно - именной указатель диакритические знаки , 327 , 330 . Диккенс , Чарльз (Dickens , Charles John Huffam) , 157 . длинное тире , 318 . Доппинг , Олле (Dopping , Olle) , 193 . драйверы устройств , 335 , 337 . Дрейтон , Майкл (Drayton , Michael) , 291 . дроби , 73 , 74 - 75 , 84 , 85 . дыры , 122 . Дэвис , Филип (Davis , Philip Jacob) , 355 . Дюрер , Альбрехт (Durer , Albrecht) , 25 , 31 . Е единицы , 45 , 103 - 111 , 279 - 280 . единицы измерения , 104 , 279 . таблица , 104 . (есть) , 177 , 183 , 230 . Ж желудок , 181 , 229 , 297 . "Жизнь" , игра , 133 . 3 завершение работы METAFONT , см . end . зависимые переменные , 93 - 95 , 100 , 236 . загиб , 29 , 140 , 246 . (заголовок) , 199 , 229 - 230 , 335 . заготовки перьев , 160 - 161 , 182 , 261 , 275 , 310 . задание размера шрифта , 108 , 331 . заостренная перьевая линия , 40 . запрещенные лексемы , 192 , 230 - 231 , 298 . Запф , Герман (Zapf , Hermann) , 7 , 233 . зарезервированные лексемы , 185 , 277 , 282 . засечки , 164 , 174 - 177 , 320 . засеянные участки , 195 . звезда , 126 . знаки препинания , 318 . золотое сечение , 22 . зубчики , 213 . И идентификатор шрифта , 316 . иерархия , 221 . изолированные математические символы , 328 , 331 . имена файлов , 48 , 51 , 192 , 336 , 341 . императивный против декларативного , 99 . (имя файла) , 191 - 192 . инверсия , рисунков , 127 . векторов , 21 . интерактивное воздействие , 54 - 57 , 73 , 200 - 201 , 203 - 205 , 231 , 235 - 237 . интерполяция функции , 306 - 307 . интерполяция , 14 , 146 , 306 - 307 . Ио (1о) , 45 , 52 , 59 . Исис (Isis) , 52 . использование памяти , 238 - 239 . исправление ошибок , 241 - 243 , 298 . истина , 76 . К Камден , Уильям (Camden , William) , 63 . Кандалл , Франк (Cundall , Frank) , 311 . Кандинский , Василий Васильевич , 15 . капсулы , 171 , 178 , 184 , 222 , 251 , 259 , 266 , 275 . Картер , Мэтью (Carter , Matthew) , 219 . Каупер , Уильям (Cowper , William) , 63 . Кафка , Франц (Kafka , Franz) , 352 . квадратные скобки , 21 - 22 , 66 , 67 , 72 , 84» 92 , 223 - 224 , 310 - 311 , 336 . квадратный корень , 76 , см . также sqrt . Квик , Джонатан (Quick , Jonathan Horatio) , 66 , 149 . кернинг , 109 , 328 - 329 . китайские иероглифы , 15 , 118 , 336 . клавиша (return) , 43 . Кнут , Джилл (Knuth , Nancy Jill Carter) , 11 , 146 , 149 . Кнут , Дональд (Knuth , Donald Ervin) , 2 - 3 , 11 , 15 , 146 , 204 , 218 , 267 , 294 , 303 , 316 , 320 , 357 , 373 . (код) , 329 . коды , 293 - 295 . Колборн , Дороти (Colburn , Dorothy) , 119 . колоколовидное распределение , 263 . (команда addto) , 232 . (команда charlist) , 329 , 330 . (команда cull) , 130 , 232 . (команда delimiters) , 230 . (команда everyjob) , 231 . (команда extensible) , 330 . (команда fontdimen) , 330 . (команда headerbyte) , 330 . (команда interim) , 167 , 230 . (команда let) , 230 . (команда ligtable) , 329 . (команда message) , 201 , 231 . (команда newinternal) , 230 . (команда protection) , 230 . (команда randomseed) , 230 . (команда save) , 167 , 230 . (команда shipout) , 232 . (команда show) , 231 . (команда special) , 232 . (команда вывода на экран) , 232 . (команда описания метрики) , 333 . (команда открытия окна) , 203 , 232 . (команда переключения режима) , 231 . (команда , применяемая к рисунку) , 130 , 232 .
Приложение I: Предметно - именной указатель 367 (команда) , 229 . команда тестирования , 52 . командная строка , 50 , 199 , 280 , 288 , 313 . команды , 167 , 229 - 232 , 242 , 333 . комментарии , 55 , 62 - 63 . коммутативность , 259 . комплексные числа , 80 . (компонента пары) , 223 . (компонента преобразования) , 223 . Конвей , Джон (Conway , John Horton) , 133 . конец файла , 299 , см . также end . конечный символ , 52 , 350 - 351 . константы , 71 , 74 , 275 - 276 . контрольная сумма , 332 , 336 , 337 . контрольные точки , 25 - 31 , 82 - 83 , 145 , 241 . (контрольные точки) , 141 , 225 . контур , 133 . кончик в форме ромба , 160 - 161 , 309 . координата х , 17 - 19 . координата у , 17 - 19 . координаты , 17 - 23 , 35 , 121 , 203 , 205 . косинус , 79 , 80 , см . также cosd . крайние точки , 140 , 162 - 163 . красивая фигура , 197 . кривые , 25 - 31 , см . пути . круг , 135 - 136 , см . также окружность . круглые скобки , 63 , 71 , 72 , 73 , 84 , 140 , 222 - 227 , 258 . кубические корни , 189 . кубические кривые Безье , 26 , 218 . кубы , 125 . курсив , 67 , 218 , 354 . курсивные поправки , 114 , 117 , 286 , 288 , 316 , 327 - 328 , 331 . Кэмпбелл лорд (Campbell , John Campbell) , 371 . Л Ламэ , Габриэль (Lame , Gabriel) , 138 . Ларошфуко , Франсуа (La Rochefoucald , Francois VI) , 325 . ле Бё , Пьер (le Be , Pierre) , 219 . Лебан , Брюс (Leban , Bruce Philip) , 254 , 281 , 307 . левосторонний опасный поворот , 155 . лексемы , 54 - 55 , 61 - 63 , 222 . (лексемы - параметры) , 177 - 178 . лексемы - цепочки , 61 - 63 . (лигатурная подстановка) , 329 . лигатуры , 317 - 318 , 327 - 329 . линейные зависимости , 94 - 95 . линейные формы , 75 , 94 . Линней , Карл (Linne , Karel von = Linnaeus) , 337 . лишние уравнения , 93 - 94 . логарифм , см . mlog . логотип METAFONT , 1 - 3 , 34 - 35 , 107 - 111 , 172 - 173 , 196 - 197 , 211 - 212 , 216 , 313 - 316 . логотипы , 4 , 109 , 126 , 149 - 151 . ложь , 76 . Локиер , Норман (Lockyer , Sir Joseph Norman) , 69 . м магниты , 72 - 73 . Майес ван дер Ро , Людвиг (Mies van der Rohe , Ludwig) , 197 . макросы специального назначения , 172 , 260 . макросы , 48 - 49 , 65 , 126 , 171 - 179 , 187 - 191 , 297 - 311 . максимум , 77 . Малфорд , Эдвард (Mulford , Clarence Edward) , 101 . массивы переменных , 66 - 69 . масштабирование , 33 , 67 . Матфей , святой , 185 . меньше или равно , 77 . метадизайн , 13 - 15 , 115 - 117 , 306 . метаразработка см . метадизайн . меташрифт , 13 - 15 , 110 , 204 , 313 - 316 . (метка) , 329 . метки в описании метрики шрифта , 329 . метки на пробных оттисках , 49 , 199 , 286 - 287 . нехватка места , 49 , 340 . Мёбиус , Фердинанд (Mobius , August Ferdinand) , 126 . Мерк , Джон (Mirk , John) , 325 . метрика шрифта , 51 , 233 , 327 - 333 . миллиметровая бумага , 17 , 114 , 121 , 200 . минимум , 77 . модельная кривизна , 143 . модуль числа , 78 , 94 . Моксон , Джозеф (Moxon , Joseph) , 337 . Мор , Томас (More , Sir Thomas) , 227 . Морисон , Стэнли (Morison , Stanley) , 11 , 295 . музыка , 195 , 197 . мультипликативные операторы , 72 . н наибольшее целое , 77 . наклон , 67 , 117 , 218 , 331 . наклонные шрифты , 341 , 347 - 348 . намеренные ошибки , 53 . направление обхода , 131 . направления по компасу , 38 , 131 , 218 - 219 , 240 - 241 . настройка вручную , 207 . (натяжение) , 141 , 225 . натяжение , 27 , 141 , 148 , 225 . натянутая линия , 139 .
368 Приложение I: Предметно - именной указатель Национальный научный фонд (National Science Foundation) , 11 . начало сеанса , 51 , 107 , 271 , 289 . (начальное значение) , 183 . начальный символ , 52 , 351 . невозможный куб , 125 . невыделенные аргументы , 179 . (невыделенные параметры) , 177 . негатив , 127 , 130 . недостижимая лексема , 298 . независимые переменные , 93 - 95 , 100 , 236 . неизвестное числовое , 91 - 95 . неизвестные величины , не числовые , 96 . неквадратные пиксели , 106 , 157 , 216 . нелинейные уравнения , 96 - 97 , 188 - 189 , 304 - 306 . (необязательный переход) , 329 . не равно , знак , 76 . необычные пути , 122 - 123 , 131 , 133 , 148 , 164 , 240 - 241 . неполная цепочка , 62 - 63 . неправда , 10 , 243 . несовместные уравнения , 93 , 325 . (нижний индекс) , 66 . нижние индексы , 66 - 68 . нож , 36 . ноль , 248 . номинальный размер , 108 , 331 - 332 , 336 . Нор , Питер (Naur , Peter) , 61 , 101 . нулевая скорость , 148 , 310 . О обволакивание , 130 - 131 , 162 , 242 . обозначение с квадратными скобками , см . помещение в промежуточное положение , обращенный путь , 131 . общение с METFIFONT , 54 - 57 , 73 , 200 - 201 , 203 - 205 , 231 , 235 - 237 . общество пользователей TeX , см . TUG . общие нижние индексы , 68 , 189 - 190 . объединение , 132 . (объявление) , 68 , 183 . объявления , 68 - 69 . объявления типов , 68 . (объявляемая переменная) , 69 , 187 , 189 . (объявляемый суффикс) , 69 . оверлеи , 307 . ограниченная кривая , 31 , 139 , 144 . ограничивающая рамка , 34 , 47 , 88 , 113 - 119 , 287 , 319 , 327 . ограничивающий треугольник , 31 , 139 , 143 . однородные преобразования , 258 . (окно) , 203 , 232 . округление до большего , см . ceiling . округление до меньшего , см . floor . округление , 46 , 48 , 62 , 207 - 219 , 320 . окружность , 135 - 136 , 160 . октанты , 131 , 218 - 219 , 240 - 242 . опасный поворот , 9 , 22 , 118 - 119 , 127 , 155 . операнды , 71 - 72 . (оператор вывода сообщения) , 201 , 231 . (оператор лигатуры) , 329 . операторы , 71 - 72 , 242 . (определение) , 177 . (определение уровня) , 190 . определения , 171 - 179 , 187 - 192 . орнамент , 155 - 156 . Оруэлл , Джордж (Orwell , George = Blair , Eric Arthur) , 97 . остаток , 78 . ось , 115 . отбрасывание лексем , 55 - 56 , 231 , 236 - 237 . (отношение) , 182 , 222 . отношения , 70 - 77 , 182 - 183 . отражение , 67 . отступ от края рамки , 22 , 46 - 47 , 319 - 320 . оцифровка , 123 , 161 , 207 - 219 , 242 . очертания на фоне неба , 263 . ошибки при наборе , 57 , 236 . ошибки при печати , 57 , 236 . п Палайс , Ричард (Palais , Richard Sheldon) , 4 . параллелограмм , 305 - 306 . параллельные линии , 95 . (параметр) , 190 . параметрические файлы , 107 , 313 , 316 . параметры , 8 , 13 - 15 . шрифта , 107 , 115 - 116 , 317 . макроса , 171 - 179 , 187 - 190 . (параметры окна) , 203 , 232 . Пейджет , Френсис (Paget , Francis Edward) , 291 . (первичная заготовка пера) , 160 , 225 . (первичная пара) , 85 , 224 . (первичная формула) , 82 , 179 , 221 . (первичная цепочка) , 199 , 226 . (первичная числовая лексема) , 84 , 222 . (первичное булево) , 182 , 222 . (первичное вырожденное) , 227 . (первичное перо) , 160 , 225 . (первичное преобразование) , 226 . (первичное числовое) , 83 - 84 , 222 . (первичный путь) , 141 , 225 . (первичный рисунок) , 127 , 226 . перевод в пиксельные единицы , 271 , 280 . пережевывание , 191 . переинициализация переменной , 100 , 169 . перелет , 35 , 46 , 105 , 209 , 212 , 216 .
Прилоэюение I: Предметно - именной указатель 369 (переменная) , 66 , 67 , 222 . переменные , 65 - 69 , 71 . переинициализация , 100 , 169 . пересечение линий , 95 . путей , 148 - 149 . рисунков , 132 . переходы на краях линий , 242 . перпендикулярность , 41 , 80 , 95 , 247 . перьевые линии , см . stroke , перья , 33 - 51 , 159 - 164 , 309 - 310 . перья с широким кончиком , 37 - 41 , 163 - 164 , 174 - 177 . пик , 148 . пиксели , 17 , 121 , 271 , 336 . пифагорово вычитание , 78 , 84 , 223 , 250 . пифагорово сложение , 78 , 79 , 84 , 223 . (пифагоровы плюс или минус) , 84 , 223 . плоские кляксы , 208 . (плюс или минус) , 84 , 223 . подвыражение - путь , 141 , 225 . подвыражения , 71 . подпрограммы , см . макросы , подравнивание , 125 , 132 , 163 , 254 - 257 , 308 . подставляемый текст , 171 , 178 , 231 . подцепочки , 200 . позиции в стеке , 239 . позиции символов в шрифте , 118 - 119 , 293 - 295 , 332 . показатель деформированности , 138 . полиномы Бернштейна , 26 , 145 , 164 , 258 , 320 - 321 . (положение на экране) , 203 , 232 . (помеченный код) , 330 . помещение в промежуточное положение , 21 - 23 , 26 , 75 , 80 , 84 , 92 , 145 , 310 - 311 . помощь , 55 - 57 , 201 , 236 - 237 . порядок выполнения операций , 72 - 73 , 149 , 259 , 301 . почти оцифрованный символ , 308 . (правая сторона) , 100 , 183 . правда , 10 , 229 , 233 . правила синтаксиса , 61 - 62 . (предел) , 183 . (предложение с with) , 130 , 132 . предшествование , 72 - 73 , 83 - 85 , 149 , 301 . (преобразователь) , 85 , 224 . "привирает" на х , 94 . преобразование см . transform , прерывание работы METAFONT , 231 , 239 - 240 , 325 . примитивные коды , 56 . примитивы , 65 , 221 , 357 . принадлежность точки прямой , 95 - 96 . принцип вычитания векторов , 21 . приоритет операций , 72 - 73 , 83 - 85 , 149 , 221 , 301 . (присваивание) , 100 . присваивания , 40 , 45 , 99 - 100 , 110 , 171 . приставка "мета - " , 13 , 15 . пробелы , 55 , 62 , 248 . пробный путь , 247 . пробные оттиски , 339 - 355 . с низким разрешением , 111 , 339 . проверка равенства , 304 . (программа) , 167 , 229 . (программа лигатур) , 329 . (прогрессия) , 183 . произведение , 71 , 74 - 76 , 80 , 91 - 92 , 94 . промежуточные точки , 21 , 25 . пропадание значений , 68 , 100 , 168 - 169 . простые числа , 185 . процесс пищеварения , 191 , 229 - 233 . процесс разложения , 191 - 192 , 297 - 303 . прямые линии на пробных оттисках , 340 - 341 . псевдодрайвер , 324 . пупырышки , 208 - 209 , 216 . пустое утверждение , 167 , 229 . пустой список цикла , 183 , 311 . пустой текстовый аргумент , 311 . пути , 25 - 31 , 135 - 151 . путь в форме многоугольника , 36 , 309 . путь в форме ромба , 160 . Р равенство против уравнения , 183 . равносторонний треугольник , 37 , 215 . разделители групп , 167 - 169 , 179 , 301 . разделители , 73 , 178 , 222 , 266 , 301 . разложимые лексемы , 191 , 242 . размер буфера , 238 , 298 . размер стека 238 , 299 . разность рисунков , 127 , 256 . разрешение , 18 , 50 - 51 , 103 - 111 , 128 . рамка , см . ограничивающая рамка . Рамшо , Лайл (Ramshaw , Lyle Harold) , 332 . Раннинг , Теодор (Running , Theodore Rudolph) , 59 . Раскин , Джон (Ruskin , John) , 151 . распечатки программ для METAFONT , 274 . расстояние , 88 , 96 . растр , 17 , 103 , 121 , 207 . растяжимость , 330 . ребристость , 115 . редактирование , 57 . режим proof , 104 - 105 , 116 , 282 , 339 . режим smoke , 50 , 87 , 105 , 282 , 339 . Рейнолдс , Ллойд (Reynolds , Lloyd Jay) , 165 . рекурсия , 239 . рецепты , 14 .
370 Прилоэюение I: Предметно - именной указатель рисование одной точки , 34 , 162 , 212 , 265 . рисунки , 121 - 133 . рот , 181 , 191 , 297 . рубленый шрифт , 40 , 117 , 317 , 320 . С Сазерлэнд , Айвен (Sutherland , Ivan Edward) , 133 . Светоний , Гай (Suetonius Tranquillus , Gaius) , 193 . Свифт , Джонатан (Swift , Jonathan) , 111 , 133 . свободная кривая , 139 . (связка путей) , 141 - 142 , 183 , 225 . селективное дополнение , 132 . семантика , 62 . сердечко , 146 . Серлио , Себастьяно (Serlio , Sebastiano) , 31 . серые шрифты , 339 , 342 - 347 . сетка , 17 , 121 , 287 . сжимаемость , 331 . символы восточных алфавитов , 15 , 118 , 336 . символы подчеркивания , 61 , 63 , 185 , 277 , 282 . символьные лексемы , 61 - 63 , 65 . симметрическая разность , 132 . синтаксис , 62 . синус , 78 , 80 , см . также sind . (скалярный оператор умножения) , 84 , 223 . скорость выполнения операций , 51 , 111 , 153 , 156 , 159 , 240 , 242 , 246 , 256 , 276 , 277 , 289 , 303 , 309 , 310 . слишком большое число , 74 , 248 . сложение векторов , 20 , 80 . сложение рисунков , 127 , 129 , 257 . (сложносоставное) , 229 . сложносоставное утверждение , 167 , 229 . случайность , 195 - 197 . случайные числа , 195 - 197 . смещение рамки , 114 , 118 . совместные индексы см . общие нижние индексы , содержание книги , 5 - 6 . сообщения об ошибках , 53 - 58 , 235 - 240 . Соуфолл , Ричард (Southall , Richard Francis) , 188 . (сохраняя или отбрасывая) , 130 , 232 . специальные единицы измерения , 104 , 107 . (список байтов) , 330 . (список объявляемых) , 69 . (список символьных лексем) , 167 , 230 . (список суффиксов) , 183 . (список утверждений) , 167 , 229 . (список цикла) , 183 , 311 . (список чисел) , 330 . сравнение , 70 - 77 , 92 , 182 . Станфорд , Латроп (Stanford , Jane Elizabeth Lathrop) , 352 . Станфорд , Лилэнд (Stanford , Amasa Leland) , 352 . Станфордский университет , 137 , 352 . стиль печатной машинки , 67 , 117 . стирание лексем , 54 - 55 , 237 . Стравинский , Игорь Федорович , 205 . Стриндберг , Август (Strindberg , Johan August) , 197 . структура граней , 128 - 129 , 308 - 309 . структуры данных , 65 - 69 . сумма , рисунков , 127 , 129 - 130 , 256 . преобразований , 190 . векторов , 21 . (суффикс) , 66 , 173 , 188 , 200 . схема кодировки шрифта , 316 . сцепление путей , 82 - 83 , 135 , 139 , 141 , 142 , 257 . цепочек , 81 , 96 - 97 , 199 , 290 , 298 , 321 т таблицы структурных элементов: внутренние величины , 223 - 224 . единицы измерения , 104 . классы символов , 63 . коды символов , 293 - 294 . метки на пробных оттисках , 340 . параметры f ontdimen , 331 . примитивы языка , 269 - 273 . разложимые лексемы , 191 - 192 . типы , 67 . Твен , Марк (Twain , Mark = Clemens , Samuel Langhorne) , 157 . текст цикла , 183 - 184 , 231 , 298 . текстовые аргументы , 231 , 300 - 302 , 311 . тильда , 164 . Тингуэли , Джин (Tinguely , Jean) , 15 . (тин) , 68 , 183 . (тип параметров) , 177 . типы , 67 . Тобин , Джорджия (Tobin , Georgia Kay Mase) , 4 , 252 . Томсон , Джеймс (Thomson , James) , 201 . тончайшие линии , см . волосяные линии . Тори , Жоффрей (Тогу , GeofTroy) , 31 . Торо , Генри Дэвид (Thoreau , Henry David) , 233 . точечное произведение , 80 . точка отсчета , 17 , 88 , 113 . точка с запятой , 167 , 181 , 183 , 184 , 199 , 229 , 235 - 236 , 275 , 324 . точки неоднозначности , 162 , 210 - 212 , 216 . точки перегиба , 30 - 31 , 139 . точность , 62 , 74 - 81 , 155 , 249 .
Приложение I: Предметно - именной указатель 371 точные единицы длины , 44 - 4Ъ 103 - 111 , 114 - 115 , 280 , 327 . траектории , см . пути , трансформации , 56 , 153 - 157 . Траян (Trajanus) , 165 . (третичная пара) , 85 , 225 . (третичная формула) , 83 , 179 , 221 . (третичная цепочка) , 199 , 226 . (третичное булево) , 182 , 222 . (третичное вырожденное) , 227 . (третичное перо) , 160 , 226 . (третичное преобразование) , 226 . (третичное числовое) , 84 , 223 . (третичный путь) , 141 , 225 . (третичный рисунок) , 127 , 226 . треугольник , 36 - 37 , 215 . тригонометрические функции , 78 , 80 , 143 , 189 . У Уайльд , Оскар (Wilde , Oscar Fingal O'Flahertie Wills) , 333 . увеличение , 50 - 52 , 103 - 111 . увеличенные перья , 309 - 310 . угловые пиксели , 106 . угловые скобки , 61 - 62 . угол наклона пера , 33 - 34 , 38 - 40 , 164 , 176 . узор см . орнамент . Уилкинс , Джон (Wilkins , John) , 4 , 295 . Уиллис , Эллен (Willis , Ellen Jane) , 169 . (указатель направления) , 141 , 225 . умножение , 71 , 74 - 76 , 80 , 91 - 92 , 94 . вектора на скаляр , 21 . (умножить или поделить) , 84 , 223 . Уоррен , Мэрси (Warren , Mercy Otis) , 371 . упражнения , 10 , 17 - 243 . (уравнение) , 100 . уравнения , 17 , 18 , 35 , 87 - 97 , 100 , 153 , 183 . нелинейные , 96 - 97 , 188 - 189 , 304 - 306 . переполнения 340 . (условие) , 181 . (условие выхода из цикла) , 183 . условия , 181 - 183 , 191 , 231 , 270 . условное и/или , 300 - 301 . (утверждение) , 167 , 183 , 229 . утверждения , 167 , 229 - 233 . обзор , 272 - 273 . Ф файл - стенограмма , 54 , 58 , 74 , 242 , 307 - 309 . файлы - драйверы , 316 - 318 . файлы - программы , 316 , 318 . файлы - псевдодрайверы , 323 - 325 . файлы - утилиты , 323 - 325 . фасолевидная фигура , 27 - 28 , 33 - 34 , 36 - 37 . фигурные скобки , 28 - 30 , 72 , 141 , 225 . физиология системы METAFONT , 181 , 191 , 229 , 297 , 356 . Фонд развития систем (System Development Foundation) , 11 . фоновый символ , 52 , 350 - 351 . Фонт , Фрей Педро (Font , Fray Pedro) , 151 , 243 . форматный файл , 46 - 47 , 273 , 290 - 291 , 316 , 319 . Фултон А . Дж . , (Fulton , A . G . ) , 169 . Фурнье , Пьер (Fournier , Simon Pierre) , 333 . X Хаггард , Райдер (Haggard , Sir Henry Rider) , 119 . Хальтен , Понтус (Hulten , Karl Gunnar Pontus) , 15 . Хербин , Аугусте (Herbin , Auguste) , 15 . Херш , Рубен (Hersh , Reuben) , 355 . Хобби , Джон (Hobby , John Douglas) , 11 , 15 , 142 , 143 , 161 , 263 , 297 . "хорошее" положение , 189 . ц целые числа , 77 - 78 . (цепочка цифр) , 62 . (цикл) , 183 . циклы , 181 , 191 , 238 - 239 , 271 , 302 - 303 , 311 . ч часть от пути , см . помещение в промежуточное положение , черные пиксели , 344 - 345 . (четыре кода) , 330 . четырехточечный метод , 25 - 26 , 144 . число обходов , 122 , 123 , 124 , 131 , 148 , 159 . (числовая лексема) , 62 , 247 . (числовое выражение) , 84 , 223 . (числовой оператор) , 84 , 223 . числовые выражения , 84 - 85 , 269 . числовые лексемы , 61 - 62 , 178 . максимальное значение , 62 . округленные дробные значения , 62 . ш (шаг) , 183 . ("шапка" бинарной операции) , 177 , 190 . ("шапка" определения) , 177 . ("шапка" параметров) , 177 . ("шапка" переменной - макроса) , 190 . ("шапка" цикла) , 183 . Шекспир , Вильям (Shakespeare , William) , 185 , 267 , 355 .
372 Прилоэюение I: Предметно - именной указатель шестнадцатеричная система счисления , 200 , 293 . ширина , 113 . шрифт с одинаковыми межбуквенными промежутками , 318 , 320 . шутки , 10 , 243 . э Эветс , Л . K . (Evetts , L . С) , 165 . Эзоп (iEsopus) , 352 . (экранные координаты) , 203 , 232 . экспонента , см . техр . экстренная остановка , 238 . (элементарная связка) , 141 , 225 . (элементарное число) , 84 , 222 . Элингхэм , Уильям (Alingham , William) , 201 . эллипс , 135 , 138 . Эллис , Хайвлок (Ellis , Henry Havelock) , 23 . Эль Пало Альто (El Palo Alto) , 136 - 138 , 151 , 240 - 241 . Эсхилл (^Eschylus) , 59 . эффективность , 51 , 111 , 153 , 156 , 159 , 240 , 242 , 246 , 256 , 276 , 277 , 289 , 303 , 309 , 310 . Я язык FORTRAN , 249 . язык Pascal , 66 . язык SIMULA67 , 187 . ярлык , 65 - 67 , 168 , 187 , 230 - 231 .
Приложение I: Предметно-именной указатель 373 Чем больше мы ищем, тем больше блуждаем. — МЕРСИ ОТИС УОРРЕН, Мистеру Адамсу (1773) Теперь я должен снять с души своей тяжелый груз. Я считаю настолько важным, чтобы в каждой книге был предметный указатель, что внес на рассмотрение парламента проект закона, о лишении автора его авторских прав за публикацию книги без предметного указателя; более того, за такое правонарушение он будет обязан выплатить денежный штраф. Это при том, что, в связи с некоторыми техническими трудностями, мои собственные книги до сих пор оставались без предметного указателя. ЛОРД КЭ МП БЕЛЛ, Жизнеописания Верховных Судей Англии, том 3 (1857)
Общество пользователей
Приложение J: Общество пользователей TeX 375 Это приложение посвящено группированию иного рода: пользователи системы Т£Х всего мира объединились и образовали .Общество пользователей TeX (TeX Users Group, сокращенно TUG), чтобы име/гъ возможность обмениваться информацией об общих проблемах и цутях их преодоления. С 1980 года общество выпускает журнал под названием TUGboat, в котором публикуются статьи, посвященные всем аспектам систем TeX и METAFONT. TUG имеет сеть местных координаторов, через которых люди, имеющие компьютеры с одинаковой конфигурацией, могут общаться. Время от времени проводятся краткие курсы занятий, целью которых является углубленное изучение специальных вопросов; видеозаписи этих занятий можно взять напрокат. Не реже раза в год проходят общие собрания членов TUG. На этих собраниях вы можете приобрести футболки с логотипом METAFONT. Информацию об условиях вступления в TUG и подписке на TUGboat можно получить по адресам ТЕХ Users Group email: TUGQtug.org internet: http://www.tug.org/ Общество TUG призвано служить интересам людей, которых объединяют общие интересы, связанные с использованием TjrX — системы набора технических текстов и metrfont — системы разработки шрифтов. ОБЩЕСТВО ПОЛЬЗОВАТЕЛЕЙ ТЕХ, Устав, пункт II (1983) Не тяни — поторопись и сегодня подпишись! Адрес прежний: TeX Users Group email: TUGOtug.org internet: http://www.tug.org/ — ДОНАЛЬД Э. КНУТ, Все про ТЕХ (1996)
Научно-популярное издание Дональд Эрвин Кнут Все про METAFONT Литературный редактор Ж. Е. Прусакова Верстка М. Р. Саит-Аметов Художественный редактор Е. П. Дынник Корректор М. Р. Саит-Аметов Издательский дом "Вильяме". 101509, Москва, ул. Лесная, д. 43, стр. 1. Изд. лиц. ЛР № 090230 от 23.06.99 Госкомитета РФ по печати. Подписано в печать 14.04.2003. Формат 70x100/16. Гарнитура LH. Печать офсетная. Усл. печ. л. 33,46. Уч.-изд. л. 30,96. Тираж 2500 экз. Заказ № 2800. Отпечатано с диапозитивов в ФГУП "Печатный двор" Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.