Text
                    Черепашья графика на занимательных примерах

1


Бесплатное издание Все права защищены. Никакая часть этой книги не может быть воспроизведена в любой форме без письменного разрешения правообладателей. Автор книги не несёт ответственности за возможный вред от использования информации, составляющей содержание книги и приложений. Copyright 2023 Валерий Рубанцев Лилия Рубанцева 2
От автора Народная мудрость гласит: лучше один раз увидеть, чем сто раз услышать. Наглядность – один из дидактических принципов, основанных на достоверном факте, что большую часть информации человек получает от зрения. Современная компьютерная графика должна быть очень быстрой, ведь программы генерируют сложные сцены с множеством объектов. Поэтому мы получаем на экране картинки уже в готовом виде. Процесс построения изображений скрыт от нас. Мы не знаем, как программа рисует линии, квадраты или круги, не говоря уже о более сложных фигурах. Во многих языках программирования, а теперь и в паскале, есть Черепашка – исполнитель простых команд для вычерчивания линий. 3
Черепашка наглядно показывает пошаговый процесс графических построений. Если Черепашка чертит линию, то мы видим, как она приползает в начало этой линии, поворачивается в сторону конца линии и медленно ползёт «из пункта А в пункт Б», оставляя за собой след – всё увеличивающийся в длине отрезок прямой. Конечно, построение прямых линий и других геометрических примитивов мы вполне можем представить себе мысленно. Но последовательность построения сложных фигур далеко не столь очевидна. Тем более это относится к фрактальным и рекурсивным кривым. Здесь наглядность просто необходима, и без Черепашки нам не обойтись. Но черепашья графика не только познавательна, она интересна и с художественной точки зрения, ведь Черепашка может рисовать очень красивые картинки: спирали Архимеда и Ферма, лемнискату Бернулли, улитку Паскаля, фигуры Лиссажу, астроиды, кардиоиды, эпи- и гипотрохоиды. Мы научим Черепашку рисовать клевер, розочки, ромашки, кустики и деревья, параметрические и полярные кривые, снежинку Коха, кривые Пеано, Пеано-Госпера и Пенроуза. И это ещё не всё! Мы отправим нашу учёную Черепашку собирать цели и бродить по Лабиринту. В книге много занимательных проектов, но самое главное – она даёт вам в руки и в головы замечательный инструмент для реализации вашего творческого потенциала. И Черепашка поможет вам воплотить в жизнь самые смелые художественные замыслы и помыслы. Поскольку книга рассказывает о компьютерной графике, то в ней много картинок, иллюстрирующих работу программ. К сожалению, наша Черепашка не умеет рисовать автопортреты, и тут на помощь ей, и мне, и вам приходит сайт FUSION BRAIN по адресу: https://editor.fusionbrain.ai/ Умело формулируя запросы, вы сможете бесплатно получить качественные иллюстрации к своим книгам, рефератам, статьям и дневникам. Дополнительно вы можете задать стиль изображения, чтобы выбрать подходящие по теме картинки. Искусственный интеллект ещё далёк от совершенства, но иногда он выдаёт настоящие шедевры. Образцы творчества искусственного разума вы найдёте дальше, в наших проектах, но вот некоторые примеры работы этого разума по запросу Черепашка-художница. 4
5
Черепашка живёт медленно, зато долго. Берегите себя! Валерий Рубанцев 6
Условные обозначения, принятые в книге: Дополнение Указание Замечание Исходный код: uses GraphWPF, TurtleWPF; Задание для самостоятельного решения Заголовок проекта: Черепашья мигалка Исходные коды всех проектов находятся в папке _Projects 7
Оглавление Черепашья графика на занимательных примерах ............... 1 От автора ............................................................... 3 Оглавление ............................................................. 8 Исполнитель Черепашка ............................................. 11 Точки ............................................................................................................................ 23 Мышка .......................................................................................................................... 25 Многоугольники ....................................................................................................... 29 Черепашьи загогулины........................................................................................... 35 Рекурсивная Черепашка .......................................................................................... 38 Звёздчатая Черепашка ............................................................................................. 48 Черепашка из SmallBasicLibrary ..................................... 51 Знакомство с Черепашкой ..................................................................................... 52 Черепашья мигалка .................................................................................................. 59 Черепаший брейк...................................................................................................... 61 Черепаший пунктир ................................................................................................. 64 Черепашьи полоски................................................................................................. 66 Черепашьи полоски 2 ............................................................................................. 71 Шахматная доска ...................................................................................................... 75 Фрактальная Кибер-Черепашка....................................... 83 Кибер-Черепашка ...................................................................................................... 83 Фракталы ..................................................................................................................... 99 Снежинка Коха ..........................................................................................................101 Фрактальная Черепашка ........................................................................................ 102 L-системы ................................................................................................................. 123 Черепаший зоопарк ................................................. 129 Очень БОЛЬШАЯ Черепашка............................................................................... 129 8
Библиотечная Черепашка .................................................................................... 136 Блуждающие Черепашки ..................................................................................... 157 Черепашка из Small Visual Basic ......................................................................... 164 Многоугольники ..................................................................................................... 165 Полярные кривые .................................................................................................. 176 Улитка Паскаля ...................................................................................................... 189 Спирали ..................................................................................................................... 199 Лютики-цветочки: клевер, розочки, ромашки ............................................. 222 Лемниската Бернулли .......................................................................................... 240 Фигуры Лиссажу .................................................................................................... 252 Трохоиды................................................................................................................. 263 Спирограф ............................................................................................................... 277 Побочные эпитрохоиды ..................................................................................... 279 Немыслимые мысленные эксперименты .................................................... 284 Звёздный конструктор.......................................................................................... 290 Разноцветные окружности .................................................................................. 305 Параметрические кривые.................................................................................... 317 Разноцветные диагонали ..................................................................................... 341 Одноцветные диагонали .................................................................................... 358 Диагональные разрезы ......................................................................................... 361 Диагональный круг............................................................................................... 366 Разноцветные многоугольники ......................................................................... 370 Вращающиеся многоугольники ........................................................................ 379 Цветные вращающиеся многоугольники ...................................................... 388 Зубчатые многоугольники ................................................................................. 398 Звёздчатые многоугольники ............................................................................. 407 Рекурсивные квадраты ......................................................................................... 426 Слоёный торт.......................................................................................................... 430 Рекурсивные круги ................................................................................................ 434 Рекурсивные круграты и квадруги ................................................................... 438 Снежинка Коха ........................................................................................................ 444 Триколор .................................................................................................................. 449 Компьютерная радуга .......................................................................................... 459 9
Астроида .................................................................................................................. 464 Астроида 2 ............................................................................................................... 469 Астроида 3 ............................................................................................................... 474 Цветные линии ...................................................................................................... 490 Новые розетки ........................................................................................................ 494 Вращающиеся квадраты ...................................................................................... 508 Движение объекта к цели.................................................................................... 517 Все в сборе............................................................................................................... 522 Все в сборе 2 ........................................................................................................... 526 Лабиринт .................................................................................................................. 529 Задания для самостоятельного решения ...................................................... 545 Справочник .......................................................... 547 Таблица <Класс Desktop> .................................................................................... 547 Таблица <Класс GraphicsWindow> .................................................................... 547 Таблица <Класс Timer> ........................................................................................ 554 Таблица <Класс Turtle> ....................................................................................... 555 Таблица <Класс Tortoise> ................................................................................... 557 Литература .......................................................... 561 10
Исполнитель Черепашка Этот новый исполнитель появился в PascalABC.NET версии 3.9.0. Заданий для него пока нет, поэтому мы сами озадачим себя занимательными проектами, чтобы получить удовольствие и познакомиться с Черепашкой. Начинаем знакомиться с Черепашкой. Для этого импортируем в программу модуль TurtleWPF и запускаем программу: ## uses TurtleWPF; На экране тут же появляется окно с размерами по умолчанию – 800 х 600 пикселей, с заголовком Исполнитель Черепаха WPF. Фон окна чисто белый, а в самом центре окна ждёт команд героиня наших экспериментов – Черепашка (Рис. 1). А это портрет Черепашки во весь рост (Рис. 2). В запущенной программе размеры окна можно изменять, как обычно, мышкой. Она смотрит вверх, и это важно – Черепашка двигается в том направлении, куда она смотрит (или пятится в обратную сторону). Важная процедура MoveTo переносит Черепашку в точку с координатами (x, y) в пикселях (без вычерчивания линии): procedure MoveTo(x, y: real); 11
Начало координат находится в левом верхнем углу клиентской области окна. Ось Х направлена вправо, а ось Y – вниз. Рис. 1. Черепашка в натуральную величину Давайте перенесём Черепашку ближе к началу координат: MoveTo(100, 100); 12
За размеры окна (клиентской части) программы отвечают свойства Width (ширина) и Height (высота). Если вы хотите изменять размеры окна в запущенной программе, то добавьте модуль GraphWPF: ## uses GraphWPF, TurtleWPF; property Width: real read GetWidth write SetWidth; property Height: real read GetHeight write SetHeight; Рис. 2. Черепашка в полный пиксель Нам пока достаточно небольшого окна, поэтому уменьшаем его: Window.Width := 400; Window.Height := 300; MoveTo(100, 100); Запускаем программу. Окно программы ужалось, но Черепашку прекрасно видно (Рис. 3). 13
Рис. 3. Черепашка в пределах видимости Неизменный заголовок окна не даёт полезной информации для размышления, но свойства окна Title и Caption помогут нам озаглавить окно по своему желанию и хотению (Рис. 4): property Title: string read GetCaption write SetCaption; property Caption: string read GetCaption write SetCaption; Window.Title := 'Привет, Черепашка!'; Процедура Clear без параметров окрашивает фон окна в белый цвет: procedure Clear; 14
Эта же процедура с параметром с заливает окно заданным цветом с (Рис. 5): procedure Clear(c: Color); Рис. 4. Озаглавленное окно Предопределённые цвета удобно выбирать из выпадающего списка, который появляется на экране, когда вы поставите точку после слова Colors (Рис. 6). На официальном сайте системы программирования PascalABC.NET можно найти пример работы Черепашки: Down; SetSpeed(11); SetColor(Colors.Red); for var i:=1 to 450 do begin 15
SetColor(RGB(128+i,0,i)); Forw(i); Turn(96); end; Рис. 5. Затонированное окно Рис. 6. Цвета на выбор Я добавил в эту программу комментарии: 16
## uses GraphWPF, TurtleWPF; Window.Title := 'Привет, Черепашка!'; Window.Clear(Colors.SeaShell); // Черепашка опускает перо: Down; // скорость перемещения Черепашки: SetSpeed(11); // цвет линии: SetColor(Colors.Red); for var i := 1 to 450 do begin // цвет линии: SetColor(RGB(128+i,0,i)); // Черепашка ползёт вперёд: Forw(i); // Черепашка поворачивается на // заданный угол в градусах по часовой стрелке: Turn(96); end; Запускаем программу, и сразу получаем готовую картинку (Рис. 7). Картинка красивая – слов нет. Но Черепашка двигается так быстро, что мы не успеваем проследить за её работой. Скорость Черепашки задаёт процедура SetSpeed: procedure SetSpeed(sp: integer); Вы можете задавать любые целые значения, но программа всё равно приведёт их к диапазону 1..10. Чем больше это значение, тем резвее Черепашка бегает по 17
полю. По умолчанию скорость Черепашки равна 10, то есть максимально возможной. Однако в примерной программе скорость Черепашки равна 11, то есть больше 10. В этом случае Черепашка двигается без анимации - прыжками. Рис. 7. Стремительная Черепашка 18
У Черепашки есть пёрышко, которое прикреплено к её хвосту. Не все это знают, но у настоящей черепахи тоже есть хвостик. Правда, без карандаша и пера (Рис. 8). Рис. 8. Хвостатая Черепашка Если перо опущено, то Черепашка оставляет за собой след – отрезок прямой. Если перо поднято, то Черепашка двигается «бесследно». По умолчанию перо поднято, поэтому Черепашку можно легко перенести в любое место окна без ненужной линии. Как вы уже догадались, процедура Down опускает перо: procedure Down; Процедура Up, наоборот, поднимает перо: procedure Up; Процедура SetColor обмакивает перо Черепашки в чернила заданного цвета с: procedure SetColor(c: GColor) 19
Сначала это красный цвет, который выбирается из списка готовых цветов, а затем изменяется по ходу верчения спиралей при посредстве функции RGB, которой нужно передать составляющие цвета. На Рис. 7⬆ отчётливо видно, что цвет линий плавно переходит из красного в фиолетовый. Значения составляющих цвета в функции RGB изменяются в диапазоне 0..255: function RGB(r,g,b: byte): Color; Функция RandomColor возвращает случайный цвет линий: function RandomColor: Color; Процедура Forw гонит Черепашку вперёд (по направлению взгляда) на заданное расстояние r в пикселях: procedure Forw(r: real); Процедура Back действует на Черепашку аналогично, но заставляет её пятиться, то есть ползти в направлении, обратном её взгляду: procedure Back(r: real); Процедура Turn поворачивает Черепашку на угол da в градусах по часовой стрелке: procedure Turn(da: real); 20
Процедура TurnLeft поворачивает Черепашку на угол da в градусах по часовой стрелке: procedure TurnLeft(da: real); На самом деле Черепашка поворачивается вправо. Процедура TurnRight поворачивает Черепашку на угол da в градусах против часовой стрелки: procedure TurnRight(da: real); На самом деле Черепашка поворачивается влево. Чтобы не запутаться в этих поворотах, всегда используйте процедуру Turn. Если угол положительный, то Черепашка повернётся по часовой стрелке. Если угол отрицательный, то против часовой стрелки. По умолчанию Черепашку видно на экране, но иногда её нужно спрятать с глаз долой. Процедура Hide легко справляется с этой задачей: procedure Hide; 21
Процедура Show возвращает скрытную Черепашку на экран: procedure Show; По умолчанию толщина линий равна 1 пикселю, но может быть изменена процедурой SetWidth: procedure SetWidth(w: real); И последняя черепашья процедура – Save – делает снимок клиентской части окна и записывает его в файл на диске в форматах .png, .jpg, .bmp, .gif или .tiff: procedure Save(fname: string); fname – это строка с названием файла и расширением. 22
Точки Начинаем наш первый черепаший проект. Создаём окно подходящих размеров: ## uses GraphWPF, TurtleWPF; Window.Width := 400; Window.Height := 400; Window.Title := 'Точки'; Window.Clear(Colors.SeaShell); Переносим Черепашку в центр окна: // координаты центра окна: var CX := Window.Width / 2; var CY := Window.Height / 2; var r := 180; // скорость Черепашки: SetSpeed(2); // размер точки - 8 пикселей: SetWidth(8); //SetColor(Colors.Green); // переносим Черепашку в центр окна: MoveTo(CX,CY); // опускаем перо: Down; Из центра окна Черепашка ползёт вперёд на расстояние r-8, где r – это радиус окружности, на которой Черепашка ставит точки: 23
for var i := 1 to 8 do begin // случайный цвет чернил: SetColor(RandomColor); // поднимаем перо: Up; // вперед без линии, // чтобы начать чертить точку: Forw(r-8); Прибыв к месту назначения, Черепашка ползёт вперёд на 8 пикселей, чтобы прочертить отрезок длиной 8 пикселей: Down; // вперёд на 8 пикселей, // чтобы поставить точку Forw(8); После этого Черепашка телепортируется в центр окна: Up; MoveTo(CX,CY); И поворачивается здесь на угол 45 градусов по часовой стрелке, чтобы отправиться к следующей точке: Turn(360/8); end; На Рис. 1 вы видите первые результаты дрессировки нашей Черепашки. Даже с первого раза художества у неё получились изрядно хорошо! 24
Рис. 1. Точечная Черепашка Мышка В этом проекте мы научим Черепашку подчиняться мышке. Сама Черепашка не сообщает нам свои текущие координаты и угол поворота её скорлупы, поэтому мы запоминаем их в глобальных переменных: uses GraphWPF, TurtleWPF, System; 25
// координаты Черепашки: var TurtleX : real; var TurtleY : real; // угол поворота: var TurtleA : real; В главном блоке программы создаём окно приложения: begin Window.Width := 400; Window.Height := 400; Window.Title := 'Мышка'; Window.Clear(Colors.SeaShell); Задаём толщину и цвет линий, а также скорость ползания Черепашки: // толщина линий: SetWidth(2); // скорость Черепашки: SetSpeed(7); // цвет линий: SetColor(Colors.Green); Устанавливаем Черепашку в центре окна: // координаты центра окна: var CX := Window.Width / 2; var CY := Window.Height / 2; // переносим Черепашку в центр окна: MoveTo(CX,CY); // запоминаем её координаты: 26
TurtleX := CX; TurtleY := CY; По умолчания Черепашка смотрит вверх, то есть угол её поворота равен -90 градусов: // Черепашка смотрит вверх: TurtleA := -90; // опускаем перо: Down; Если нажата кнопка мышки, программа переходит в процедуру MouseDown: // процедура для обработки события // нажатия кнопки мышки: OnMouseDown := MouseDown; end. В процедуре MouseDown мы пользуемся всеми премудростями тригонометрии, чтобы правильно сориентировать Черепашку в окне программы: procedure MouseDown(x, y: real; mousebutton: integer); begin // координаты Черепашки: var tx := TurtleX; var ty := TurtleY; var ta := TurtleA; // расстояние от курсора до Черепашки: var dx := x - tx; var dy := y - ty; // вычисляем направление Черепашки: var a := Math.Atan2(dy, dx) * 180 / PI; 27
var angle := a - ta; // поворачиваем Черепашку: Turn(angle); // запоминаем угол поворота: TurtleA := a; // и координаты Черепашки: TurtleX := x; TurtleY := y; // расстояние до мышки: var dist := Sqrt(dx**2 + dy**2); // вперёд! Forw(dist); end; Теперь вы можете – не без помощи мышки! – гонять Черепашку вдоль, поперёк и под любым углом (Рис. 1). Рис. 1. Черепашьи каракули 28
Многоугольники Давайте отдохнём от ручной работы в предыдущем проекте и погоняем Черепашку программно. В главном блоке программы создаём окно, настраиваем Черепашку на работу и вызываем процедуру Triangle: begin Window.Width := 320; Window.Height := 320; Window.Title := 'Многоугольники'; Window.Clear(Colors.SeaShell); // толщина линий: SetWidth(2); // скорость Черепашки: SetSpeed(7); // цвет линий: SetColor(Colors.Green); // опускаем перо: Down; Triangle; end. Внутренние углы правильных многоугольников легко вычислить, поэтому нам нужно только правильно установить Черепашку на старте и дать ей надлежащего пинка на всю дистанцию. Вспоминаем, что у треугольника 3 стороны, а внешние углы равны по 120 градусов: 29
uses GraphWPF, TurtleWPF; // треугольник procedure Triangle; begin MoveTo(160,40); Turn(-30); var r := 300; loop 3 do begin Turn(-120); Forw(r); end end; Запускаем программу – Черепашка отлично справляется с чертёжными работами (Рис. 1). Раскинув мозгами и транспортиром, вы научите Черепашку чертить шести- и восьмиугольники (Рис. 1): // шестиугольник procedure Hexagon; begin MoveTo(235,30); Turn(-30); var r := 150; loop 6 do begin Turn(-60); Forw(r); end end; // восьмиугольник procedure Octagon; 30
begin MoveTo(306,100); Turn(-0); var r := 120; loop 8 do begin Turn(-45); Forw(r); end end; Нетрудно догадаться, что если задать достаточно большое число вершин, то многоугольник превратится в окружность (Рис. 1): // окружность procedure Circle; begin MoveTo(312,155); Turn(-0); var r := 12; var n := 80; loop n do begin Turn(-360/n); Forw(r); end end; Для построения любого правильного многоугольника нужно написать всего несколько строк кода и при этом – никаких расчётов координат вершин! 31
Рис. 1. Черепашьи многоугольники 32
Черепашке нетрудно вычертить и спираль (Рис. 2): // спираль procedure Spiral; begin MoveTo(320,320); Turn(-0); var r := 0.1; var n := 1100; loop n do begin Forw(r); Turn(10); r += 0.05; end end; Если при повороте Черепашки задать отрицательный угол Turn(-10), то вместо правой спирали получится левая (она закручена в противоположном направлении). 33
Рис. 2. Черепашья спираль 34
Черепашьи загогулины А теперь давайте перейдём к более сложным фигурам, которые наверняка убедят вас в том, что вы не напрасно познакомились с Черепашкой. Для начала мы заставим многоугольники вращаться вокруг одной из своих вершин: uses GraphWPF, TurtleWPF; var povorot, angle1, angle2, side : integer; procedure PolyStop(); begin repeat Forw(side); Turn(angle1); povorot += angle1; until (povorot mod 360 = 0); end; // Вращающиеся фигуры procedure PolyRoll(); begin loop 12 do begin PolyStop(); Turn(angle2); end; end; Изменяя значения переменных angle1 и angle2, вы получите разные многоугольники, вращающиеся вокруг центра окна (Рис. 1): 35
// Вращающиеся многоугольники: procedure RotatePolygons; begin povorot := 0; angle1 := 90; angle2 := 30; side := 100; PolyRoll(); end; procedure RotatePolygons2; begin povorot := 0; angle1 := 60; angle2 := 45; side := 80; PolyRoll(); end; begin Window.Width := 320; Window.Height := 320; Window.Title := 'Черепашьи загогулины'; Window.Clear(Colors.SeaShell); // устанавливаем Черепашку в центре окна: var CX := Window.Width / 2; var CY := Window.Height / 2; MoveTo(CX, CY); // толщина линий: SetWidth(2); // скорость Черепашки: SetSpeed(10); // цвет линий: 36
SetColor(Colors.Green); // опускаем перо: Down; //RotatePolygons; RotatePolygons2; end. Рис. 1. Черепашья вертиугольники 37
Рекурсивная Черепашка Необыкновенно простая рекурсивная процедура PolySpi при разных значениях переменных даёт потрясающее многообразие великолепных спиралей (Рис. 1-6)! uses GraphWPF, TurtleWPF; const WIDTH = 640; HEIGHT = 640; CX = WIDTH / 2; CY = HEIGHT / 2; var angle, side, maxSide : real; inc : integer; // Рекурсивные спирали procedure PolySpi(); begin if (side > maxSide) then exit; Forw(side); Turn(angle); side += inc; PolySpi(); end; // рекурсивная спираль 1 procedure Spiral1(); begin // устанавливаем Черепашку: var x := CX - 30; var y := CY + 30; MoveTo(x,y); maxSide := 470; inc := 1; 38
side := 60; angle := 95; PolySpi(); end; // квадратная спираль procedure QSpiral(); begin var x := CX - 0; var y := CY + 0; MoveTo(x,y); maxSide := 600; inc := 3; angle := 90; PolySpi(); end; // квадратная спираль 2 procedure QSpiral2(); begin var x := CX - 0; var y := CY + 0; MoveTo(x,y); maxSide := 470; inc := 3; angle := 87; PolySpi(); end; // треугольная спираль 1 procedure TSpiral1(); begin var x := CX - 80; var y := CY + 0; 39
MoveTo(x,y); maxSide := 600; inc := 8; angle := 120; PolySpi(); end; // треугольная спираль 2 procedure TSpiral2(); begin var x := CX + 20; var y := CY - 20; MoveTo(x,y); inc := 4; angle := 117; maxSide := 560; PolySpi(); end; // СПИРАЛЬ 2 procedure Spiral2(); begin var x := CX; var y := CY - 10; MoveTo(x,y); inc := 1; angle := 30; maxSide := 170; side := 3; PolySpi(); end; begin Window.Width := WIDTH; 40
Window.Height := HEIGHT; Window.Title := 'Рекурсивная Черепашка'; Window.Clear(Colors.SeaShell); // толщина линий: SetWidth(1.6); // скорость Черепашки: SetSpeed(11); // цвет линий: SetColor(Colors.Green); // опускаем перо: Down; //Spiral1(); //QSpiral(); //QSpiral2(); //TSpiral1(); //TSpiral2(); Spiral2(); end. 41
Рис. 1. Рекурсивная спираль 42
Рис. 2. Квадратная спираль 43
Рис. 3. Рекурсивная спираль 44
Рис. 4. Треугольная спираль 45
Рис. 5. Рекурсивная треугольная спираль 46
Рис. 6. Многоугольная спираль 47
Звёздчатая Черепашка В этом проекте Черепашка крутит звёздчатые многоугольники (Рис. 1): uses GraphWPF, TurtleWPF; const WIDTH = 320; HEIGHT = 320; CX = WIDTH / 2; CY = HEIGHT / 2; var angle, side : real; // Звёздчатые многоугольники procedure NewPoly(n : integer); begin loop n do begin Forw(side); Turn(angle); Forw(side); Turn(2 * angle); end; end; procedure Star(); begin // устанавливаем Черепашку: var x := CX - 35; var y := CY - 30; MoveTo(x,y); Turn(18); // звезда: side := 110; angle := 144; 48
NewPoly(5); end; procedure Star2(); begin // устанавливаем Черепашку: var x := CX - 73; var y := CY - 104; MoveTo(x,y); Turn(0); // зубчатое колесо: side := 36; angle := 125; NewPoly(24); end; begin Window.Width := WIDTH; Window.Height := HEIGHT; Window.Title := 'Звёздчатая Черепашка'; Window.Clear(Colors.SeaShell); // толщина линий: SetWidth(2); // скорость Черепашки: SetSpeed(11); // цвет линий: SetColor(Colors.Green); // опускаем перо: Down; Star(); //Star2(); end. 49
Рис. 1. Звёздчатые многоугольники 50
Черепашка из SmallBasicLibrary Черепаха - русский Чарапаха - белорусский Tortoise, turtle - английский Galápago - испанский Testudo - латинский Tartaruga - португальский Впервые Черепашка появилась в языке программирования Лого (Logo), который был разработан в 1967 году Сеймуром Пейпертом и Идит Харель. Лого создавался как средство обучения детей дошкольного и младшего школьного возраста основам программирования. Главными объектами языка Лого были слова и предложения, что и определило выбор названия для самого языка – лого по-гречески значит слово. Но известность этому языку программирования принесла маленькая Черепашка (Turtle), которая умела выполнять несложные команды и вычерчивать линии. Именно благодаря Черепашке этот язык стал очень популярным в 70-80-е годы прошлого века, а Черепашка появилась и в других языках программирования – LISP, SCHEME и многих версиях бейсика. Есть Черепашка и в библиотеке SmallBasicLibrary - она «спрятана» в классе Turtle. В разных системах программирования Черепашки отличаются по форме. Обычно это стилизованные черепахи, но 51
встречаются и примитивные виды Черепашек – равносторонние треугольники и наконечники стрелок (Рис. 1). Их объединяет желание ползать по экрану и возможность указывать направление их перемещения. Рис. 1. Черепашки разных видов Знакомство с Черепашкой Морская черепашка По имени Наташка, С очками из Китая Такая вот крутая. Натали, Морская черепашка Чтобы увидеть Черепашку на экране, достаточно выполнить метод Turtle.Show (Рис. 1). Она всегда появляется в центре окна и смотрит вверх. Направление её взгляда знать очень важно, потому что Черепашка может ползти только туда, куда глаза глядят! 52
Рис. 1. Привет, Черепашка! А теперь пора познакомится и с кодом, который познакомил нас с Черепашкой. Комментарий не обязателен, но он подсказывает нам название и назначение программы: // Знакомство с Черепашкой Чтобы использовать библиотеку SmallBasicLibrary, нужно указать полный путь к ней, если она находится не в папке с программой: 53
{$reference 'LIB/SmallBasicLibrary.dll'} В разделе uses прописываем используемые библиотеки: uses Microsoft.SmallBasic.Library; Чтобы не печатать длинное имя класса Графического окна, создаём короткий псевдоним: type gw = GraphicsWindow; Все подготовительные работы оформляем в процедуре Prepare: procedure Prepare(); begin Процедура Hide Графического окна прячет его, пока мы переносим окно в центр экрана: gw.Hide(); Свойство Title устанавливает и возвращает заголовок Графического окна: gw.Title := 'Знакомство с Черепашкой'; Процедура Show Графического окна показывает его на экране: 54
gw.Show(); Свойства Left и Top задают координаты верхнего левого угла Графического окна на экране: gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; Свойство CanResize разрешает или запрещает изменять размеры окна в работающей программе. По умолчанию значение свойства CanResize равно true, поэтому размеры окна можно изменять мышкой. gw.CanResize := false; Свойство BackgroundColor окрашивает фон окна в заданный цвет: gw.BackgroundColor := 'SeaShell'; end; Теперь главный блок программы получился коротким, простым и понятным: begin Prepare; // создаём Черепашку: Turtle.Show; end. 55
Размеры окна по умолчанию слишком велики для наших простых экспериментов, но их легко изменить. Желаемые размеры окна лучше задавать константами, чтобы при необходимости можно было бы легко исправить их: // размеры окна: const GW_WIDTH = 320; const GW_HEIGHT = 320; Свойства Графического окна Width и Height устанавливают и возвращают текущие размеры окна: gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; Но при изменении размеров окна Черепашка остаётся на прежнем месте. Давайте вернём Черепашку на её законное место – в центр окна. При неизменных размерах окна координаты его центра не изменяются, поэтому мы также запоминаем их в константах: const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; Свойства Черепашки X и Y устанавливают и возвращают текущие координаты Черепашки: // устанавливаем Черепашку в центре окна: Turtle.X := CX; Turtle.Y := CY; 56
Имейте в виду, что координаты Черепашки отсчитываются в системе координат канвы, то есть начало координат находится в левом верхнем углу, а ось Y направлена вниз! Код программы слегка удлинился, но зато стал более универсальным: // Знакомство с Черепашкой {$reference 'LIB/SmallBasicLibrary.dll'} uses Microsoft.SmallBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 320; const GW_HEIGHT = 320; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Знакомство с Черепашкой'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.CanResize := false; gw.BackgroundColor := 'SeaShell'; end; 57
procedure Show(); begin // создаём Черепашку: Turtle.Show(); // устанавливаем Черепашку в центре окна: Turtle.X := CX; Turtle.Y := CY; end; begin Prepare; Show; end. 58
Черепашья мигалка Процедура Turtle.Show показывает Черепашку на экране, а процедура Turtle.Hide прячет её от нас. Мы можем использовать эти процедуры, чтобы заставить Черепашку мигать. Также нам понадобится таймер, чтобы отсчитывать время между миганиями. В библиотеке SmallBasic.Library есть простой класс Timer, скромных возможностей которого вполне достаточно для наших целей. Добавьте в процедуру Prepare следующий код: // Таймер: Timer.Interval := 250; Timer.Pause; Timer.Tick += OnTick; Timer.Resume; Свойство Timer.Interval задаёт промежуток времени между срабатываниями таймера. Событие Timer.Tick происходит при каждом срабатывании таймера, и тогда вызывается процедура, которую мы добавили к этому событию, то есть OnTick. Процедура Timer.Pause приостанавливает работу таймера, а процедура Resume возобновляет её. Чтобы попеременно скрывать и показывать Черепашку, определяем переменную n: 59
// счётчик миганий: var n := 0; А вот и процедура OnTick, которую вызывает таймер: procedure OnTick(); begin if n mod 2 = 0 then Turtle.Hide else Turtle.Show; n += 1; end; Запускаем программу и попеременно видим на экране эти картинки (Рис. 1). Рис. 1. Черепашья мигалка 60
Черепаший брейк Три процедуры класса Turtle: Turtle.TurnLeft(); Turtle.TurnRight(); Turtle.Turn(угол : double); поворачивают Черепашку вокруг собственной оси. Первая процедура – на 90 градусов против часовой стрелки, вторая – на 90 градусов по часовой стрелке, третья – на произвольный угол. В последнем случае Черепашка повернётся против часовой стрелки, если угол отрицательный, и по часовой стрелке – если положительный. В процедуре Show поворачиваем Черепашку влево на 45 градусов: procedure Show(); begin // создаём Черепашку: Turtle.Show(); // устанавливаем Черепашку в центре окна: Turtle.X := CX; Turtle.Y := CY; // поворачиваем Черепашку: Turtle.Turn(-45); end; 61
В процедуре Prepare задаём интервал между срабатываниями таймера близким к 1000 миллисекунд, чтобы Черепашка успела повернуться: Timer.Interval := 800; В процедуре OnTick крутим Черепашку туда-сюда. Так: procedure OnTick(); begin if n mod 2 = 0 then Turtle.TurnRight else Turtle.TurnLeft; n += 1; end; Или так: procedure OnTick(); begin if n mod 2 = 0 then Turtle.Turn(90) else Turtle.Turn(-90); n += 1; end; Для неуклюжей Черепашки танец она исполняет вполне себе зажигательный (Рис. 1). 62
Рис. 1. Черепаший брейк 63
Черепаший пунктир За положение карандаша (или пера) отвечают процедуры Черепашки PenDown и PenUp. Первая опускает карандаш, а вторая – поднимает его. Соответственно этому Черепашка либо чертит линию, либо просто перемещается в новое положение. Черепашка занимает стартовую позицию у нижней границы окна: procedure Show(); begin // создаём Черепашку: Turtle.Show(); // устанавливаем Черепашку: Turtle.X := CX; Turtle.Y := CY + GW_HEIGHT div 2; end; Таймерный интервал можно уменьшить: Timer.Interval := 250; В процедуре Prepare задаём свойства линий: // цвет линий: gw.PenColor := 'Green'; // толщина линий: gw.PenWidth := 4; 64
Свойство Графического окна PenColor выбирает цвет карандаша, а свойство PenWidth определяет толщину линий. Все черепашьи художества переносим в процедуру OnTick, где Черепашка попеременно поднимает и опускает хвостик с карандашом: procedure OnTick(); begin if n mod 2 = 0 then Turtle.PenDown else Turtle.PenUp; Turtle.Move(12); n += 1; end; Процедура Turtle.Move двигает Черепашку вперёд на указанное число пикселей. Если карандаш поднят, то Черепашка просто ползёт, а если опущен – марает экран. В итоге черепахоползания получается прекрасная пунктирная линия (Рис. 1). Рис. 1. Чересполосица 65
Черепашьи полоски Черепашка при изрядном старании может вычерчивать полоски двух чередующихся цветов. Это могут быть горизонтальные полоски (Рис. 1): {$reference 'LIB/SmallBasicLibrary.dll'} uses Microsoft.SmallBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; const GW_HEIGHT = 640; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Черепашьи полоски'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.CanResize := false; gw.BackgroundColor := 'SeaShell'; Turtle.Show; 66
Turtle.PenDown; end; // ГОРИЗОНТАЛЬНЫЕ ПОЛОСКИ procedure ПолоскиГориз(n : integer); begin // отступ от левого края: var dx := 10; // длина полоски: var w := GW_WIDTH - dx; // отступ от верхнего края: var dy := 10; // толщина полоски: var h := (GW_HEIGHT - dx * 1) / n; var offX := dx; var offY := dy + h / 2; var color1 := 'Red'; var color2 := 'Yellow'; gw.PenWidth := h; var x := offX; var y := offY; Turtle.Speed := 9; Turtle.X := x; Turtle.Y := y; Turtle.Angle := 90; for var row := 0 to n - 1 do begin // цвет полоски: var color := row mod 2 = 0 ? color1 : color2; gw.PenColor := color; Turtle.Move(w); y += h; Turtle.Y := y; Turtle.X := offX; end; end; 67
Рис. 1. Лежачие полоски И вертикальные полоски (Рис. 2): // ВЕРТИКАЛЬНЫЕ ПОЛОСКИ 68
procedure ПолоскиВерт(n : integer); begin // отступ от верхнего края: var dy := 10.0; // длина полоски: var w := GW_HEIGHT - dy; // отступ от левого края: var dx := 10; // толщина полоски: var h := (GW_WIDTH - dx) / n; var offX := dx + h / 2; var offY := dy; var color1 := 'Red'; var color2 := 'Yellow'; gw.PenWidth := h; var x := offX; var y := offY; Turtle.Speed := 9; Turtle.X := x; Turtle.Y := y; Turtle.Turn(180); for var row := 0 to n - 1 do begin // цвет полоски: var color := row mod 2 = 0 ? color1 : color2; gw.PenColor := color; Turtle.Move(w); x += h; Turtle.Y := offY; Turtle.X := x; end; end; begin Prepare; 69
//ПолоскиГориз(12); ПолоскиВерт(24); end. Рис. 2. Стоячие полоски 70
Черепашьи полоски 2 Давайте наделим нашу Черепашку небольшими художественными способностями, чтобы она самостоятельно выбирала цвет и длину полосок: {$reference 'LIB/SmallBasicLibrary.dll'} uses Microsoft.SmallBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; const GW_HEIGHT = 640; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Черепашьи полоски'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.CanResize := false; gw.BackgroundColor := 'Black'; Turtle.Show; Turtle.PenDown; end; 71
// ПОЛОСКИ procedure ПолоскиГориз(n : integer); begin // отступ от левого края: var dx := 10.0; // отступ от верхнего края: var dy := 10; // толщина полоски: var h := (GW_HEIGHT - dx) / n; var offX := dx; var offY := dy + h / 2; gw.PenWidth := h; var x := offX; var y := offY; Turtle.Speed := 9; Turtle.X := x; Turtle.Y := y; Turtle.Angle := 90; for var row := 0 to n - 1 do begin // общая длина полоски: var w := GW_WIDTH - dx; while w > 0 do begin // цвет полоски: gw.PenColor := gw.GetRandomColor; // длина полоски: var len := Random(24.0, 120); if w - len < 24 then len := w; Turtle.Move(len); w -= len; end; y += h; Turtle.Y := y; Turtle.X := offX; end; end; begin Prepare; ПолоскиГориз(20); end. 72
Рис. 1. Полосатые полоски Если вам больше по душе вертикальные полоски, то добавьте к программе ещё одну процедуру. Или просто поверните картинку на 90 градусов (Рис. 2). 73
Рис. 2. Универсальная абстрактная живопись Курица, посмотрев на художества Черепашки (Рис. 1⬆), сказала бы: Сносно! 74
Шахматная доска Клетки шахматной доски окрашены в 2 цвета, которые чередуются по горизонтали и по вертикали. Все подготовительные операции вам известны: {$reference 'LIB/SmallBasicLibrary.dll'} uses Microsoft.SmallBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; const GW_HEIGHT = 640; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Шахматная доска'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.CanResize := false; gw.BackgroundColor := 'SeaShell'; end; Вычисление координат и размеров клеток потребует от вас некоторых математических умений на уровне начальной школы: 75
procedure ChessBoard(c, r : integer); begin // отступ от левого края: var dx := 10.0; // отступ от верхнего края: var dy := 10.0; // длина клетки: var w := (GW_WIDTH - dx) / c; // высота клетки: var h := (GW_HEIGHT - dy) / r; var offX := dx; var offY := dy + h / 2; var color1 := 'Red'; var color2 := 'Yellow'; gw.PenWidth := h; var x := offX; var y := offY; Turtle.Show; Turtle.PenDown; Turtle.Speed := 10; Turtle.X := x; Turtle.Y := y; Turtle.TurnRight; for var row := 0 to r - 1 do begin for var col := 0 to c - 1 do begin // цвет клетки: var color := (row + col) mod 2 = 0 ? color1 : color2; gw.PenColor := color; Turtle.Move(w); end; y += h; Turtle.Y := y; Turtle.X := offX; end; end; 76
begin Prepare; ChessBoard(8, 8); //ChessBoard(6, 120); //ChessBoard(180, 2); //RandomBoard(30, 30); end. Зато шахматная доска получилась изумительно красивой (Рис. 1). Наша программа достаточно универсальная, чтобы вычерчивать горизонтальные (Рис. 2) и вертикальные (Рис. 3) полоски разной ширины и высоты. Пары цветов вы можете выбирать по своему вкусу. Ежели вкус отсутствует или просто лень, то пусть компьютер сам выбирает цвет очередной клетки: procedure RandomBoard(c, r : integer); begin // отступ от левого края: var dx := 10.0; // отступ от верхнего края: var dy := 10.0; // длина клетки: var w := (GW_WIDTH - dx) / c; // высота клетки: var h := (GW_HEIGHT - dy) / r; var offX := dx; var offY := dy + h / 2; var color1 := 'Red'; var color2 := 'Yellow'; gw.PenWidth := h; 77
var x := offX; var y := offY; Turtle.Show; Turtle.PenDown; Turtle.Speed := 10; Turtle.X := x; Turtle.Y := y; Turtle.TurnRight; for var row := 0 to r - 1 do begin for var col := 0 to c - 1 do begin // цвет клетки: var color := gw.GetRandomColor; gw.PenColor := color; Turtle.Move(w); end; y += h; Turtle.Y := y; Turtle.X := offX; end; end; Черепашка запросто нарисует абстрактные картинки, которые могут украсить интерьер современного дома (Рис. 4). 78
Рис. 1. Пожароопасная шахматная доска 79
Рис. 2. Горизонтальная растяжка шахматной доски 80
Рис. 3. Вертикальная растяжка шахматной доски 81
Рис. 4. Искусственный разум творит искусство 82
Фрактальная Кибер-Черепашка Мы научили Черепашку выполнять наши команды, записанные на языке паскаль. Но ведь в каждом живом существе, в его геноме записана информация о том, как оно должно расти и развиваться, а также инстинкты, управляющие поведением достаточно примитивных организмов. Черепашка тоже умеет выполнять не только внешнюю программу, но и внутреннюю – записанную в её геноме и реализованную через инстинкты. Следующие проекты мы посвятим моделированию такой кибернетической Черепашки. Кибер-Черепашка 83
Черепашка рождается в условленном месте экрана, обозначенном координатами x0 и y0, а её голова направлена в сторону света, заданную начальным углом a0: // Кибер-Черепашка // ПРОГРАММА ДЛЯ МОДЕЛИРОВАНИЯ // КИБЕРЧЕРЕПАШКИ {$reference 'LIB/SmallBasicLibrary.dll'} uses Microsoft.SmallBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 320; const GW_HEIGHT = 320; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; // начальное положение Черепашки: var x0 := 0.0; y0 := 0.0; a0 := 0.0; Основное предназначение Черепашки (судьба) - перемещение по плоскости и вычерчивание линий, поэтому нам необходимо задать её основные свойства: // длина единичного перемещения Черепашки: size := 0.0; // угол единичного поворота Черепашки: teta := 0; // скорость движения Черепашки: speed := 9; // цвет линии: 84
penColor := 'Green'; // толщина линии: penWidth := 2; // список команд, которые должна выполнить Черепашка: instinct: string; Здесь всё просто и понятно, кроме, пожалуй, инстинкта, который и определяет поведение Черепашки. Дальше мы зададим ей жизненную программу (инстинкт), и вы сможете сами программировать Черепашку. Настройка окна приложения не должна вызвать у вас вопросов: procedure Prepare(); begin gw.Hide(); gw.Title := 'Кибер-Черепашка'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.CanResize := false; gw.BackgroundColor := 'SeaShell'; end; Теперь мы вполне можем перейти к программированию инстинкта Черепашки. Пусть её жизненный путь начнётся с прямой линии. Задаём место рождения Черепашки: procedure Line(); begin // начальная позиция Черепашки: x0 := CX; y0 := CY + 100; 85
Её направление: a0 := 0; Длину «ползка»: size := 200; И самое главное – инстинкт: // прямая линия: instinct := 'F'; Процесс рождения Черепашки не зависит от самой Черепашки, а полностью лежит на совести её родителей, то есть на нас с вами: // Черепашка занимает исходное положение procedure Start(); begin Turtle.Speed := speed; gw.PenColor := penColor; gw.PenWidth := penWidth; Turtle.PenDown; Turtle.X := x0; Turtle.Y := y0; Turtle.Angle := a0; Turtle.Show; end; В большинстве языков программирования, поддерживающих черепашью графику, движение Черепашки вперёд обозначается командой Forward. Первую 86
букву этого слова мы используем для этой же цели. После того как Черепашка заняла исходное положение, она должна выполнить те действия, которые ей предписывает инстинкт. Для этого у неё имеется мозг, который умеет обрабатывать и исполнять команды инстинкта: // Черепашка выполняет инстинкт procedure Execute(); begin // Черепашка считывает команды: for var i := 1 to instinct.Length do begin // очередная команда: var cmd := instinct[i]; // двигаться вперед на один шаг: case cmd of 'F': Turtle.Move(size); end; end end; Пока Черепашка маленькая, она может только двигаться вперёд, но и этого вполне достаточно, чтобы дать ей путёвку в жизнь: begin Prepare; Line; end. Запускаем программу, и Черепашка проводит вертикальный отрезок длиной в size пикселей (Рис. 1). 87
Рис. 1. Черепашка в ползунковом возрасте На следующем этапе своей черепашьей жизни наша Киберчерепашка сможет выполнять команды + и -, то есть поворачиваться направо и налево на заданный угол teta. В её окрепшем мозгу появляются новые шишки для обработки более сложного инстинкта: // Черепашка выполняет инстинкт procedure Execute(); begin // Черепашка считывает команды: for var i := 1 to instinct.Length do begin // очередная команда: var cmd := instinct[i]; // двигаться вперед на один шаг: case cmd of 'F': Turtle.Move(size); 88
'+': Turtle.Turn(teta); '-': Turtle.Turn(-teta); end; end end; Треугольник мы уже вычерчивали с помощью обычной Черепашки. Как вы помните, для этого Черепашка должна вычертить одну сторону, повернуться на угол 120 градусов, вычертить вторую сторону, опять повернуться на 120 градусов и, наконец, вычертить третью сторону. Отсюда следует, что угол единичного поворота равен 120 градусам, а инстинкт запишется строкой: // Треугольник: procedure Triangle(); begin // начальная позиция Черепашки: x0 := CX - 140; y0 := CY + 160; a0 := 0; teta := 120; size := 300; speed := 5; instinct := 'F+F+F+'; Start(); Execute(); end; Для проверки нашей гипотезы запускаем программу. Черепашка нас не подвела – треугольник удался на славу (Рис. 2)! 89
Рис. 2. Черепашковый треугольник Совершенно аналогично вы можете сформировать инстинкты для вычерчивания любых правильных многоугольников (Рис. 3): // квадрат procedure Quadrat(); begin // начальная позиция Черепашки: x0 := CX - 140; y0 := CY + 160; a0 := 0; size := 300; teta := 90; instinct := 'F+F+F+F+'; Start(); Execute(); 90
end; // пятиугольник procedure Pentagon(); begin // начальная позиция Черепашки: x0 := CX - 95; y0 := CY + 160; a0 := -18; size := 200; teta := 72; instinct := 'F+F+F+F+F+'; Start(); Execute(); end; // шестиугольник procedure Shestigon(); begin // начальная позиция Черепашки: x0 := CX - 120; y0 := CY + 80; a0 := 0; size := 150; teta := 60; instinct := 'F+F+F+F+F+F+'; Start(); Execute(); end; 91
// восьмиугольник procedure Vosmigon(); begin // начальная позиция Черепашки: x0 := CX - 150; y0 := CY + 70; a0 := 0; size := 130; teta := 45; instinct := 'F+F+F+F+F+F+F+F+'; Start(); Execute(); end; 92
Рис. 3. Черепашковые многоугольники Как вы видите, инстинкт становится всё более длинным, при этом одна и та же пара команд F+ просто повторяется n раз, если через n обозначить число вершин правильного многоугольника. Давайте научимся выращивать инстинкты! Пусть при рождении Черепашка получает простейший инстинкт, который принято называть аксиомой, а затем, с каждым годом инстинкт развивается и становится всё более сложным благодаря правилам роста. Для треугольника и других правильных многоугольников аксиома самая простая: axiom := 'F'; 93
Она означает, что Черепашка должна двигаться вперёд на расстояние size. Наша первая Черепашка только это и умела это делать. Поскольку все многоугольники строятся по одному и тому же сценарию, то и правило для них одно и то же: newF := 'F+F'; А действует оно так. Через год (возраст Черепашки определяется переменной iter) каждая команда F, в соответствии с правилом роста, заменяется значением переменной newF, то есть F+F: iter = 1  axiom := 'F+F' Это значит, что через год (а у Черепашек год короткий!) наша Черепашка научится вычерчивать угол (Рис. 4). Рис. 4. Молодая Черепашка - «угловатая» 94
На второй год с поведение Черепашки усложнится: iter = 2  axiom := '(F)+(F)'  '(F+F)+(F+F)'  'F+F+F+F' Опять в аксиоме каждая буква F заменяется выражением F+F. Для удобства преобразований мы воспользовались скобками, но самой Черепашке они не нужны. Вот теперь Черепашка может вычертить настоящий треугольник (Рис. 5). Рис. 5. Двухлетняя Черепашка умеет чертить треугольник Для построения треугольника достаточно инстинкта 'F+F+F+', то есть одну сторону Черепашка вычертит дважды. Увы, все инстинкты несовершенны, но зато довольно просты. Инстинкт Черепашки формируется, конечно, не вручную - она имеет для этого в мозгу специальную процедуру: 95
// Черепашка формирует инстинкт procedure CreateInstinct(); begin loop iter do begin instinct := string.Empty; for var j := 1 to axiom.Length do begin // очередная команда аксиомы: var cmd := axiom[j]; if (cmd = 'F') then instinct += newF else instinct += cmd; end; axiom := instinct; end end; Её действие мы уже разобрали. Каждая буква F заменяется значением переменной newF, а все остальные символы (команды поворота + и - ) переходят в новый инстинкт без изменения. Так как мы привыкли использовать при моделировании поведения Черепашки переменную instinct, то результатом работы процедуры CreateInstinct пусть будет значение этой переменной, хотя мы могли бы заменить её и переменной axiom. Выделим «треугольный» инстинкт в отдельную процедуру: procedure Triangle(); begin // начальная позиция Черепашки: x0 := CX - 140; y0 := CY + 160; a0 := 0; teta := 120; size := 300; 96
speed := 5; iter := 2; axiom := 'F'; newF := 'F+F'; CreateInstinct(); Start(); Execute(); end; Вы научились формировать простейший инстинкт, повинуясь которому Черепашка рисует треугольник (см. Рис. 5⬆). Достаточно изменить значение угла teta, и новый инстинкт – для построения квадрата – готов: procedure Quadrat(); begin // начальная позиция Черепашки: x0 := CX - 140; y0 := CY + 160; a0 := 0; size := 300; teta := 90; //speed := 10; iter := 2; axiom := 'F'; newF := 'F+F'; CreateInstinct(); Start(); Execute(); end; На третий год жизни Черепашка сумеет вычертить пятиугольник, шестиугольник и восьмиугольник: procedure Pentagon(); begin 97
// начальная позиция Черепашки: x0 := CX - 95; y0 := CY + 160; a0 := -18; size := 200; teta := 72; iter := 3; axiom := 'F'; newF := 'F+F'; CreateInstinct(); Start(); Execute(); end; procedure Shestigon(); begin // начальная позиция Черепашки: x0 := CX - 120; y0 := CY + 80; a0 := 0; size := 150; teta := 60; iter := 3; axiom := 'F'; newF := 'F+F'; CreateInstinct(); Start(); Execute(); end; procedure Vosmigon(); begin // начальная позиция Черепашки: x0 := CX - 150; y0 := CY + 70; a0 := 0; 98
size := 130; teta := 45; iter := 3; axiom := 'F'; newF := 'F+F'; CreateInstinct(); Start(); Execute(); end; Мы могли бы продолжать этот процесс и дальше, но совершенно понятно, что с годами Черепашка сможет выстраивать любые правильные многоугольники, поэтому давайте усложним поведение Черепашки с помощью более изощрённых инстинктов и научим её чертить фракталы. Фракталы Фракталы – это замечательные геометрические фигуры, составленные из точно таких же фигур, но меньшего размера. Например, из маленьких отрезков вы можете составить отрезок большей длины, из него – ещё более длинный, и так до бесконечности (Рис. 1). Рис. 1. Фрактальные отрезки Аналогично из квадратиков вы можете построить большой квадрат (Рис. 2). 99
Рис. 2. Фрактальные квадраты Примерами фракталов в природе могут служить: кровеносная система, береговая линия, горный рельеф, перистые облака (Рис. 3), разряд молнии, системы рек с притоками, трещины в почве, ветвистые растения, фьорды, прожилки на листьях (Рис. 4), ледяные узоры на стёклах… Рис. 3. Перистые облака 100
Рис. 4. Жилкование листа Снежинка Коха Более сложный пример фрактальной кривой придумал в 1904 году Гельг фон Кох. Построение начинается с отрезка, который можно условно разделить на три равные части (Рис. 1а). Среднюю часть отрезка заменяем двумя отрезками, каждый из которых равен трети исходного отрезка (Рис. 1б). Затем мы повторяем эту операцию сколько угодно раз (Рис. 1в). Рис. 1. Построение снежинки Коха 101
Фрактальная Черепашка Если вы и дальше продолжите построение кривой Коха, то быстро убедитесь, что вручную её строить весьма утомительно. Поэтому лучше научить этому занятию Черепашку. Только строить она будет не кривую, а снежинку Коха, которая отличается от кривой только тем, что её звенья не выстраиваются в прямую линию, а поворачиваются на 60 градусов, вследствие чего кривая становится замкнутой. Итак, с углом teta мы определились. С аксиомой тоже можно справиться, а вот правило, порождающее инстинкт, придётся позаимствовать у самого господина Коха: // Снежинка Коха: procedure Koch(); begin x0 := CX - 75; y0 := CY + 135; a0 := 0; speed := 10; teta := 60; size := 90; iter := 1; axiom := 'F++F++F'; newF := 'F-F++F-F'; CreateInstinct(); Start(); Execute(); end; 102
С возрастом Черепашка рисует всё более и более сложные узоры, так что в четырёхлетнем возрасте снежинка у неё получается не хуже настоящей (Рис. 1, справа)! // Снежинка Коха: procedure Koch(); begin a0 := 0; speed := 10; teta := 60; x0 := CX - 76; y0 := CY + 150; size := 3.5; iter := 4; axiom := 'F++F++F'; newF := 'F-F++F-F'; CreateInstinct(); Start(); Execute(); end; В 1890 году Джузеппе Пеано построил функцию, график которой, названный кривой Пеано, покрывает квадратами (или, если угодно, ромбами) всю плоскость. И хотя кривая Пеано не является фракталом в полном смысле этого слова, но всё равно очень забавно наблюдать, как Черепашка её вычерчивает (Рис. 3). С годами (то есть с увеличением значения iter) она «квадрирует» все большую и большую поверхность: // Кривая Пеано: procedure Peano(); begin x0 := CX - 300; y0 := CY + 310; a0 := 45; 103
teta := 90; size := 32; iter := 3; speed := 10; axiom := 'F'; newF := 'F-F+F+F+F-F-F-F+F'; CreateInstinct(); Start(); Execute(); end; Рис. 1. Кривая Коха при iter=1 и iter=4 Вообще говоря, настоящие снежинки также имеют 6 лучей, но более сложной формы (Рис. 2). 104
Рис. 2. Стилизованные снежинки Если вы хотите получить большие картинки, то удвойте размеры окна программы: // размеры окна: const GW_WIDTH = 320 * 2; const GW_HEIGHT = 320 * 2; Ещё более усложнив инстинкт, мы научим Черепашку выделывать очень сложную кривую (Рис. 4): // 32-сегментная кривая procedure SC32(); begin x0 := CX - 130; y0 := CY + 135; a0 := 0; teta := 90; size := 4; iter := 2; speed := 10; axiom := 'F+F+F+F'; newF := '-F+F-F-F+F+FF-F+F+FF+F-F-FF+FF-FF+F+F-FF-F-F+FF-F-F+F+F-F+'; CreateInstinct(); Start(); Execute(); end; 105
Рис. 3. Кривая Пеано при iter= 3 106
Рис. 4. 32-сегментная кривая 107
Добавим к инстинкту Черепашки новую команду, которая в генетическом коде обозначается буквой b. Получив такую команду, Черепашка переползает вперёд, но линию за собой не чертит. Для обработки нового гена мы прибавим в процедуру СreateInstinct одну строку: procedure CreateInstinct(); begin loop iter do begin instinct := string.Empty; for var j := 1 to axiom.Length do begin // очередная команда аксиомы: var cmd := axiom[j]; case cmd of 'F': instinct += newF; 'b': instinct += newb; else instinct += cmd; end; end; axiom := instinct; end end; То есть новый ген b входит в состав инстинкта аналогично гену F. А вот в процедуре Execute Черепашка должна сначала поднять карандаш, а после перемещения снова опустить его: // Черепашка выполняет инстинкт procedure Execute(); begin // Черепашка считывает команды: for var i := 1 to instinct.Length do begin // очередная команда: var cmd := instinct[i]; // двигаться вперед на один шаг: case cmd of 'F': Turtle.Move(size); 108
'+': Turtle.Turn(teta); '-': Turtle.Turn(-teta); 'b': begin Turtle.PenUp; Turtle.Move(size); Turtle.PenDown; end; end; end end; С помощью этого гена Черепашка может рисовать фигуры, состоящие из нескольких не связанных между собой частей (Рис. 5): // Мозаика procedure Mozaic(); begin x0 := CX + 160; y0 := CY + 160; a0 := 0; teta := 90; size := 9; iter := 2; speed := 10; axiom := 'F-F-F-F'; newF := 'F-b+FF-F-FF-Fb-FF+b-FF+F+FF+Fb+FFF'; newb := 'bbbbbb'; CreateInstinct(); Start(); Execute(); end; Обратите внимание: каждое вхождение нового гена b в инстинкт заменяется значением переменной newb! 109
Рис. 5. Мозаика при iter= 2 Вы можете добавлять к инстинкту Черепашки и другие гены, которые обозначаются буквами латинского алфавита. Они участвуют в формировании инстинкта, но при его выполнении игнорируются. 110
Для каждого нового гена должно быть записано правило, по которому он заменяется в инстинкте другими генами. Нашей Черепашке вполне достаточно четырёх неисполняемых генов, которые формируют инстинкт в процедуре CreateInstinct: var newb: newW: newX: newY: newZ: string; string; string; string; string; procedure CreateInstinct(); begin loop iter do begin instinct := string.Empty; for var j := 1 to axiom.Length do begin // очередная команда аксиомы: var cmd := axiom[j]; case cmd of 'F': instinct 'b': instinct 'W': instinct 'X': instinct 'Y': instinct 'Z': instinct else instinct end; end; axiom := instinct; end end; += += += += += += += newF; newb; newW; newX; newY; newZ; cmd; В процедуру Execute никаких изменений вносить не следует, поскольку эти команды Черепашка не выполняет. Зато новые гены могут настолько усложнить инстинкт Черепашки, что она начнёт вычерчивать кривые удивительной красоты! Сейчас мы научим нашу КиберЧерепашку вычерчивать Треугольный треугольник: 111
// Треугольники procedure Triangles(); begin x0 := CX - 55; y0 := CY + 95; a0 := 30; teta := 120; size := 36; iter := 6; speed := 10; axiom := 'bX'; newb := 'b'; newF := 'F'; newX := '--FXF++FXF++FXF--'; CreateInstinct(); Start(); Execute(); end; Хотя задача оказалась непростой, но КиберЧерепашка справилась с заданием отлично (Рис. 6). С двумя новыми генами Черепашка построит кривую Пеано-Госпера, которая, как и кривая Коха, также напоминает снежинку, но заполненную внутри замысловатыми линиями (Рис. 7): // Кривая Пеано-Госпера: procedure PeanoGosper(); begin x0 := CX - 262; y0 := CY - 75; a0 := 0; teta := 60; size := 10; iter := 4; 112
speed := 10; axiom := 'FX'; newF := 'F'; newX := 'X+YF++YF-FX--FXFX-YF+'; newY := '-FX+YFYF++YF+FX--FX-Y'; CreateInstinct(); Start(); Execute(); end; А вот чтобы научить Черепашку рисовать почти настоящую снежинку, мы должны наделить её памятью. По команде [ она запомнит свои координаты и угол поворота, а по команде ] возвратится в это состояние. Поскольку эти гены не влияют на формирование инстинкта, то процедура СreateInstinct останется без изменений, а в процедуру Execute нужно добавить строчки: // стек: _stack := 'Stack'; // Черепашка выполняет инстинкт procedure Execute(); begin // Черепашка считывает команды: for var i := 1 to instinct.Length do begin // очередная команда: var cmd := instinct[i]; // двигаться вперед на один шаг: case cmd of 'F': Turtle.Move(size); '+': Turtle.Turn(teta); '-': Turtle.Turn(-teta); 'b': begin Turtle.PenUp; Turtle.Move(size); Turtle.PenDown; 113
end; '[': begin Stack.PushValue(_stack, Turtle.Angle); Stack.PushValue(_stack, Turtle.X); Stack.PushValue(_stack, Turtle.Y); end; ']': begin Turtle.Y := Stack.PopValue(_stack); Turtle.X := Stack.PopValue(_stack); Turtle.Angle := Stack.PopValue(_stack); end end; end end; Как видите, память Черепашки устроена по принципу стека: Черепашка вспоминает последние запомненные события, после чего они стираются из её памяти. Теперь Черепашка не только умеет выполнять команды инстинкта, но и обладает памятью. А вот так она строит почти настоящую снежинку (Рис. 8): // Снежинка: procedure Snowflake(); begin x0 := CX; y0 := CY; a0 := 0; size := 12; iter := 2; teta := 60; speed := 10; // цвет линии: penColor := 'Blue'; axiom := '[F]+[F]+[F]+[F]+[F]+[F]'; newF := 'F[++F][-FF]FF[+F][-F]FF'; CreateInstinct(); Start(); 114
Execute(); // цвет линии: penColor := 'Green' end; С помощью очень простого инстинкта и памяти Черепашка нарисует очень красивую картинку, состоящую из ромбов (Рис. 9): // Ромбы procedure Rhombes(); begin x0 := CX+5; y0 := CY-264; a0 := 0; teta := 60; size := 20; iter := 6; speed := 10; axiom := 'F'; newF := '-F+F+[+F+F]-'; CreateInstinct(); Start(); Execute(); end; Кривая Пенроуза (Рис. 10) также состоит из ромбов, но для её построения Черепашке понадобится и очень сложный инстинкт, и крепкая память: // Кривая Пенроуза procedure Penrose(); begin x0 := CX + 5; y0 := CY - 0; a0 := 0; teta := 36; 115
size := 36; iter := 4; speed := 10; axiom := '[Y]++[Y]++[Y]++[Y]++[Y]'; newW := 'YF++ZF----XF[-YF----WF]++'; newX := '+YF--ZF[---WF--XF]+'; newY := '-WF++XF[+++YF++ZF]-'; newZ := '--YF++++WF[+ZF++++XF]--XF'; newF := ''; CreateInstinct(); Start(); Execute(); end; О мозаиках Пенроуза вы можете прочитать в книге Мартина Гарднера [ГМ93]. И последнее, чему мы научим нашу Черепашку, - это строить кривую, которая очень напоминает настоящее растение (Рис. 11): // Куст procedure Bush(); begin x0 := CX + 175; y0 := CY + 321; a0 := 0; size := 14; iter := 4; teta := 180 div 8; speed := 10; axiom := 'F'; newF := '-F+F+[+F-F-]-[-F+F+F]'; CreateInstinct(); Start(); Execute(); end; 116
Рис. 6. Треугольники при iter= 6 117
Рис. 7. Кривая Пеано-Госпера при iter= 4 118
Рис. 8. Снежинка при iter= 2 119
Рис. 9. Ромбы при iter= 6 120
Рис. 10. Кривая Пенроуза при iter= 4 121
Рис. 11. Куст при iter= 4 122
L-системы И вот, глядя на этот очаровательный «куст», мы можем, наконец, раскрыть тайну поведения нашей Черепашки. Оказывается, инстинкт Черепашки описывается с помощью L-системы, которая была предложена шведским биологом Аристидом Линденмайером (поэтому иначе они называются системами Линденмайера) для описания растений. Пример с кустом и следующий – с деревом (Рис. 1) - убедительно доказывают, что с помощью этой системы можно конструировать весьма правдоподобные растения (в некоторых графических редакторах они генерируются практически так же). // Дерево procedure Tree(); begin x0 := CX; y0 := CY + 320; a0 := 0; size := 10; iter := 5; teta := 180 div 6; speed := 10; axiom := 'F'; newF := 'F[-F]F[+F][F]'; CreateInstinct(); Start(); 123
Execute(); end; Рис. 1. Дерево 124
Все древовидные структуры, а также многие рассмотренные нами фракталы описываются аксиомой и несколькими правилами, которые с увеличением числа итераций iter могут создать кривые любой степени детализации (в том числе и бесконечной, если у вас есть бесконечно много времени и других ресурсов). Неожиданно оказалось, что построение кривых по системе Линденмайера очень удобно реализовать с помощью черепашьей графики, поскольку Черепашка умеет выполнять все команды, используемые в L-системах. И ещё парочка деревьев (Рис. 2 и 3): // Дерево2 procedure Tree2(); begin x0 := CX; y0 := CY + 320; a0 := 0; size := 7.5; iter := 4; teta := 180 div 6; speed := 10; axiom := 'F'; newF := 'F[+F]F[-F]F'; CreateInstinct(); Start(); Execute(); end; 125
// Дерево3 procedure Tree3(); begin x0 := CX + 80; y0 := CY + 310; a0 := 0; size := 11.5; iter := 4; teta := 180 div 6; speed := 10; axiom := 'F'; newF := 'FF-[-F+F+F]+[+F-F-F]'; CreateInstinct(); Start(); Execute(); end; 126
Рис. 2. Дерево от дерева недалеко падает 127
Рис. 3. Кустистое дерево 128
Черепаший зоопарк Вы познакомились с двумя Черепашками. Самая молодая Черепашка пришла к нам прямиком из программы PascalABC.NET. Второй Черепашке уже больше десятка лет, но она до сих пор находится в прекрасной физической форме! Но мир полон и другими Черепашками. С некоторыми из них вы познакомитесь в следующих проектах. Очень БОЛЬШАЯ Черепашка Седлай черепаху! Со времён языка Лого Черепашка используется как наглядное средство при обучении детей программированию. Эта славная традиция не увяла и до настоящего времени. Например, в книге Python für Kids (Рис. 1), которая выдержала в Германии уже 4 издания, всё обучение программированию строится вокруг этого милого животного (это я о Черепашке, а не о Питоне – хотя упомянутая недобрым словом змея не имеет прямого отношения к одноимённому языку). 129
Рис. 1. Обложка книги Справедливости ради следует отметить, что немецкая Черепашка (Schildkröte) и бойчее, и мощнее Смоллбейсиковской. Например, наша Черепашка только и умеет что вычерчивать линии, а немецкая - ещё и заливные фигуры (Рис. 2). Рис. 2. Заливные квадраты 130
Наша Черепашка всегда пребывает в одиночестве, а немецких черепах можно плодить целыми стадами (Рис. 3) и беспощадно топтать ими одновременно всю канву окна приложения. Рис. 3. 36 Черепашек, разбегающихся во все стороны Благодаря невероятному совпадению или везению Черепашка в качестве средства обучения попала не в суп, а ещё и в американскую книжку – и тоже по программированию на Питоне, и тоже для детей (Рис. 4). Американская Черепашка так же одинока, как и наша, и даже на настоящую черепаху не похожа, но зато умеет рисовать закрашенные фигуры (Рис. 5). Не обошёлся без Черепашки и видеокурс PluralSight - Teaching Kids Programming with C# (Рис. 6), посвящённый обучению детей, начиная с 10-летнего возраста, программированию на языке Си-шарп. Надо отметить, что видеокурс очень короткий, поэтому пользы от него будет немного. Впрочем, нам никто и ничто не мешает использовать Черепашку-курсистку в своих проектах (Рис. 7)! 131
Рис. 4. Другая книга для детей Рис. 5. Закрашенная звёздочка 132
Рис. 6. Всё лучшее – детям! Рис. 7. Ломаные спирали 133
Для этого добавьте ссылку на динамическую библиотеку SmallBasicFun.dll: {$reference 'LIB/SmallBasicLibrary.dll'} {$reference 'LIB/SmallBasicFun.dll'} uses Microsoft.SmallBasic.Library, SmallBasicFun; Вообще говоря, в библиотеке SmallBasicFun не одна, а две Черепашки – БОЛЬШАЯ и маленькая. Большая Черепаха, хотя и большая, но довольно глупая: она умеет рисовать только квадраты и правильные многоугольники (Рис. 8). Для этого она имеет 2 процедуры: GiantTortoise.DrawSquare(color : string); GiantTortoise.DrawShape(sides : integer; color : string); Рис. 8. Многоугольники большой Черепашки 134
Процедура DrawSquare помогает вычертить Черепахе квадрат со сторонами заданного цвета. Обратите внимание, что можно задать случайный цвет 'random', и тогда все стороны квадрата будут вычерчены линиями случайного цвета. Ни размеры квадрата, ни его положение на канве изменить нельзя. Эта процедура использует для рисования квадрата другую процедуру DrawShape, которой она передаёт значение параметра sides = 4. Вы можете задать другое значение этому параметру, и Черепаха послушно вычертит правильный многоугольник: {$reference 'LIB2/SmallBasicLibrary.dll'} {$reference 'LIB2/SmallBasicFun.dll'} uses Microsoft.SmallBasic.Library, SmallBasicFun; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 320; const GW_HEIGHT = 320; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Очень БОЛЬШАЯ Черепашка'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; // Квадрат procedure Quadrat(); 135
begin // рисуем квадрат: GiantTortoise.DrawSquare(Colors.Red); GiantTortoise.DrawSquare('random'); end; // Многоугольник procedure Polygon(); begin // рисуем многоугольник: GiantTortoise.DrawShape(9, 'random'); end; begin Prepare; Quadrat(); //Polygon(); end. Библиотечная Черепашка Вторая Черепашка – маленькая, да удаленькая! Она может выполнять всё, что умеет делать и стандартная Черепашка, и кое-что у неё есть в запасе! Чтобы Черепашка появилась на экране, нужно выполнить процедуру Show: Tortoise.Show(); Процедура Hide, наоборот, скрывает Черепашку, но при этом она продолжает выполнять все команды: 136
Tortoise.Hide(); Процедура Reset возвращает Черепашке параметры по умолчанию: Tortoise.Reset(); Две процедуры – SetX и SetY – переносят Черепашку в указанную точку клиентской области окна без вычерчивания линии: Tortoise.SetX(x : double); Tortoise.SetY(y : double); Процедура SetPosition одновременно изменяет обе координаты Черепашки: Tortoise.SetPosition(x, y : integer); Две других функции – GetX и GetY – сообщают нам текущие координаты Черепашки: Tortoise.GetX() : double; Tortoise.GetY() : double; Теперь мы можем создать Черепашку и поставить её перед фактом в самом центре окна (Рис. 1). 137
Рис. 1. Маленькая Черепашка в центре внимания {$reference 'LIB2/SmallBasicLibrary.dll'} {$reference 'LIB2/SmallBasicFun.dll'} uses Microsoft.SmallBasic.Library, SmallBasicFun; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; const GW_HEIGHT = 640; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Библиотечная Черепашка'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); 138
gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; // создаём Черепашку: Tortoise.Show(); // устанавливаем Черепашку в центре окна: Tortoise.SetX(CX); Tortoise.SetY(CY); end; . . . begin Prepare; // треугольная спираль: Spiral(600, 120, 8); end. Зная, что все Черепашки любят выделывать спирали, мы не можем отказать и нашей Черепашке в этом удовольствии! Процедура Move заставляет двигаться её вперёд на lengthInPixels пикселей: Tortoise.Move(lengthInPixels : double); Процедура MoveTo перемещает Черепашку в указанную точку с координатами (x,y): Tortoise.MoveTo(x, y : double); Процедура Turn поворачивает Черепашку на degrees градусов: по часовой стрелке, если угол положительный, или против часовой стрелки, если угол отрицательный: Tortoise.Turn(degrees : double); 139
Если предыдущая процедура отсчитывает угол от текущего поворота Черепашки, то процедура SetAngle поворачивает Черепашку точно в заданном направлении. Если angle = 0, то Черепашка смотрит вверх, если angle = 180, то Черепашка смотрит вниз: Tortoise.SetAngle (angle : double); Функция GetAngle возвращает текущий угол поворота Черепашки: Tortoise.GetAngle() : double; Функция SetOrientation объединяет усилия двух методов – SetPosition и SetAngle: Tortoise. SetOrientation( x, y, angle : integer) : double; Однако вернёмся к спиралям и напишем две простые процедуры для их вычерчивания: procedure Step(len, angle: double); begin Tortoise.Move(len); Tortoise.Turn(angle); end; procedure Spiral(len, angle, _step: double); begin // устанавливаем Черепашку: Tortoise.SetX(70); Tortoise.SetY(CY + 300); Tortoise.SetSpeed(10); Tortoise.SetPenColor('Green'); var i := len; while(i >= 10) do begin Step(i, angle); i -= _step; 140
end; end; Задавая разные значения параметру angle, вы будете получать очень красивые многоугольные спирали (Рис. 2-5): // треугольная спираль: Spiral(600, 120, 8); // квадратная спираль: Spiral(620, 90, 4); // пятиугольная спираль: Tortoise.SetAngle(-18); Spiral(380, 72, 2); // шестиугольная спираль: Spiral(320, 60, 1.5); Ещё более интересные спирали получаются, если задавать «неправильный» угол. Тогда спирали дополнительно закручиваются относительно центра (Рис. 6-10): // треугольная спираль с закруткой: Spiral(620, 120 + 1, 8); // квадратная спираль с закруткой: Spiral(620, 90+0.5, 4); // квадратная спираль Spiral(620, 90+1, 4); с закруткой: 141
// квадратная спираль Spiral(580, 90+2, 8); с закруткой: Рис. 2. Треугольная спираль 142
Рис. 3. Квадратная спираль 143
Рис. 4. Пятиугольная спираль 144
Рис. 5. Шестиугольная спираль 145
Рис. 6. Треугольная спираль с закруткой 146
Рис. 7. Квадратная спираль с закруткой 147
Рис. 8. Вторая квадратная спираль с закруткой 148
Рис. 9. Третья квадратная спираль с закруткой // шестиугольная спираль с закруткой: Spiral(320, 60.3, 1.5); 149
Рис. 10. Шестиугольная спираль с закруткой 150
Процедура SetPenColor устанавливает цвет линий: Tortoise. SetPenColor(color : string); А процедура SetPenWidth – их толщину: Tortoise. SetPenWidth(widthInPixels : double); Функции GetPenColor и GetPenWidth возвращают текущий цвет линий и их толщину: Tortoise.GetPenColor() : string; Tortoise.GetPenWidth() : double; Процедура PenDown опускает карандаш, и тогда Черепашка оставляет за собой след, а процедура PenUp поднимает его, и Черепашка заметает следы: Tortoise.PenDown(); Tortoise.PenUp(); Процедура SetSpeed задаёт перцу и скорости Черепашке. Самая медленная скорость – при значении параметра speed, равном 1, самая быстрая – при значении speed, равном 10: Tortoise. SetSpeed(speed : integer); 151
Когда нужно максимально ускорить Черепашку, следует воспользоваться функцией InstantSpeed с параметром goFast = true: Tortoise.InstantSpeed( goFast : boolean); Функция GetSpeed возвращает текущую скорость Черепашки: Tortoise.GetSpeed() : integer; А теперь давайте вычертим фрезу: // фреза procedure Squaggle(zoom, z: double); begin Tortoise.Move(50*zoom); Tortoise.Turn(150/z); Tortoise.Move(60*zoom); Tortoise.Turn(100/z); Tortoise.Move(30*zoom); Tortoise.Turn(90/z); end; procedure Squaggle(n: integer; zoom: double; z: double); begin Tortoise.SetSpeed(10); Tortoise.SetX(CX-180); Tortoise.SetY(CY); Tortoise.SetPenColor('Green'); loop n do Squaggle(zoom,z); end; Малозубая фреза (Рис. 11): // фреза: Squaggle(20, 5, 1); 152
Рис. 11. Фреза с зубами // фреза: 153
Squaggle(77, 5.5, 1.01); Рис. 12. Зубастая фреза 154
А можно и немного почеркаться прямо в окне приложения. Запускаем программу и водим мыша за нос (Рис. 13). Рис. 13. Черепашьи почеркушки 155
// почеркушки: Inspi(); // почеркушки procedure Inspi(side, angle, inc: double); begin for var i := 0 to 10 - 1 do begin Tortoise.Move(side); Tortoise.Turn(angle + inc * i); end; end; procedure Inspi(); begin Tortoise.SetX(CX); Tortoise.SetY(CY); Tortoise.SetSpeed(10); while(true) do begin Tortoise.SetX(gw.MouseX); Tortoise.SetY(gw.MouseY); Tortoise.SetPenColor(gw.GetRandomColor()); Inspi(100, 150, 1.5); end; end; 156
Блуждающие Черепашки Обе Черепашки – и большая, и маленькая – ведут свой род от совершенно дикой Черепашки из класса MyTurtle. Она плохо поддаётся дрессировке, но у неё есть одна способность, которая напрочь отсутствует у её одомашненных родственниц, - она умеет плодиться. И при создании каждой новой Черепашки ей можно задать свой любимый размерчик. Давайте создадим для начала пару разнокалиберных Черепашек: {$reference 'LIB2/SmallBasicLibrary.dll'} {$reference 'LIB2/SmallBasicFun.dll'} uses Microsoft.SmallBasic.Library, SmallBasicFun; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 320; const GW_HEIGHT = 320; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Блуждающие Черепашки'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); 157
gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; // создаём первую Черепашку: var turtle1 := new MyTurtle(); turtle1.Show(); // устанавливаем Черепашку в центре окна: turtle1.X := CX; turtle1.Y := CY; // создаём вторую Черепашку: var turtle2 := new MyTurtle(1.6); turtle2.Show(); turtle2.X := CX + 20; turtle2.Y := CY + 20; end; begin Prepare; end. Как вы видите, дело это совершенно нехитрое – и вот уже новорождённые Черепашки предстали перед нашими глазами (Рис. 1). Немного поднатужившись, мы сможем породить и 36 Черепашек – что мы глупее немцев с их питоновыми Черепашками? Но породить мало – нужно ещё придать Черепашкам правильное направление и дать им крепкого родительского пинка под зад, чтобы они разбежались во все стороны: procedure Running(); begin var turtles := new MyTurtle[36]; for var i := 0 to 36 - 1 do begin turtles[i] := new MyTurtle(1.6); turtles[i].X := CX; 158
turtles[i].Y := CY; turtles[i].Angle := i * 360 / 36; turtles[i].Speed := 10; turtles[i].Show(); end; foreach var t in turtles do t.Move(300); end; Рис. 1. Двойняшки-Черепашки Моментальное семейное фото с места событий демонстрирует полную победу наших Черепашек над иноземными панцерами (Рис. 2). А сейчас давайте смоделируем ситуацию поиска жизненного пути неориентированными в жизни созданиями. 159
Рис. 2. Фигурно! В процедуре RandomWalk мы производим на свет 12 юных Черепашек, которые тут же пускаются в бесконечный путь познания добра и зла: 160
procedure RandomWalk(); begin var turtles := new MyTurtle[12]; for var i := 0 to 12 - 1 do begin turtles[i] := new MyTurtle(1.2); turtles[i].X := Random(CX - 50, CX + 50); turtles[i].Y := Random(CY - 50, CY + 50); turtles[i].LineColor := gw.GetRandomColor().AsColor(); turtles[i].Speed := 10; turtles[i].Show(); end; while(true) do for var i := 0 to 12 - 1 do begin turtles[i].Angle := Random(360); turtles[i].Move(Random(10, 50)); end; end; Поскольку ума у них ещё нет, а рвения хоть отбавляй, то они будут суетиться, ища место под солнцем, а их жизненный путь предстанет перед нашими глазами в виде перепутанного клубка нитей или своеобразного броуновского движения, которое англосаксоны называют случайным блужданием (Рис. 3). А чего больше в этом эксперименте - физики или педагогики, - решайте сами! Если увеличить толщину линий, то Черепашки натворят настоящую абстрактную картину (Рис. 4): turtles[i].LineWidth := 20; 161
Рис. 3. Эх, куда глаза глядят! 162
Рис. 4. Служенье муз не терпит суеты… 163
Черепашка из Small Visual Basic Вы уже хорошо знакомы с Черепашкой из библиотеки SmallBasic. Эта Черепашка не остановилась в своём развитии и эволюционировала дальше. Сам Small Basic превратился в Small Visual Basic (Рис. 1). Рис. 1. Эволюцию не остановишь Новая программа добавила слово Visual к названию родительской программы, поскольку написана на языке Visual Basic. И установщик программы, и исходники вы легко найдёте в Интернете. Все программы, использующие библиотеку SmallVisualBasicLibrary.dll должны начинаться с такого кода: 164
{$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; Саму библиотеку скопируйте в папку со своими программами. Многоугольники Вы уже знаете, что любая Черепашка с лёгкостью начертит любой многоугольник. Умеет это делать и наша новая знакомица. Вот так лихо она начертила правильный треугольник (Рис. 1): {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; const GW_HEIGHT = 640; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Многоугольники'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; // цвет линий: gw.PenColor := 'Red'; 165
// толщина линий: gw.PenWidth := 3; // цвет заливки фигур: gw.BrushColor := 'Yellow'; // скорость Черепашки: Turtle.Speed := 9; end; // треугольник procedure Triangle; begin Turtle.X := 10; Turtle.Y := GW_HEIGHT-10; Turtle.Show; Turtle.Turn(90); var r := GW_WIDTH - 20; loop 3 do begin Turtle.Move(r); Turtle.Turn(-120); end; end; begin Prepare; Triangle; end. Треугольник весьма хорош, а из кода можно узнать, что свойство Turtle.Speed задаёт скорость (прыткость) ползания Черепашки. Свойства Turtle.X и Turtle.Y телепортируют Черепашку в точку с заданными координатами. Процедура Turtle.Show показывает Черепашку на экране, а процедура Turtle.Hide скрывает её. Процедура Turtle.Turn(angle) поворачивает Черепашку на заданный угол. Если угол положительный, то по часовой стрелке, если отрицательный – в обратном направлении. Процедура Turtle.Move(disrtance) двигает Черепашку вперёд на заданное расстояние. 166
Рис. 1. Визуальный треугольник Цвет и толщина пера, как и раньше, определяются свойствами PenColor и PenWidth: 167
// цвет линий: gw.PenColor := 'Red'; // толщина линий: gw.PenWidth := 3; Мы также выбрали жёлтый цвет для закрашивания фигур: // цвет заливки фигур: gw.BrushColor := 'Yellow'; Вы, конечно, заметили, что ни одна наша Черепашка не умеет закрашивать фигуры, а только чертит контуры. Новая Черепашка освоила не только чертёжные, но и малярные работы! Пишем новую процедуру: // ЗАКРАШЕННЫЙ ТРЕУГОЛЬНИК procedure TriangleFill; begin Turtle.X := 10; Turtle.Y := GW_HEIGHT-10; Turtle.Show; Turtle.Turn(90); var r := GW_WIDTH - 20; Turtle.CreateFigure; loop 3 do begin Turtle.Move(r); Turtle.Turn(-120); end; Turtle.FillFigure(); end; Процедура Turtle.CreateFigure должна предшествовать рисованию замкнутой фигуры, а процедура Turtle.FillFigure после этого закрашивает фигуру заданным цветом. Рис. 2 наглядно показывает, что закрашенные фигуры гораздо красивее пустых, а соединения линий более качественные. 168
Рис. 2. Закрашенный треугольник Если Черепашка начертит только 2 стороны треугольника, то фигура всё равно будет правильно закрашена, как будто у неё есть третья, невидимая сторона (Рис. 3). 169
Рис. 3. Закрашенный угол 170
Точно так же Черепашка вычертит и красный квадрат с жёлтой заливкой. Но давайте вспомним замечательный квадрат Малевича, который совсем чёрный и без контура. Убираем контурные линии и макаем кисточку в чёрную краску: // ЗАКРАШЕННЫЙ КВАДРАТ procedure QuadratFill; begin // толщина линий: gw.PenWidth := 0; // цвет заливки фигур: gw.BrushColor := 'Black'; Turtle.X := 10; Turtle.Y := GW_HEIGHT-10; Turtle.Show; Turtle.Turn(90); var r := GW_WIDTH - 20; Turtle.CreateFigure; loop 4 do begin Turtle.Move(r); Turtle.Turn(-90); end; Turtle.FillFigure(); end; Запускаем программу – и черепаший шедевр готов (Рис. 4). Если число сторон многоугольника достаточно велико, то Черепашка начертит круг (Рис. 5). 171
Рис. 4. Рисуем по-чёрному 172
Рис. 5. Черепаший круговорот 173
Солнечный круг, Небо вокруг… Лев Ошанин Пусть всегда будет солнце А вот и звезда по имени Солнце (Рис. 6): // окружность procedure CircleFill; begin gw.BackgroundColor := 'DodgerBlue'; // толщина линий: gw.PenWidth := 0; // цвет заливки фигур: gw.BrushColor := 'Yellow'; Turtle.X := GW_WIDTH - 15; Turtle.Y := GW_HEIGHT / 2 - 10; var r := 24; var n := 80; Turtle.CreateFigure; loop n do begin Turtle.Turn(-360/n); Turtle.Move(r); end; Turtle.FillFigure(); end; 174
Рис. 6. Солнечный круг 175
Полярные кривые Гораздо удобнее чертить окружность по координатам её центра и по длине радиуса. Координаты точек окружности вычисляем по формулам: x = R * cos(Z) (1) y = R * sin(Z) (2) Точки последовательно соединяем отрезками прямых: // окружность procedure DrawCircle(xc,yc,r : double; n : integer := 80); begin // текущий полярный угол: var z := 0.0; // начальная позиция Черепашки: Turtle.X := xc + r; Turtle.Y := yc; Turtle.CreateFigure; loop n + 1 do begin // следующие координаты Черепашки: var x := xc + r * cos(z); var y := yc + r * sin(z); // чертим отрезок: Turtle.MoveTo(x,y); // следующий угол: z += 2 * PI / n; end; Turtle.FillFigure(); end; // толщина линий: gw.PenWidth := 2; // цвет заливки фигур: gw.BrushColor := 'Yellow'; 176
DrawCircle(CX, CY, 300); Результат мы получим тот же самый, но более геометрически (Рис. 1). Рис. 1. Солнечный круг – версия 2 177
У Черепашки в лапках оказался мощный чертёжный инструмент, и теперь она запросто вычертит разноцветные концентрические круги (Рис. 2): // толщина линий: gw.PenWidth := 2; gw.PenColor := 'Black'; // скорость Черепашки: Turtle.Speed := 100; for var i := 0 to 15 do begin // цвет заливки фигур: gw.BrushColor := gw.GetRandomColor; DrawCircle(CX, CY, 310 - 20 * i); end; Для большего художественного эффекта можно изменять радиусы кругов случайным образом (Рис. 3): // толщина линий: gw.PenWidth := 2; gw.PenColor := 'Black'; // скорость Черепашки: Turtle.Speed := 100; var dr := 0.0; var r := 310.0; while dr <= r - 20 do begin // цвет заливки фигур: gw.BrushColor := gw.GetRandomColor; DrawCircle(CX, CY, r - dr); dr += Random(20, 60); if r - dr < 20 then dr := dr - 20; end; 178
Рис. 2. Концентрические круги 179
Рис. 3. Дело случая Замечательно смотрятся эксцентрические круги (Рис. 4): // толщина линий: 180
gw.PenWidth := 2; gw.PenColor := 'Black'; // скорость Черепашки: Turtle.Speed := 100; var dr := 0.0; var r := 310.0; for var i := 0 to 7 do begin // цвет заливки фигур: gw.BrushColor := gw.GetRandomColor; DrawCircle(CX + 20 * i, CY - 20 * i, r - 40 * i); end; Любимая всеми Черепашками красно-жёлтая раскраска кругов (Рис. 5): // толщина линий: gw.PenWidth := 1.0; gw.PenColor := 'Black'; // скорость Черепашки: Turtle.Speed := 100; var dr := 0.0; var r := 310.0; var color1 := 'Red'; var color2 := 'Yellow'; for var i := 0 to 15 do begin var color := i mod 2 = 0 ? color1 : color2; // цвет заливки фигур: gw.BrushColor := color; DrawCircle(CX + 10 * i, CY - 10 * i, r - 20 * i); end; Чертим розетки (Рис. 6-8). // чертим розетки: DrawRosette(60, 0); // чертим розетки: DrawRosette(40, 40); 181
// чертим розетки: DrawRosette(50, 80); Рис. 4. Эксцентрические круги 182
Рис. 5. Становится жарко 183
Рис. 6. Черепашья розетка 184
Рис. 7. Черепаший розеточный бублик 185
Рис. 8. Черепашья розеточная баранка Совмещаем приятное для глаз с полезным для ума (Рис. 9): // чертим розетки: 186
DrawRosette(90, 60, true); Рис. 9. Позолотили 187
После иллюстраций код, производящий эту геометрическую красоту, должен быть вам абсолютно понятен: procedure Circle(xc,yc,r : double; n : integer := 80); begin // текущий полярный угол: var z := 0.0; // приращение угла: var dz := DegToRad(360/n); // начальная позиция Черепашки: Turtle.X := xc + r; Turtle.Y := yc; loop n + 1 do begin // следующие координаты Черепашки: var x := xc + r * cos(z); var y := yc + r * sin(z); // чертим отрезок: Turtle.MoveTo(x,y); // следующий угол: z += dz; end; end; // ЧЕРТИМ РОЗЕТКУ procedure DrawRosette(n, offset: integer; fill : boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // радиус: var r := GW_WIDTH / 4 - 5 - offset / 2; if fill then begin // толщина линий: gw.PenWidth := 2; gw.BrushColor := 'Yellow'; gw.PenColor := 'Red'; DrawCircle(CX, CY, GW_WIDTH / 2 - 5); end; 188
// толщина линий: gw.PenWidth := 1.8;//1.5; gw.PenColor := 'DarkGreen'; var angle := 0.0; var dAngle := DegToRad(360/n); loop n do begin var xc := CX + Cos(angle) * r + offset * Cos(angle); var yc := CY + Sin(angle) * r + offset * Sin(angle); Circle(xc,yc,r); angle += dAngle; end; end; Улитка Паскаля Надоело мне уже Лопать ваше бланманже Приготовьте для царя Сто улиток Паскаля! Новофилатов, Капризный Царь По названию кривой можно было бы предположить, что она названа в честь знаменитого французского физика Блеза Паскаля, но это не так. На самом деле, его опередил отец и математик в одном лице - Этьен Паскаль (Étienne Pascal (1588 - 1651)). Вот так-то: принято считать, что природа отдыхает на детях, но отец и сын Паскали опровергли этот постыдный вздор. 189
Улитку Паскаля называют также «по-латински» лимаконом Паскаля (Limacon) - от настоящего латинского слова limax, которое и значит улитка. Само крещение кривой произошло в 1650 году французским математиком, астрономом и физиком Жилем Робервалем (Рис. 1) (настоящая его фамилия - Персонье или Персон, Personier, Personne). Рис. 1. Gilles Personne de Roberval (1602-1675) Кроме интересной формы, улитка Паскаля обладает и «магическим» свойством - с её помощью можно решить одну из задач древности – о трисекции угла. Правда, реше- 190
ние будет уже не классическим, поскольку в оригинале требуется все построения выполнять только циркулем и линейкой. Однако с этими правилами задача неразрешима. Улитка Паскаля – частный случай эпитрохоиды. Вычерчиваем улитку (Рис. 2) и с трудом узнаём в этой петле заявленную выше «зверушку»: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; const GW_HEIGHT = 640; const CX = GW_WIDTH div 2; const CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Улитка Паскаля'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; 191
procedure DrawUlitka(); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; // Улитка Паскаля: var k := 1.0; var l := 2.0; var r := 90; var n := 120; var angle := 0.0; var dAngle := DegToRad(360 / n); var offset := 0.0; var x0: double := 70; var y0: double := CY; for var i := 0 to n + 1 do begin // радиус: var rz := (1 + l * Cos(k * angle)) * r; var x := x0 + l * rz * Cos(angle) + k * Cos(l * angle); var y := y0 + l * rz * Sin(angle) + k * Sin(l * angle); // первая точка: if i = 0 then begin Turtle.X := x; Turtle.Y := y; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; end; begin Prepare; 192
DrawUlitka(); end. Рис. 2. Улитка Паскаля 193
При других параметрах кривой мы вместо улитки Паскаля получим петельное сцепление (Рис. 3) и кардиоиду (название произошло от греческого слова καρδιά (кардиа) - сердце), которая, скорее, напоминает помидор сорта бычье сердце (Рис. 4), чем настоящий пламенный мотор (Рис. 5). Заливку сложных фигур Черепашка делает отвратительно, поэтому мы пойдём на хитрость: сначала начертим заливную фигуру без контура, а затем – контур без заливки: procedure DrawPetlya(fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'Yellow'; // петельное сцепление: var k := 2.0; var l := 2.0; var var var var var var var r := 52; n := 150; angle := 0.0; dAngle := DegToRad(360 / n); offset := 0.0; x0: double := 315; y0: double := CY; Turtle.CreateFigure; for var i := 0 to n + 2 do begin // радиус: var rz := (1 + l * Cos(k * angle)) * r; var x := x0 + l * rz * Cos(angle) + k * Cos(l * angle); var y := y0 + l * rz * Sin(angle) + k * Sin(l * angle); // первая точка: 194
if i = 0 then begin Turtle.X := x; Turtle.Y := y; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); end; begin Prepare; //DrawUlitka(); DrawPetlya(true); DrawPetlya(); end. Петля получилась великолепно! По такому же сценарию строим кардиоиду: procedure DrawCardioid(fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 2.8; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'Red'; // кардиоида: var k := 1.0; var l := 1.0; 195
var var var var var r := 240; n := 150; angle := 0.0; dAngle := DegToRad(360 / n); offset := 0.0; var x0: double := 115; var y0: double := CY; Turtle.CreateFigure; for var i := 0 to n + 2 do begin // радиус: var rz := (1 + l * Cos(k * angle)) * r; var x := x0 + l * rz * Cos(angle) + k * Cos(l * angle); var y := y0 + l * rz * Sin(angle) + k * Sin(l * angle); // первая точка: if i = 0 then begin Turtle.X := x; Turtle.Y := y; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); end; begin Prepare; //DrawUlitka(); // DrawPetlya(true); // DrawPetlya(); DrawCardioid(true); DrawCardioid(); end. 196
Рис. 3. Петельное сцепление 197
Рис. 4. Кардиоида 198
Рис. 5. Помидоры бычачьих форм Спирали В нашем деле главное – этот самый, реализьм! Кредо Шефа в интерпретации Лёлика, Кинокомедия Бриллиантовая рука У нас дела пойдут куда лучше, если мы представим себе улитку в виде спирали, на которую раковины многих одностворчатых моллюсков очень похожи. Мы не будь дураками окучим сразу две спирали – архимедову и логарифмическую. 199
Свойства первой спирали досконально изучил древнегреческий математик, физик, механик и инженер из Сиракуз гражданин Архимед (Рис. 1). Чем и обессмертил эту загогулину. Дословный перевод его имени – Супер-ум, так что скромностью его явно обнесли. Наверное, Архимед больше знаком вам по его знаменитому кличу Эврика! (по-гречески εὕρηκα значит нашёл), которым он выразил свою радость по случаю открытия гидростатического закона (он же школьный закон Архимеда). Рис. 1. Ἀρχιμήδης; 287 до н. э. — 212 до н. э. Формула архимедовой спирали очень проста, поэтому мы закодируем её в один приём (Рис. 2): 200
{$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; GW_HEIGHT = 640; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Спирали'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; procedure DrawSpiral(); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 2.0; gw.PenColor := 'DarkGreen'; var k := 5.0; var var var var var var r := 90; n := 240; angle := 0.0; dAngle := DegToRad(1080 / n); x0: double := 290; y0: double := CY; 201
for var i := 0 to n + 1 do begin // радиус: var rz := angle / k * r; var x := x0 + Cos(angle) * rz; var y := y0 + Sin(angle) * rz; // первая точка: if i = 0 then begin Turtle.X := x; Turtle.Y := y; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; end; begin Prepare; DrawSpiral; end. Но - одинокая спираль наводит тоску на жаждущую веселья душу, поэтому мы лучше одновременно закрутим несколько спиралей (Рис. 3-5 ). Зададимся риторическим вопросом: а похожа ли архимедова спираль на улитку, как мы изначально надеялись? – И честно ответим себе: ни фига она не похожа! Забавные спиральки получаются при больших значениях толщины линий (Рис. 6): procedure DrawSpiral2(nv : integer); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 24.0; gw.PenColor := 'DarkGreen'; 202
var k := 5.0; // поворот спирали: var da := 0.0; loop nv do begin var r := 82; var n := 1600; . . . И даже одноцветные спиральки по-своему интересны (Рис. 7). Конечно, Пабло Пикассо и другим абстракционистам именно так улитка и видится в их высокохудожественных грёзах (Рис. 8), но, поверьте, она совсем не такая! 203
Рис. 2. Эврика: Архимедова спираль наверчена! 204
Рис. 3. 3 Архимедовых спирали 205
Рис. 4. 4 Архимедовых спирали 206
Рис. 5. Клубок из шести спиралей 207
Рис. 6. Толстые спирали 208
Рис. 7. Вегетарианские спирали 209
Рис. 8. Почти абстрактная улитка Архимедову спираль активно используют питоны и другие удавы в своих акробатических упражнениях (Рис. 9). Рис. 9. Сетчатый питон 210
Примером почти идеальной архимедовой спирали могут служить аккуратно свернутые шланги и пожарные рукава (Рис. 10). Рис. 10. Архимедовы шланги Русское слово шланг произошло от немецкого Schlange – змея. А вот собственно шланг по-немецки называется Schlauch. То есть мы внешнее сходство между шлангами и змеями видим, а немцы нет. Зато немцы в очереди (тоже Schlange) видят именно змею. Итак, с нами приключилась полуэврика: мы нарисовали не совсем того представителя животного мира, которого хотели. Поэтому давайте вооружимся острым взором и пристально, взглядом оголодавшего француза, присмотримся к настоящим улиткам (Рис. 11). Присмотрелись? – Вот они такие и есть. А наш досадный геометрический промах заключается в том, что витки архимедовой спирали раскручиваются равномерно, а улиточные – «с ускорением». Поэтому для наших бионических экзерсисов больше подойдёт спираль логарифмическая, которую можно описать двухэтажным уравнением: 211
r = Rekz Рис. 11. Натуральные улитки Без особых хлопот мы переведём эту формулу на язык паскаля, который тоже был француз: // ЛОГАРИФМИЧЕСКАЯ СПИРАЛЬ procedure DrawLogSpiral(); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; var var var var var var var k := 0.14; r := 10; n := 240; angle := 0.0; dAngle := DegToRad(1080 / n); x0: double := 290; y0: double := CY; while angle < 8.3 * PI do begin // радиус: 212
var rz := Exp(k * angle) * r; var x := x0 + Cos(angle) * rz; var y := y0 + Sin(angle) * rz; // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; end; Нажимаем кнопочку заветную – и в лучшем виде получаем знатную логарифмическую спираль, которой позавидует любая, самая съедобная в мире улитка (Рис. 12). Величину «ускорения» раскрутки витков спирали можно варьировать коэффициентом k. При больших значениях k спирали получаются более улётными и скрываются за горизонтом уже на первом витке. Спиральная галактика Водоворот 213
Рис. 12. Логарифмическая спираль: k=0.14 214
Поскольку все мы – дети Галактики, то из нескольких логарифмических спиралей легко создадим вполне реалистическую картину лихо закрученной многозвёздной системы (Рис. 13). Если же мы взглянем на небо до зубов вооружённым глазом, то найдём там немало вот таких звёздных улиток. procedure DrawLogSpiral(nv : integer); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 2.0; gw.PenColor := 'DarkGreen'; var k := 0.2; // поворот спирали: var da := 0.0; var n := 240; var r := 9.5; var dAngle := DegToRad(1080 / n); var x0: double := 310; var y0: double := CY; loop nv do begin var angle := 0.0; while angle < 8.0 * PI do begin // радиус: var rz := Exp(k * angle) * r; var x := x0 + Cos(angle + da) * rz; var y := y0 + Sin(angle + da) * rz; // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; // случайный цвет спирали: gw.PenColor := gw.GetRandomColor; 215
da += DegToRad(360 / nv); end; end; Рис. 13. Логарифмическая галактика 216
В одной из спиральных галактик находится и наше Солнце, и Земля, и мы с вами (Рис. 14). Не правда ли, она очень похожа на наши логарифмические спирали? Рис. 14. Галактика Млечный путь в профиль Со спиралями связан и трагикомический случай. Знаменитый швейцарский математик Якоб Бернулли (Рис. 15) пожелал, чтобы на его могиле была изображена логарифмическая спираль. 217
Рис. 15. Jakob Bernoulli (1654 -1705) Однако за дело взялся второгодник и вместо логарифмической высек архимедову спираль, которая и по сию пору украшает нижнюю часть надгробия (Рис. 16). Вокруг спирали можно прочитать латинскую надпись EADEM MUTATA RESURGO (изменённая, я вновь воскресаю), которая отражает свойства именно логарифмической спирали. Вот так-то: математику учить нужно даже тем, кто хочет зарыть и свой, и чужой талант в землю! Рис. 16. Надгробие на могиле Якоба Бернулли 218
Спираль Ферма – это разновидность спирали Архимеда. Иногда её называют также параболической спиралью. Эта спираль названа в честь французского математика Пьера де Ферма (Рис. 17). Рис. 17. Pierre de Fermat, 1607-1665 Координаты точек этой спирали задаются параметрическими уравнениями: var x := x0 + sign * r * Sqrt(angle) * Cos(angle); var y := y0 + sign * r * Sqrt(angle) * Sin(angle); Полная спираль состотит из двух ветвей, котрые начинаются в одной точке, а их координаты отличаются только знаком sign. Сначала Черепашка вычерчивает зелёную ветвь, а затем – красную: DrawFermatSpiral(); DrawFermatSpiral(-1); 219
// СПИРАЛЬ ФЕРМА procedure DrawFermatSpiral(sign : integer := 1); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; if sign = -1 then gw.PenColor := 'Red'; var r := 50; var n := 240; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; while angle < 12 * PI do begin var x := x0 + sign * r * Sqrt(angle) * Cos(angle); var y := y0 + sign * r * Sqrt(angle) * Sin(angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; end; Получилась весьма изрядная спиральная загогулина (Рис. 18). 220
Рис. 18. Спираль Ферма 221
Лютики-цветочки: клевер, розочки, ромашки Но не выросла ещё та ромашка, На которой я себе погадаю. Песня из к/ф Ещё раз про любовь Пора нам, однако, от представителей фауны перейти к более безопасным представителям флоры, то есть к цветочкам. Когда-то очень давно итальянский математик и монах Бенедиктинского ордена Луиджи-Гидо Гранди (Рис. 1) написал вот такое бесхитростное уравнение в полярных координатах: r = R * sin(kZ) которое при разных значениях коэффициента k давало кривые, названные в его честь розами Гвидо Гранди. Нам не составит большого труда нарисовать эти розочки, которые почему-то больше похожи на совсем другие цветочки. Рис. 1. Luigi Guido Grandi (1671 – 1742) 222
Например, по формуле R= sin(2Z) (Рис. 2) будет вычерчен лист клевера, а по формуле R= sin(12Z) (Рис. 3) – ромашка. Легко понять, что параметр k определяет число лепестков (их будет в точности 2k). begin Prepare; DrawClever(true); DrawClever(); end. // ЧЕРТИМ КЛЕВЕР procedure DrawClever(fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'LightGreen'; var r := 400; var n := 1500; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; // клевер: var k := 2; Turtle.CreateFigure; while angle <= 2 * PI + 0.02 do begin var rz := Sin(k * angle) * r; var x := x0 + rz * Cos(angle); var y := y0 + rz * Sin(angle); // первая точка: if angle = 0 then begin Turtle.X := x; 223
Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); end; begin Prepare; // DrawClever(true); // DrawClever(); DrawRomashka(true); DrawRomashka; end. // ЧЕРТИМ РОМАШКИ procedure DrawRomashka(fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'White'; var r := 320; var n := 1500; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; // ромашка: 224
var k := 12; Turtle.CreateFigure; while angle <= 2 * PI + 0.02 do begin var rz := Sin(k * angle) * r; var x := x0 + rz * Cos(angle); var y := y0 + rz * Sin(angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); end; Мы сумеем вычертить и более экзотические цветки, если возьмём более увесистое уравнение: begin Prepare; // DrawClever(true); // DrawClever(); // // DrawRomashka(true); DrawRomashka; DrawStar(true); DrawStar(); end. // ЧЕРТИМ ЗВЁЗДОЧКУ procedure DrawStar(fill: boolean := false); begin // скорость Черепашки: 225
Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 2.8; gw.PenColor := 'Yellow'; // цвет заливки фигур: gw.BrushColor := 'Red'; var r := 100; var n := 1500; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; var k := 5; //var k := 12; Turtle.CreateFigure; while angle <= 2 * PI + 0.02 do begin var rz := r/(2 + Cos(angle * k + ((Floor(0.1 * angle)/5 Floor(Floor(0.1 * angle)/5))))) * 3; var x := x0 + rz * Cos(angle); var y := y0 + rz * Sin(angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); end; 226
Рис. 2. Роза Гвидо Гранди при k = 2 (клевер) 227
Рис. 2. Роза Гвидо Гранди при k = 12 (ромашка) И в этом случае параметр k задает число лепестков (или лучей) цветка, но уже один к одному (Рис. 3-4). 228
Рис. 3. Одинокая звёздочка при k = 5 229
Рис. 4. Одинокая звёздочка при k = 12 Цветочки станут наряднее, если мы повторим звёздочки несколько раз (Рис. 5-7): begin 230
Prepare; // DrawClever(true); // DrawClever(); // // DrawRomashka(true); DrawRomashka; // // DrawStar(true); DrawStar(); DrawStar(6, 10); //DrawStar(12, 5); //DrawStar(12,3); end. // ЧЕРТИМ ЗВЁЗДОЧКУ procedure DrawStar(k, no : integer); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; var var var var var r := 100; n := 1500; dAngle := DegToRad(1080 / n); x0: double := 320; y0: double := CY; var da := 0.0; var dda := DegToRad(no); loop integer(360 / k / no) do begin var angle := 0.0; while angle <= 2 * PI + 0.02 do begin var rz := r/(2 + Cos(angle * k + ((Floor(0.1 * angle)/5 Floor(Floor(0.1 * angle)/5))))) * 3; var x := x0 + rz * Cos(angle + da); var y := y0 + rz * Sin(angle + da); // первая точка: 231
if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; da += dda; end; end; А если мы к поворотам добавим смещения, то доведём наши звёзды до смущения (Рис. 8). DrawStar(6, 2, 50); // ЧЕРТИМ ЗВЁЗДОЧКИ СО СМЕЩЕНИЕМ procedure DrawStar(k, no, offset: integer); begin // скорость Черепашки: Turtle.Speed := 1000; // толщина линий: gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; var var var var var r := 85; n := 1500; dAngle := DegToRad(1080 / n); x0: double := 320; y0: double := CY; var da := 0.0; var dda := DegToRad(no); loop integer(360 / k / no) do begin var angle := 0.0; 232
while angle <= 2 * PI + 0.02 do begin var rz := r/(2 + Cos(angle * k + ((Floor(0.1 * angle)/5 Floor(Floor(0.1 * angle)/5))))) * 3; var x := x0 + rz * Cos(angle + da) + offset * Cos(angle); var y := y0 + rz * Sin(angle + da) + offset * Sin(angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; da += dda; end; end; Если чертить звёздочки очень грубо, то они превращаются в правильные треугольники: // ЧЕРТИМ ЗВЁЗДОЧКИ СО СМЕЩЕНИЕМ procedure DrawStar2(k, no: integer); begin // скорость Черепашки: Turtle.Speed := 1000; // толщина линий: gw.PenWidth := 1.5; gw.PenColor := 'DarkGreen'; var var var var var r := 300; n := 9; dAngle := DegToRad(1080 / n); x0: double := CX; y0: double := CY; var da := 0.0; var dda := DegToRad(no); 233
loop 60 do begin var angle := 0.0; while angle <= 2 * PI + 0.02 do begin var rz := r/(2 + Cos(angle * k + ((Floor(0.1 * angle)/5 Floor(Floor(0.1 * angle)/5))))) * 3; var x := x0 + rz * Cos(angle + da); var y := y0 + rz * Sin(angle + da); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; da += dda; end; end; Треугольники вращаются вокруг центра и создают забавную картинку (Рис. 9). 234
Рис. 5. Клонированные звёздочки при k = 6 235
Рис. 6. Клонированные звёздочки при k = 12 236
Рис. 7. Суперклонированные звёздочки при k = 12 237
Рис. 8. Клонированные звёздочки со смещением 238
Рис. 9. Звёзды в шоке! 239
Лемниската Бернулли Не дарите в женский день Своим милым дребедень – Лучше блюдечко с зарплатой Повяжите лемнискатой! Нововишневский, Красная джибурда Лемнискату Бернулли придумал в 1694 году тот самый Якоб, которому так не повезло с логарифмической спиралью. В полярных координатах её уравнение выглядит не очень приветливо: r = R√2cos(kZ) но, слегка поднапрягшись, мы справимся и с этой напастью: begin Prepare; // лемниската: DrawLem(true); DrawLem(false); // клевер: // DrawLem(true, Pi/2); // DrawLem(false,Pi/2); end. // ЧЕРТИМ ЛЕМНИСКАТУ БЕРНУЛЛИ procedure DrawLem(fill: boolean := false; a : double := 0.0); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 240
else gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'LightGreen'; var r := 220; var n := 1500; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; var k := 2; var offset := 0.0; Turtle.CreateFigure; while angle <= 2 * PI + 0.02 do begin var rz := r * Sqrt(2 * Cos(angle * k)); var x := x0 + Cos(angle + a) * rz + offset * Cos(angle); var y := y0 + Sin(angle + a) * rz + offset * Sin(angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: try Turtle.MoveTo(x, y) except Turtle.MoveTo(x0, y0)end; angle += dAngle; end; if fill then Turtle.FillFigure(); end; Лемниската - по-гречески λημνισχος - значит лента. В Древней Греции так называли повязку, которой закрепляли 241
венок на голове победителя в спортивных играх. Сам Бернулли окрестил кривую lemniscus. Лемниската Бернулли является частным случаем овала Кассини - кривой, которую за 14 лет до Бернулли исследовал итальянский и французский астроном Джованни Доменико Кассини (Рис. 1). Рис. 1. Giovanni Domenico/Jean-Dominique Cassini (1625-1712) Лемниската - по-гречески λημνισχος - значит лента. В Любители астрономии, безусловно, вспомнят кратеры Кассини на Луне и Марсе, а также знаменитую щель Кассини – «зазор» между кольцами Сатурна шириной около 5 тысяч километров (Рис. 2). 242
Рис. 2. Кольца Сатурна И здесь от величины параметра k зависит число лепестков у цветка. Обычная лемниската Бернулли очень похожа на опрокинутую восьмёрку или на математический знак бесконечности (Рис. 3). Если же перпендикулярно первой лемнискате вычертить ещё одну, то опять возникнет листок клевера (Рис. 4). 243
Рис. 3. Лемниската Бернулли при k = 2 244
Рис. 4. Клевер из двух лемнискат при k = 2 245
Обычно лист клевера состоит из трёх равных частей, почему и называется трилистником (Рис. 5). Именно в таком виде он символизирует Ирландию на гербе Великобритании (Рис. 6) (роза обозначает Англию, чертополох - Шотландию и лук-порей - Уэльс). Рис. 5. Клевер-трилистник 246
Рис. 6. Варианты герба Великобритании с листочками клевера По преданию, с помощью трилистника Святой Патрик объяснял людям, что такое Святая Троица. «Правильный» лист клевера украшает эмблему немецкого футбольного клуба Greuther Fürth из пригорода Нюрнберга (Рис. 7). 247
Рис. 7. Клеверная эмблема Иногда можно найти и четырёхлистник, который, как принято считать во многих странах мира, приносит удачу. Например, многие немецкие газеты вставили счастливый листок в новогодние поздравления (Рис. 8). Рис. 8. Год будет счастливым! Поскольку сумма первых двух цифр 2011 года равна сумме двух последних цифр, то его также можно считать удачным! Задав параметру k значение 3, мы как раз и получим трилистник (Рис. 9) (или пропеллер, если вы не любите ботанику). А чтобы изобразить розочку, достаточно несколько раз прокрутить пропеллер вокруг полюса (Рис. 10): DrawLems(3, 6); // ЧЕРТИМ ЛЕМНИСКАТЫ procedure DrawLems(k, no : integer); begin 248
// скорость Черепашки: Turtle.Speed := 100; // толщина линий: gw.PenWidth := 1.5; gw.PenColor := 'DarkGreen'; var r := 220; var n := 1500; var dAngle := DegToRad(1080 / n); var x0: double := CX; var y0: double := CY; var da := 0.0; var dda := DegToRad(no); loop integer(360 / k / no) do begin var angle := 0.0; while angle <= 2 * PI + 0.02 do begin var rz := r * Sqrt(2 * Cos(angle * k)); var x := x0 + Cos(angle + da) * rz; var y := y0 + Sin(angle + da) * rz; // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; try Turtle.MoveTo(x, y) except Turtle.MoveTo(x0, y0)end; angle += dAngle; end; da += dda; end; end; 249
Рис. 9. Лемниската-пропеллер при k = 3 250
Рис. 10. Лемниската-розочка при k = 3 251
Лемниската Бернулли ведёт себя очень скрытно возле полюса, поэтому перемещаем Черепашку внутри блока try..except: try Turtle.MoveTo(x, y) except Turtle.MoveTo(x0, y0)end; Когда возникнет ошибка, мы просто соединим текущую точку с начальной. Фигуры Лиссажу У девчонок на пляжу Сплошь фигуры Лиссажу! Нововишневский, Сборник Всякая фигурень Фигуры Лиссажу очень похожи на клубок синусоид, вписанных в прямоугольник. Вид синусоид определяется параметрами kx и ky (Рис. 1-4). Например, при kx=1, ky=2 получится удивлённая «лемниската Бернулли». {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; GW_HEIGHT = 640; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; 252
procedure Prepare(); begin gw.Hide(); gw.Title := 'Фигуры Лиссажу'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; // ЧЕРТИМ ФИГУРЫ ЛИССАЖУ procedure DrawLissajous(fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.8; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'LightGreen'; // // // // // // var kx := 1; var ky := 2; var kx := 3; var ky := 2; var kx := 7; var ky := 11; var kx := 17; var ky := 11; var r := 100; var n := 3000; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; 253
Turtle.CreateFigure; while angle <= 2 * PI + 0.01 do begin var rz := 3 * r; var x := x0 + Cos(angle * kx) * rz; var y := y0 + Sin(angle * ky) * rz; // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); end; begin Prepare; DrawLissajous(true); // DrawLissajous(); end. 254
Рис. 1. Фигура Лиссажу при kx=1, ky=2 255
Рис. 2. Фигура Лиссажу при kx=3, ky=2 256
Рис. 3. Фигура Лиссажу при kx=7, ky=11 257
Рис. 4. Фигура Лиссажу при kx=17, ky=11 Как следует из названия, эти кривые были найдены французским математиком Жюлем Антуаном Лиссажу (Рис. 5). 258
Рис. 5. Jules Antoine Lissajous (1822 - 1880) По(пу)лярная анимация Эй, товарищ! Больше жизни! Спортивный марш Музыка И. Дунаевского, стихи В. Лебедева- Кумача А теперь давайте простыми (с)подручными средствами анимируем наши унылостатичные картинки. Для этого мы отправим Черепашку двигаться вдоль да по кривой и ставить на ней красные точки: procedure Animation(); begin DrawLissajous(true); DrawLissajous(); // скорость Черепашки: Turtle.Speed := 10; gw.PenColor := 'Red'; gw.PenWidth := 4.8; 259
var kx := 1; var ky := 2; var r := 100; var n := 1500; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; var np := 0; while angle <= var rz := 3 var x := x0 var y := y0 2 * + + * PI + 0.01 do begin r; Cos(angle * kx) * rz; Sin(angle * ky) * rz; // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; if np mod 3 = 0 then Turtle.PenDown else Turtle.PenUp; np += 1; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; end; Процесс фигурного движения точки вдоль кривой выглядит живо и забавно (Рис. 1). Легко заметить, что красные точки расставлены неравномерно. Дело в том, что наш метод вычисления координат точек кривой приводит к тому, что где-то их 260
окажется густо, а где-то – пусто. На Рис. 2 хорошо видно, что, чем больше кривизна кривой, тем меньше скорость Черепашки, и наоборот. Рис. 1. Черепашьи мульти-пульти 261
Рис. 2. Осторожнее на поворотах! 262
Трохоиды В русской народной сказке Колесо описана трогательная, но вполне реалистичная история о том, как незадачливый переросток никак не мог жениться по причине своего злостного косноязычия. Тогда его мамаша посоветовала ему говорить с девушками «покруглее». На следующем сватании этот маменькин сынок сидит-сидит да и встрянет в разговор: «Колесо!». И эта декламация продолжалась до тех пор, пока жениха вместе с его мамашей не выгнали в очередной раз с позором. И тут мамаша спрашивает своего отпрыска: - Ну что ты всё заладил: «колесо да колесо?» На что последний резонно отвечает ей: - Так вы же сами, мамаша, велели мне говорить покруглее! Как отмечал наш классик, Александр Сергеевич Пушкин, сказка ложь, да в ней намёк!, посему поговорим о кривых покруглее. А чтобы было совсем кругло, возьмем пару окружностей и покатим одну по другой. Катить окружность нужно без проскальзывания! Первую окружность назовём образующей, вторую - опорной. В любом месте образующей окружности, внутри неё или даже за её пределами закрепим карандаш (Рис. 1). Главное, чтобы карандаш был жёстко с ней связан, тогда при качении он оставит на бумаге след – кривую, которую математики называют трохоидой. По-гречески слово trochoeidе́s значит похожий на колесо, круглый. Отношение радиусов образующей и опорной окружностей обозначают буквой m = r / R. 263
Расстояние от карандаша до центра образующей окружности принято обозначать буквой h. Если катить образующую окружность по «ободу» снаружи, то мы получим эпитрохоиду, если внутри «обода» – гипотрохоиду. По-гречески epi – над, hypo – под. Рис. 1. Процесс (по)рождения эпи- и гипотрохоид Если h = r, то карандаш вычертит эпициклоиду (Рис. 2, слева). 264
Рис. 2. Различные трохоиды При h < r -укороченную (сжатую) эпитрохоиду (Рис. 2, в центре), а при h > r -удлинённую (растянутую) эпитрохоиду (Рис. 2, справа). Но кроме столь скупых трохоид, мы сможем, поигрывая параметрами, вычерчивать самые настоящие узоры! Кроме собственно трохоиды, мы серым цветом намечаем опорную окружность, чтобы облегчить себе понимание происходящих верчений окружностей (Рис. 3). В процедуре DrawE мы выбираем требуемое число оборотов производящей окружности. И задать его нужно так, чтобы узор не прервался на самом интересном месте! Для этого подбираем число nep так, чтобы производящая окружность объехала опорную целое число раз. 265
Рис. 3. (Не)наглядный пример // ЧЕРТИМ ЭПИТРОХОИДЫ procedure DrawE(fill: boolean := false); begin 266
// скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.5; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'LightGreen'; var n := 1000; var dAngle := DegToRad(1080 / n); var angle := 0.0; var x0: double := 320; var y0: double := CY; // радиус опорной окружности: var rop := 200; // радиус производящей окружности: var rpr := 20; // смещение от центра: var h := 90; Вычисляем число оборотов и «по-научному»: var nep := integer(Rop/NOD(Rop, rpr)); Обратите внимание, что здесь мы пользуемся функцией NOD: // БЫСТРЫЙ АЛГОРИТМ ЕВКЛИДА function NOD(n1, n2: integer): integer; begin while (n2 > 0) do begin var n := n1 mod n2; (n1, n2) := (n2, n); end; Result := n1; end; 267
var m := rpr / Rop; Turtle.CreateFigure; while angle <= 2 * nep * PI + 0.025 do begin var x := x0 + (Rop + rpr) * Cos(m * angle) – h * Cos(angle + m * angle); var y := y0 + (Rop + rpr) * Sin(m * angle) - h * Sin(angle + m * angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); // чертим опорную окружность: gw.PenColor := 'Gray'; gw.DrawEllipse(CX-rop,CY-rop,2*rop, 2*rop); end; Эпитрохоида с другими параметрами (Рис. 4): // радиус опорной окружности: var rop := 88; // радиус производящей окружности: var rpr := 55; // смещение от центра: var h := 124; Ещё одна эпитрохоида (Рис. 5): // радиус опорной окружности: 268
var rop := 104; // радиус производящей окружности: var rpr := 92; // смещение от центра: var h := 92; Нет некрасивых трохоид (Рис. 6): x0 := CX + 30; y0 := CY + 30; // радиус опорной окружности: var rop := 125; // радиус производящей окружности: var rpr := 85; // смещение от центра: var h := 125; Формулы для расчёта гипотрохоиды (Рис. 7) отличаются от эпитрохоидных только знаками, так что обсуждать их мы не будем. // ЧЕРТИМ ГИПОТРОХОИДЫ procedure DrawH(fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.5; gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'LightGreen'; var n := 1500; var dAngle := DegToRad(1080 / n); var dcx := 0; var dcy := 0; 269
var angle := 0.0; var x0: double := 320; var y0: double := CY; // радиус опорной окружности: var rop := 176; // радиус производящей окружности: var rpr := 64; // смещение от центра: var h := 200; var nep := integer(Rop/NOD(Rop, rpr)); var m := rpr / Rop; Turtle.CreateFigure; while angle <= 2 * nep * PI + 0.02 do begin var x := x0 + (Rop - rpr) * Cos(m * angle) + h * Cos(angle - m * angle); var y := y0 + (Rop - rpr) * Sin(m * angle) – h * Sin(angle - m * angle); // первая точка: if angle = 0 then begin Turtle.X := x; Turtle.Y := y; angle += dAngle; continue; end; // чертим отрезок: Turtle.MoveTo(x, y); angle += dAngle; end; if fill then Turtle.FillFigure(); // чертим опорную окружность: gw.PenColor := 'Gray'; gw.DrawEllipse(CX-rop + dcx,CY-rop + dcy,2*rop, 2*rop); end; 270
Рис. 4. Изящная эпитрохоида 271
Рис. 5. Эпитрохоида в шашечку 272
Рис. 6. Красота – страшная сила! 273
Рис. 7. Гипотрохоиды тоже красивы Эта трохоида просто гипер-гипотрохоидна (Рис. 8): 274
dcx := 20; dcy := 20; x0 := CX + dcx; y0 := CY + dcy; // радиус опорной окружности: var rop := 125*2; // радиус производящей окружности: var rpr := 85*2; // смещение от центра: var h := 125*2; 275
Рис. 8. Королева красоты 276
Спирограф Dum spiro, spero. Пока дышу, надеюсь. Латинская пословица Задолго до того как компьютеры осилили графику, люди уже научились выделывать на бумаге изрядные кренделя. Причём с помощью детской игрушки (Рис. 1), которую её изобретатель британский инженер Дэнис Фишер (Denys Fisher, 1918– 2002) назвал спирографом и в 1965 году показал на Нюрнбергской международной выставке игрушек. Рис. 1. Первый спирограф Не удивляйтесь, если в больнице вас заставят в спирограф дуть. Дело в том, что в мире есть и совсем не игрушечные спирографы. Они нужны для того, чтобы измерять объём легких и частоту дыхания (Рис. 2, слева). Прибор, который только измеряет, но не записывает показания, называется спирометром (Рис. 2, справа). О значении слова спиро в названии дыхательного аппарата легко догадаться по эпиграфу, а вот что оно значит в игрушечном спирографе, достоверно не известно. Скорее всего, спираль, хотя спирограф вычерчивает не спирали, а гипотрохоиды. 277
Рис. 2. Спирограф и спирометр Ещё один Спирограф можно обнаружить на небе – это туманность в созвездии Зайца (Рис. 3). Рис. 3. Планетарная туманность Спирограф 278
Как видите, в набор входила пластина с отверстиями разного диаметра и несколько кругов, также отличающихся по диаметру. Внутри кругов проделаны дырочки для карандаша. Легко понять, что при обкатывании маленького круга внутри большого карандаш будет вычерчивать гипотрохоиды. Круги, чтобы избежать проскальзывания при качении, снабжены небольшими зубчиками, которые на рисунке видны плохо. Игрушка пользовалась большим успехом и четыре года подряд признавалась лучшей обучающей игрушкой мира, поэтому неудивительно, что спирографы выпускаются и до сих пор, и некоторые из них стали значительно изощрённее предка (Рис. 4). Рис. 4. Современные спирографы Побочные эпитрохоиды Игры с параметрами трохоид приведут нас не только к красивым узорам, но и к неожиданным «открытиям». Оказывается, улитка Паскаля – это всего лишь весьма примитивная эпитрохоида, которую легко получить, если задать радиусы обеих окружностей одинаковыми: rop = rpr (Рис. 1-2): n := 1500; 279
dAngle := DegToRad(1080 / n); dcx := 80; dcy := 0; x0 := CX + dcx; y0 := CY + dcy; // радиус опорной окружности: var rop := 95; // радиус производящей окружности: var rpr := 95; // смещение от центра: var h := 165; dcx := 40; dcy := 0; x0 := CX + dcx; y0 := CY + dcy; // радиус опорной окружности: rop := 40; // радиус производящей окружности: rpr := 40; // смещение от центра: h := 250; А портрет «донны Розы Дальвадорец» а ля Пикассо столь же просто нарисовать при h = rop + rpr (Рис. 3). При этом число лепестков определяется отношением радиусов окружностей rop/rpr. n := 1000; dAngle := DegToRad(1080 / n); dcx := 5; dcy := 5; x0 := CX + dcx; y0 := CY + dcy; // радиус опорной окружности: var rop := 150; // радиус производящей окружности: var rpr := 10; // смещение от центра: var h := 160; 280
Рис. 1. Эпитрохоидная улитка Паскаля 281
Рис. 2. Ещё одна улитка Паскаля 282
Рис. 3. Эпитрохоидная розочка 283
С помощью нашей программы вы вряд ли найдёте особенную кривую, которую назовут вашим именем, но вот узоры вы можете нарисовать презанятные! По крайней мере, классическому спирографу очень далеко до нашей программы! Немыслимые мысленные эксперименты Очевидное? – Невероятное! Профессор Капица Умная мысля приходит опосля. Народная мудрость Возьмите две одинаковые монеты (если у вас таковых не оказалось, то представьте их мысленно) и поместите их одну над другой на некотором расстоянии, как показано на Рис. 1. А теперь уж точно мысленно соедините центры монет отрезком, который мы назовём водилом. Верхнюю монету жёстко прикрепим к водилу, а вокруг нижней монеты водило может свободно вращаться. Повернём водило на 180 градусов (то есть на половину оборота). Вопрос: какое положение займёт нижняя монета, оказавшись внизу? Рис. 1. Начинаем мысленный эксперимент 284
Поскольку эксперимент мысленный, то сначала завершите его, и только потом загляните в ответ (Рис. 2). Рис. 2. Готово! Я думаю, вы не ошиблись в своих предположениях. Результат вполне ожидаемый: верхняя монета повернулась на пол-оборота и остановилась вверх ногами. Усложним эксперимент. Прижмите монеты друг к другу так, чтобы они касались друг друга (Рис. 3). Позволим верхней монете вращаться вокруг собственной оси. Теперь катите верхнюю монету по нижней без проскальзывания, опять же на пол-оборота. А вопрос тот же: какое положение займёт верхняя монета? 285
Рис. 3. Эксперимент номер два Ответ уже не столь очевиден, как в первом случае, ведь верхняя монета будет дополнительно вращаться вокруг своего собственного центра. На время прервём мысленный эксперимент и воспользуемся нашей программой для рисования трохоид. При rop = rpr мы получим вот такую эпитрохоиду (Рис. 4). Рис. 4. Вспомогательная эпитрохоида 286
Кривая повёрнута так, чтобы соответствовать нашим предыдущим рисункам. Легко видеть, что нижняя точка верхней монеты снова окажется внизу, когда верхняя монета совершит пол-оборота (Рис. 5). Рис. 5. Неожиданный результат? Таким образом, верхняя монета повернётся на полный оборот! Довольно странно, если учесть, что точка касания монет прошла только половину окружности. Для прояснения ситуации уменьшим радиус верхней монеты вдвое, тогда она обернётся при повороте на 180 градусов на полтора оборота (Рис. 6). При этом 287
вокруг собственного центра она повернётся на полный оборот. Становится понятно, что пол-оборота добавляет вращение самого центра верхней монеты относительно центра монеты нижней. Рис. 6. Эпитрохоида при rop = 2rpr и полукругосветное путешествие монеты Если мы изловчимся и заменим монеты шестернями, то изобретём планетарную передачу (Рис. 7). Рис. 7. Планетарная передача 288
Здесь в центре «композиции» находится солнечная шестерня, а вокруг неё вращаются планетарные шестерни, числом от двух до четырёх. Планетарные шестерни закреплены на водиле. Нередко планетарные шестерни обкатываются и по внешней кольцевой (коронной) шестерне, которую также называют эпициклом. Ну а если размахнуться до вселенских масштабов, то недолго создать и Солнечную систему (Рис. 8). Рис. 8. Гелиоцентрическая система мира Как вы помните, впервые заставил Землю и другие планеты вращаться вокруг Солнца польский астроном Николай Коперник (Рис. 9). 289
Рис. 9. Mikolaj Kopernik (Nicolaus Copernicus) (1473-1543) Звёздный конструктор Ведь, если звёзды зажигают значит - это кому-нибудь нужно? Значит - кто-то хочет, чтобы они были? Владимир Маяковский. Послушайте! Давайте напишем программу, которое начертит нам красивые звёзды. Число лучей у звезды определяется значением параметра numPoint. Значения параметров innerRadius и outerRadius задают размер лучей – внутренний и внешний. Каждый луч состоит из двух отрезков. Чтобы провести отрезок, Черепашка должна знать координаты 290
двух точек – внутренней и внешней. Поэтому в каждом цикле рисования мы вычисляем координаты этих двух точек. Формулы для вычисления координат очень простые: procedure Star(numPoint: integer; innerRadius, outerRadius: double; fill: boolean := false); begin // скорость Черепашки: Turtle.Speed := 100; // толщина линий: if fill then gw.PenWidth := 0 else gw.PenWidth := 1.8; gw.PenColor := 'DarkRed'; // цвет заливки фигур: gw.BrushColor := 'Red'; dcx := 0.0; dcy := 30.0; x0 := CX + dcx; y0 := CY + dcy; Turtle.CreateFigure; for var i := 0 to numPoint do begin var angle := i * 2.0 * PI / numPoint; var x := x0 + outerRadius * Sin(angle); var y := y0 - outerRadius * Cos(angle); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); angle := (i + 0.5) * 2.0 * PI / numPoint; x := x0 + innerRadius * Sin(angle); y := y0 - innerRadius * Cos(angle); // чертим отрезок: Turtle.MoveTo(x, y); end; if fill then Turtle.FillFigure(); end; 291
Если значение параметра innerRadius положительное, то получаются привычные звёзды (Рис. 1-10). Рис. 1. Красная звезда 292
begin Prepare; Star(5, 120, 320, true); Star(5, 120, 320); end. gw.PenColor := 'SteelBlue'; // цвет заливки фигур: gw.BrushColor := 'SkyBlue'; dcx := 0.0; dcy := 5.0; x0 := CX + dcx; y0 := CY + dcy; Star(17, 120, 320, true); Star(17, 120, 320); Интересности начинаются, когда параметр innerRadius получает отрицательные значения (Рис. 3): gw.PenColor := 'DarkGreen'; // цвет заливки фигур: gw.BrushColor := 'LightGreen'; var var var var dcx := 0.0; dcy := 35.0; x0 := CX + dcx; y0 := CY + dcy; Star(5, -120, 320, true); Star(5, -120, 320); Изменяя число лучей, внутренний радиус и цвета контура и заливки, вы сможете получить очень красивые звёзды (Рис. 4-6): gw.PenColor := 'Blue'; // цвет заливки фигур: gw.BrushColor := 'DodgerBlue'; 293
dcx := 0.0; dcy := 20.0; x0 := CX + dcx; y0 := CY + dcy; Star(7, -120, 320, true); Star(7, -120, 320); gw.PenColor := 'DarkOrange'; // цвет заливки фигур: gw.BrushColor := 'Khaki'; dcx := 0.0; dcy := 5.0; x0 := CX + dcx; y0 := CY + dcy; Star(15, -80, 320, true); Star(15, -80, 320); gw.PenColor := 'Indigo'; // цвет заливки фигур: gw.BrushColor := 'Violet'; dcx := 0.0; dcy := 5.0; x0 := CX + dcx; y0 := CY + dcy; Star(12, -80, 320, true); Star(12, -80, 320); gw.PenColor := 'Green'; // цвет заливки фигур: gw.BrushColor := 'Lime'; dcx := 0.0; dcy := 5.0; x0 := CX + dcx; y0 := CY + dcy; 294
Star(16, -250, 320, true); Star(16, -250, 320); gw.PenColor := 'Teal'; // цвет заливки фигур: gw.BrushColor := 'LightSeaGreen'; dcx := 0.0; dcy := 0.0; x0 := CX + dcx; y0 := CY + dcy; Star(6, -320, 320, true); Star(6, -320, 320); gw.PenColor := 'DarkRed'; // цвет заливки фигур: gw.BrushColor := 'LightSalmon'; dcx := 0.0; dcy := 0.0; x0 := CX + dcx; y0 := CY + dcy; Star(12, -320, 320, true); Star(12, -320, 320); gw.PenColor := 'Sienna'; // цвет заливки фигур: gw.BrushColor := 'Wheat'; dcx := 0.0; dcy := 0.0; x0 := CX + dcx; y0 := CY + dcy; Star(36, -320, 320, true); Star(36, -320, 320); 295
Рис. 2. Многолучевая звезда 296
Рис. 3. Отрицательная звезда 297
Рис. 4. Синяя звезда 298
Рис. 5. Жёлтая звезда 299
Рис. 6. Фиолетовая звезда 300
Рис. 7. Зелёная звезда 301
Рис. 8. Сине-зелёная звезда 302
Рис. 9. Оранжево-розовая звезда 303
Рис. 10. Желтоватая звезда 304
Разноцветные окружности Мы уже обучили Черепашку рисовать окружности. Но одноцветные окружности не очень интересны, поэтому теперь Черепашка будет вычерчивать разноцветные окружности. Способ рисования очень простой: мы вычерчиваем каждую сторону многоугольника толстым цветным карандашом случайного цвета: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 640; GW_HEIGHT = 640; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Разноцветные окружности'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; // ЧЕРТИМ ОКРУЖНОСТЬ procedure DrawCircle(); 305
begin var var var var dcx := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; // число сторон многоугольника: var n := 120; // радиус описанной окружности: var r := CX - 20; for var j := 0 to 19-1 do begin // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // координаты начальной и конечной точек сторон: var startX := 0.0; var startY := 0.0; var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 16.0 * r / (GW_WIDTH / 4); // 8 // чертим окружность: for var i := 0 to n+1 do begin x := xc + r * Cos(DegToRad(alpha)); y := yc - r * Sin(DegToRad(alpha)); gw.PenColor := gw.GetRandomColor(); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; startX := x; startY := y; end; // уменьшаем радиус описанной окружности: var dr := integer(gw.PenWidth) + 2; 306
r -= dr; end; end; begin Prepare; DrawCircle(); end. В итоге наших усилий Черепашка нарисовала замечательные концентрические и в то же время эксцентрически пёстрые окружности (Рис. 1-2)! Толстый карандаш (или перо) несколько грубоват для тонкой, изящной работы, поэтому наши окружности напоминают плохо сшитые лоскутки в стиле пэчворк. Чтобы избавиться от излишней пестроты, мы научим Черепашку выбирать градиентные цвета с помощью функции GetGradientColor: // ВЫЧИСЛЯЕМ ЦВЕТ ПО ЕГО НОМЕРУ В ГРАДИЕНТЕ function GetGradientColor(id: double) : string; begin var r, g, b : integer; var n1 := integer(id / 256) mod 3; var n2 := integer(id) mod 256; if (n1 = 0) then begin b := 255; r := n2; g := integer(255 - n2); end else if (n1 = 1) then begin r := 255; g := n2; b := integer(255 - n2); end else begin g := 255; b := n2; r := integer(255 - n2); 307
end; Result := gw.GetColorFromRGB(r, g, b); end; procedure DrawCircle2(); begin var dcx := 0.0; var dcy := 0.0; var xc := CX + dcx; var yc := CY + dcy; // число сторон многоугольника: var n := 360; // радиус описанной окружности: var r := CX - 20; for var j := 0 to 11 do begin var rc := 18.0;// - j / 1.8; var id := 0.0; // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // координаты начальной и конечной точек сторон: var startX := 0.0; var startY := 0.0; var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 8.0 * r / (GW_WIDTH / 4); // чертим окружность: for var i := 0 to n+1 do begin x := xc + r * Cos(DegToRad(alpha)); y := yc - r * Sin(DegToRad(alpha)); gw.PenColor := GetGradientColor(id); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else 308
// чертим отрезок: Turtle.MoveTo(x, y); // новый цвет: //id += 768.0/rc; //психоделические цвета id += 768.0/n; alpha += da; startX := x; startY := y; end; // уменьшаем радиус описанной окружности: var dr := integer(gw.PenWidth) + -2; r -= dr; end; end; Теперь Черепашка выдаёт нам плавно окрашенные градиентные окружности (Рис. 3). Если ускорить изменение цветов вдоль окружности id += 768 * 2.0 / n; то картинка станет и того краше (Рис. 4). Если вас всё ещё тянет к прекрасному, попробуйте поэкспериментировать с приращением цвета. Например, при значении // новый цвет: id += 768 / rc; //психоделические цвета окружности вызывают рябь и дрожь в глазах (Рис. 5). Такие картины были бы хороши и полезны в общественных местах, например, в барах и ресторанах. 309
Рис. 1. Пёстро 310
Рис. 2. Ещё пестрее 311
Рис. 3. Градиентные окружности 312
Рис. 4. Цветная акселерация 313
Рис. 5. Цветная круговерть 314
Рис. 6. Расцветка Вырви глаз Не ленитесь экспериментировать – и вас получатся новые замечательные картинки (Рис. 7)! 315
id += 768 / n + 384; Рис. 7. Лучезарные окружности 316
Параметрические кривые Кривая линия может быть задана параметрическими уравнениями. Для плоской кривой понадобятся 2 уравнения – для каждой из координат. Координаты – это функции от некоторой переменной t. В этом проекте мы рассмотрим параметрические кривые, которые задаются такими уравнениями: var x = Cos(A * t) + Cos(B * t) / 2 + Sin(C * t) / 3; var y = Sin(A * t) + Sin(B * t) / 2 + Cos(C * t) / 3; Параметр t изменяется в диапазоне 0..2π, а коэффициенты А, В и С – это произвольные целые числа. Эти уравнения вы можете найти в книге Фрэнка Фарриса (Frank A. Farris) Creating Symmetry: The Artful Mathematics of Wallpaper Patterns (Рис. 1). Рис. 1. Книга о симметрии 317
В ней параметры равны: А = 1 В = 6 С = 14 Им соответствует кривая на Рис. 2. Рис. 2. Образец для подражания 318
Но нам, конечно, хотелось бы посмотреть и на другие кривые – может быть, среди них найдутся ещё более интересные! В этом проекте мы напишем 2 процедуры: begin Prepare; DrawCurve(); //DrawColorCurve(); end. Процедура DrawCurve чертит кривые с цветной заливкой, но с одноцветным контуром. Процедура DrawColorCurve чертит кривые без заливки, но с разноцветным контуром. К сожалению, совместить разноцветный контур с цветной заливкой невозможно. Однако вы можете сначала сделать заливку, а затем наложить на неё цветной контур, как мы это делали в предыдущих проектах. В этом проекте мы применим универсальный метод построения кривых по точкам из предварительно составленного списка. Он позволит нам вычертить линию и сразу же закрасить её. Поскольку Черепашка двигается медленно, то этот метод сократит время построения кривой в 2 раза. В начале программы я собрал коэффициенты для построения кривых, которые вы увидите дальше: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 800; GW_HEIGHT = 800; 319
CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // коэффициенты: var A := 1; B := 6; C := 14; //A := 10; B := 21; C := 11; //A := 10; B := 19; C := 11; //A := 10; B := 39; C := 20; //A := -1; B := 39; C := 40; //A := -1; B := 38; C := 40; //A := 9; B := 38; C := 40; //A := -29; B := 38; C := 40; //A := 19; B := 43; C := 19; //A //A //A //A //A := := := := := 1; B := 15; C := 14; 15; B := 29; C := 14; 1; B := 6; C := 308; 1; B := -110; C := -113; 1; B := -53; C := -112; Вы просто раскомментируйте нужную строку и вызовите нужную процедуру. Координаты точек удобно хранить в экземплярах примитивного класса Point: // КЛАСС КООРДИНАТ ТОЧЕК type Point = auto class x, y : double; end; А все точки мы поместим в список lstPoints, откуда их легко извлекать: // список точек кривой: var lstPoints := new List<Point>(); Для этого проекта я выбрал тёмно-зелёный цвет фона: procedure Prepare(); begin 320
gw.Hide(); gw.Title := 'Параметрические кривые'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; end; В функции CreateCurve мы вычисляем координаты всех точек кривой и отправляем их в список lstPoints на хранение: // СОЗДАЁМ КРИВУЮ function CreateCurve() : double; begin lstPoints.Clear; // коэффициент увеличения: var k := 219; // шаг: var maxk := Max(Max(A, B), C); Чем длиннее кривая, тем больше нужно точек, чтобы она получилась гладкой. Число точек зависит от размера шага, поэтому подбирайте его соответственно вычерчиваемой кривой: //var step := Min(1.0 / maxk / 8.0, 0.005); var step := Min(1.0 / maxk / 8.0, 0.0025); var dcx := 0.0; var dcy := 10.0; var x0 := CX + dcx; var y0 := CY + dcy; var t := 0.0; while t <= PI * 2 do begin var x := Cos(A * t) + Cos(B * t) / 2 + Sin(C * t) / 3; var y := Sin(A * t) + Sin(B * t) / 2 + Cos(C * t) / 3; x := x0 + x * k; y := y0 + y * k; 321
lstPoints.Add(new Point(x, y)); t += step; end; Координаты точек, вычисляемые по формулам, настолько малы, что их необходимо домножить на некий коэффициент k, чтобы кривая заняла достойное место на экране. Коэффициент maxk используется и в следующих процедурах. Чтобы не вычислять его дважды, функция CreateCurve возвращает его в готовом виде: Result := maxk; end; Процедура DrawCurve, кроме коэффициента maxk, получает и заполненный координатами точек кривой список lstPoints. Перед вычерчиванием кривой процедура задаёт цвет контура и заливки, а также толщину линий: // ЧЕРТИМ ОДНОЦВЕТНУЮ КРИВУю С ЗАЛИВКОЙ procedure DrawCurve(fill: boolean:= true); begin var maxk := CreateCurve(); // цвет линий: gw.PenColor := 'Yellow'; // цвет заливки фигур: gw.BrushColor := 'LimeGreen'; // толщина пера: gw.PenWidth := 2; // толстая линия: if (maxk > 40) then gw.PenWidth := 2.5; Перед вызовом процедуры Turtle.CreateFigure нужно сразу установить Черепашку в последнюю точку кривой, иначе она начертит лишнюю прямую: // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; 322
Turtle.CreateFigure; А дальше в цикле foreach мы последовательно извлекаем из списка lstPoints координаты точек кривой и отсылаем туда Черепашку с пёрышком, и она чертит отрезок кривой линии: foreach var pt in lstPoints do begin var x := pt.x; var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; По умолчанию все фигуры закрашиваются: if fill then Turtle.FillFigure(); end; Но вы можете оставить только контур, если зададите параметру fill значение false. Чертим первую кривую по коэффициентам из книги: A := 1; B := 6; C := 14; Черепашка отлично справилась с первым заданием (Рис. 3). Дальше книга нам не помощник, поэтому методом тыка подбираем значения коэффициентов так, чтобы Черепашке не было стыдно за нас и за свою работу (Рис. 4). Дело это кропотливое и хлопотное, но оно того стоит (Рис. 5). Форма кривой зависит от коэффициентов, но сразу трудно представить себе, куда они загнут кривую (Рис. 6). 323
Рис. 3. Следуем положительным примерам 324
Рис. 4. A = 10; B = 21; C = 11; 325
Рис. 5. A = 10; B = 19; C = 11; 326
Рис. 6. A = 10; B = 39; C = 20; Иногда получаются красивые симметричные кривые (Рис. 7). 327
Рис. 7. A = -1; B = 39; C = 40; Некоторые кривые могут даже удивить своей неожиданной формой (Рис. 8). 328
Рис. 8. A = -1; B = 38; C = 40; 329
В центре фигур могут самопроизвольно возникать дополнительные кривые (Рис. 9). Рис. 9. A = 9; B = 38; C = 40; 330
Лишняя суета Черепашки может и навредить искусству (Рис. 10). Рис. 10. A = -29; B = 38; C = 40; 331
Завершает нашу черепашью выставку параметрических кривых изрядный огурец (Рис. 11). А Черепашка – молодец! Рис. 11. A := 19; B := 43; C := 19; 332
Мы умеем вычислять цвет отрезков по их индексам. Для этого у нас есть готовая функция GetGradientColor: // ВЫЧИСЛЯЕМ ЦВЕТ ПО ЕГО НОМЕРУ В ГРАДИЕНТЕ function GetGradientColor(id: double) : string; begin var r, g, b : integer; var n1 := integer(id / 256) mod 3; var n2 := integer(id) mod 256; if (n1 = 0) then begin b := 255; r := n2; g := integer(255 - n2); end else if (n1 = 1) then begin r := 255; g := n2; b := integer(255 - n2); end else begin g := 255; b := n2; r := integer(255 - n2); end; Result := gw.GetColorFromRGB(r, g, b); end; Она несовершенная, но простая и быстрая. Процедура DrawColorCurve чертит кривую по точкам из списка lstPoints, но для каждого отрезка кривой получает цвет от функции GetGradientColor по индексу этого отрезка: // ЧЕРТИМ РАЗНОЦВЕТНУЮ КРИВУЮ procedure DrawColorCurve; begin gw.BackgroundColor := 'Black'; CreateCurve(); 333
// толщина пера: gw.PenWidth := 2.5; var id := 0.0; var n := lstPoints.Count;//360; Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; foreach var pt in lstPoints do begin var x := pt.x; var y := pt.y; gw.PenColor := GetGradientColor(id); // чертим отрезок: Turtle.MoveTo(x, y); id += 768.0/n; end; end; Как вы знаете, индекс можно вычислять по-разному, так что дерзайте! Запускаем программу, и «книжная» кривая стала разноцветной (Рис. 12). Цветные линии лучше смотрят на чёрном фоне (Рис. 13). Отсутствие заливки позволяет лучше разглядеть причудливые изгибы параметрических кривых (Рис. 14). На построение этой крутозаверченной кривой потребуется немало времени, так что запаситесь напитками, вкусняшками и терпением (Рис. 15). А эта кривая слегка пострадала в верхних крутых изгибах, но мне очень хотелось быстрее увидеть её во всей красе (Рис. 16). Даже простые математические формулы могут порождать художественные творения (Рис. 17), недоступные многим современным аляповатым мастерам. 334
Рис. 12. Цветная книжная кривая 335
Рис. 13. A := 1; B := 15; C := 14; 336
Рис. 14. A := 15; B := 29; C := 14; 337
Рис. 15. A := 1; B := 6; C := 308; 338
Рис. 16. A := 1; B := -110; C := -113; 339
Рис. 17. A := 1; B := -53; C := -112; 340
Разноцветные диагонали Представьте, что у вас есть правильный многоугольник, в котором вы должны провести все возможные диагонали. Вы легко справитесь с задачей для многоугольников с небольшим числом вершин (Рис. 1). Рис. 1. Правильные многоугольники с диагоналями     У треугольника вообще нет диагоналей. У квадрата – 2 диагонали. У пятиугольника – 5 диагоналей. У шестиугольника – 9 диагоналей. Дальше считать диагонали и чертить их вручную уже затруднительно, поэтому мы поручим это дело компьютеру, но нам нужно сначала объяснить ему, как это делается, то есть написать программу. Как мы только что установили, начинать нужно с квадратов, так как в треугольнике диагоналей мы провести не сможем. А закончим мы 30-угольником, иначе все линии просто сольются на экране всмятку и огорчат нас до невозможности, как сказал Глеб Жеглов несознательному гражданину Ручечнику. Удобнее всего строить правильный многоугольник по координатам его центра (xc, yc) и радиусу описанной окружности r (Рис. 2). 341
Рис. 2. Геометрия – это наука! Отсчёт угла alpha начинается от положительного направление оси X против часовой стрелки. Я недаром повернул все многоугольники на Рис. 1⬆ так, чтобы самая первая вершина лежала на оси Х, - так нам удобнее вычислять координаты вершин. Если же вы захотите повернуть многоугольник, то всякий раз добавляйте нужное значение к текущему углу alpha. Приращение угла при переходе к следующей вершине легко вычислить, если учесть, что окружность нужно разбить на n частей n точками. В полной окружности 360 градусов, значит, на каждую дугу окружности придётся 360/n градусов: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; 342
type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Разноцветные диагонали'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; // ЧЕРТИМ МНОГОУГОЛЬНИК procedure DrawPoly(n: integer); begin // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: var r := GW_WIDTH / 2 - 10; // координаты точек: var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 1.8; gw.PenColor := 'Black'; 343
var var var var dcx := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; Координаты вершин многоугольника легко вычислить по тригонометрическим формулам, но значение угла в градусах следует пересчитать в радианы, как того требуют функции Sin и Cos: // чертим многоугольник: for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc + r * Sin(DegToRad(alpha)); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); При переходе к следующей вершине увеличиваем угол alpha: alpha += da; end; end; Запускаем программу – и правильный многоугольник вычерчен на славу (Рис. 3): begin Prepare; DrawPoly(12); end. 344
Рис. 3. Чертим правильно правильные многоугольники Но – диагоналей ему не хватает, поэтому берёмся за разработку следующего метода – Draw. 345
Пронумеруем вершины многоугольника от 0 до (n-1) (Рис. 4) и соединим нулевую вершину со всеми остальными. Рис. 4. Все диагонали нулевой вершины Так мы получим и две стороны многоугольника, и его диагонали. Легко подсчитать, что для n-угольника потребуется провести (n-1) линию. Для нашего 8-угольника – 7 линий. Переходим к точке 1. Здесь всё то же самое, но линия к предыдущей вершине уже проведена, поэтому достаточно соединять текущую вершину со всеми последующими, но не с предыдущими (Рис. 5). Из точки 1 мы проведём 6 линий. Действуя аналогично, получим (Рис. 6):     Для точки 2 – 5 линий Для точки 3 – 4 линии Для точки 4 – 3 линии Для точки 5 – 2 линии 346
  Для точки 6 – 1 линию Для точки 7 – 0 линий Рис. 5. Диагонали первой вершины Итак, чтобы вычертить все стороны и углы, нужно соединить каждую вершину 0..n-2 со всеми последующими. Чтобы не вычислять всякий раз координаты вершин, лучше завести два вещественных массива coordsX и coordsY для их координат. Как их вычислить, вы уже знаете: // ЧЕРТИМ ДИАГОНАЛИ // со сторонами procedure Draw(n: integer); begin var alpha := 0.0; var da := 360.0/n; var r := GW_WIDTH/2 - 10; var dcx := 0.0; var dcy := 0.0; var xc := CX + dcx; 347
var yc := CY + dcy; // координаты вершин: var coordsX := new double[n]; var coordsY := new double[n]; // вычисляем координаты вершин многоугольника: for var i := 0 to n-1 do begin coordsX[i] := xc + r * Cos(DegToRad(alpha)); coordsY[i] := yc + r * Sin(DegToRad(alpha)); alpha += da; end; Рис. 6. Все стороны и диагонали 8-угольника 348
Теперь провести все необходимые линии нам не составит труда: // чертим диагонали: if (n < 16) then gw.PenWidth := 2 else gw.PenWidth := 1.5; Turtle.Speed := 20; for var j := 0 to n-1-1 do begin var x := coordsX[j]; var y := coordsY[j]; for var i := j + 1 to n-1 do begin gw.PenColor := gw.GetRandomColor(); Turtle.X := x; Turtle.Y := y; Turtle.MoveTo(coordsX[i], coordsY[i]); end; end; end; Чтобы линии, проведённые из разных вершин, отличались друг от друга, мы окрасим их в разные цвета случайным образом (Рис. 7). Более насыщенные узоры мы получим для 20- (Рис. 8) и 30-угольников (Рис. 9). Давайте для интереса подсчитаем число линий в наших многоугольниках. Если у многоугольника n вершин, то число линий равно сумме членов арифметической прогрессии: (n-1), (n-2), . . ., (3), (2), (1) = (n-1) + 1 --------- * (n-1) 2 Или: 349
n * (n - 1) ----------2 Однако вернёмся в начало проекта, где мы собирались провести только диагонали. Таким образом, мы должны исключить из списка линий все стороны многоугольника. Число диагоналей легко подсчитать, если из общего числа линий вычесть стороны многоугольника, которых n штук. Тогда на долю диагоналей придётся: n * (n - 3) ----------2 Снова начертим все диагонали из вершины 0, но на этот раз стороны не проводим (Рис. 10). Вы видите, что теперь соседняя по ходу вершина имеет номер не на единицу, а на двойку больше текущей, а заканчивать нужно также не на предпоследней вершине, а на предпредпоследней, так как к этому времени все диагонали уже будут проведены (Рис. 11). 350
Рис. 7. Правильный 12-угольник со всеми диагоналями 351
Рис. 8. Правильный 20-угольник со всеми диагоналями 352
Рис. 9. Работаем на максимуме 353
Рис. 10. Только диагонали! Рис. 11. Все диагонали 8-угольника // без сторон procedure Draw2(n: integer); begin var alpha := 0.0; 354
var da := 360.0/n; var r := GW_WIDTH/2 - 10; var var var var dcx := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; // координаты вершин: var coordsX := new double[n]; var coordsY := new double[n]; // вычисляем координаты вершин многоугольника: for var i := 0 to n-1 do begin coordsX[i] := xc + r * Cos(DegToRad(alpha)); coordsY[i] := yc + r * Sin(DegToRad(alpha)); alpha += da; end; // чертим стороны и диагонали: if (n < 16) then gw.PenWidth := 2 else gw.PenWidth := 1.5; Turtle.Speed := 20; for var j := 0 to n-1-1 do begin var x := coordsX[j]; var y := coordsY[j]; for var i := j + 2 to n-1 do begin gw.PenColor := gw.GetRandomColor(); И нам нужно выполнить ещё одно условие, чтобы избежать соединения первой и последней вершин многоугольника: // не чертим сторону между первой и последней вершинами: if (i - j = n - 1) then continue; Turtle.X := x; Turtle.Y := y; 355
Turtle.MoveTo(coordsX[i], coordsY[i]); end; end; end; На многоугольниках с большим числом сторон их отсутствие не очень заметно, поэтому для иллюстрации последнего метода мы ограничимся 8-угольником (Рис. 12): Draw2(8); 356
Рис. 12. Все диагонали 8-угольника – компьютерный вариант 357
Одноцветные диагонали В некоторых случаях лучше не гнаться за пестротой линий, а вычертить их одним цветом. В новом, облегчённом проекте мы этим и займёмся, а заодно сообщим в заголовке окна число диагоналей в заданном многоугольнике. Вызываем процедуру Draw, которая на этот раз имеет три параметра:    n – число сторон правильного многоугольника color – цвет линий thickness – толщина линий (по умолчанию равна 2) begin Prepare; Draw(25, 'Blue', 1); end. В самой процедуре Draw нужно изменить и добавить всего несколько строк: // ЧЕРТИМ МНОГОУГОЛЬНИК С ДИАГОНАЛЯМИ procedure Draw(n: integer; color : string; tickness: integer := 2); begin var alpha := 0.0; var da := 360.0/n; var r := GW_WIDTH/2 - 10; var var var var dcx := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; // координаты вершин: var coordsX := new double[n]; var coordsY := new double[n]; 358
// вычисляем координаты вершин многоугольника: for var i := 0 to n-1 do begin coordsX[i] := xc + r * Cos(DegToRad(alpha)); coordsY[i] := yc + r * Sin(DegToRad(alpha)); alpha += da; end; // цвет линий: gw.PenColor := color; // толщина линий: gw.PenWidth := tickness; Turtle.Speed := 20; for var j := 0 to n-2 do begin var x := coordsX[j]; var y := coordsY[j]; for var i := j + 1 to n-1 do begin Turtle.X := x; Turtle.Y := y; Turtle.MoveTo(coordsX[i], coordsY[i]); end; end; gw.Title := 'Число диагоналей = ' + n * (n - 3) / 2; end; По-моему, кружева получились великолепно (Рис. 1)! 359
Рис. 1. Все ниточки посчитаны до ниточки! 360
Диагональные разрезы Более интересная и сложная задача – посчитать, на сколько частей диагонали разбивают многоугольник. В книге Росса Хонсбергера (Ross Honsberger) [HR74], страницы 99-107 вы найдёте 3 способа выведения формулы для подсчёта числа областей (регионов, частей), на которые диагонали разбивают выпуклые многоугольники (не обязательно правильные): К сожалению, эта формула не годится для правильных многоугольников с чётным числом вершин (кроме квадрата), потому что формула выведена в предположении, что никакие три диагонали не пересекаются в одной точке. На Рис. 1 легко видеть, что диагонали правильного 6-угольника нарушают это ограничение. В результате число областей в таких многоугольниках оказывается меньше, чем даёт формула. Например, диагонали правильного 6-угольника разбивают его на 24 части (посчитайте!), а согласно формуле их должно быть 25. Давайте учтём это ограничение в новом проекте, который будет считать не только диагонали, но и области, на которые они делят правильный многоугольник. Добавьте к предыдущему проекту функцию GetKoeff2 для подсчёта числа сочетаний: 361
Рис. 1. Многоугольник-нарушитель function GetKoeff2(n, k : integer) : decimal; begin var res : decimal := 1; 362
for var i := k - 1 to 0 step -1 do begin res := res * (n - i); res := res div (k - i); end; Result := res; end; И внесите коррективы в остальной код: // ЧЕРТИМ МНОГОУГОЛЬНИК С ДИАГОНАЛЯМИ procedure Draw(n: integer; color : string; tickness: real := 2); begin var alpha := 0.0; var da := 360.0/n; var r := GW_WIDTH/2 - 10; var var var var dcx := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; // координаты вершин: var coordsX := new double[n]; var coordsY := new double[n]; // вычисляем координаты вершин многоугольника: for var i := 0 to n-1 do begin coordsX[i] := xc + r * Cos(DegToRad(alpha)); coordsY[i] := yc + r * Sin(DegToRad(alpha)); alpha += da; end; // цвет линий: gw.PenColor := color; // толщина линий: gw.PenWidth := tickness; Turtle.Speed := 20; for var j := 0 to n-2 do begin var x := coordsX[j]; var y := coordsY[j]; for var i := j + 1 to n-1 do begin 363
Turtle.X := x; Turtle.Y := y; Turtle.MoveTo(coordsX[i], coordsY[i]); end; end; var s := 'Число диагоналей = ' + n * (n - 3) / 2; if (n = 4) or (n mod 2 <> 0) then s += ' Число кусков = ' + (GetKoeff2(n,4) + GetKoeff2(n-1,2)).ToString; gw.Title := s; end; begin Prepare; //Draw(6, 'Blue'); //Draw(4, 'Blue'); //Draw(5, 'Blue'); //Draw(7, 'DarkGreen'); //Draw(11, 'DarkGreen') Draw(9, 'Red') end. На этом наша титаническая работа заканчивается, и мы легко справляемся со статистическими расчётами (Рис. 2). Для уверенности в себе сравниваем результаты работы нашей программы с картинкой из книги. Рис. 3 убеждает нас, что программа считает правильно. 364
Рис. 2. Большому куску и программа радуется! 365
Рис. 3. Контрольный пример Диагональный круг Если в воображаемом круге провести много-много диагоналей, то мы получим почти настоящий круг с интересным муаром (Рис. 1): {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; 366
CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Диагональный круг'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; end; // ЧЕРТИМ ДИАГОНАЛИ procedure Draw(); begin // цвет линий: gw.PenColor := 'Lime'; // толщина линий: gw.PenWidth := 1.0; Turtle.Speed := 20; // радиус окружности: var r := GW_WIDTH/2 - 10; // угол: var a := 0.0; // шаг: var step := 2.0; var var var var dcx := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; // проводим линии: for var i := 0 to integer(180 / step)-1 do begin var rad := DegToRad(a); // начало линии: 367
var x1 := xc - r * Cos(rad); var y1 := yc - r * Sin(rad); Turtle.X := x1; Turtle.Y := y1; // конец линии: var x2 := xc + r * Cos(rad); var y2 := yc + r * Sin(rad); Turtle.MoveTo(x2, y2); a += step; end; end; begin Prepare; Draw(); end. 368
Рис. 1. Муар 369
Разноцветные многоугольники При известной сноровке и ловкости рук мы сможем вычертить градиентные многоугольники. Для этого достаточно внутри большего многоугольника рисовать меньший, и тогда они создадут плавный переход цвета от внешнего многоугольника к его центру. Взяв за основу проект Разноцветные диагонали и добавив всего несколько новых строк, мы научим нашу программу рисовать градиентные картинки (Рис. 1-4). {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Разноцветные многоугольники'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'SeaShell'; end; // ВЫЧИСЛЯЕМ ЦВЕТ ПО ЕГО НОМЕРУ В ГРАДИЕНТЕ 370
function GetGradientColor(id: double) : string; begin var r, g, b : integer; var n1 := integer(id / 256) mod 3; var n2 := integer(id) mod 256; if (n1 = 0) then begin b := 255; r := n2; g := integer(255 - n2); end else if (n1 = 1) then begin r := 255; g := n2; b := integer(255 - n2); end else begin g := 255; b := n2; r := integer(255 - n2); end; Result := gw.GetColorFromRGB(r, g, b); end; // ЧЕРТИМ МНОГОУГОЛЬНИКИ procedure DrawPoly(n: integer); begin // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: var r := GW_WIDTH / 2 - 10; // координаты точек: var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 3; Turtle.Speed := 100; var dcx := -90.0; // 3 371
dcx var var var := 0.0; // 4,8,60 dcy := 0.0; xc := CX + dcx; yc := CY + dcy; var id := 256.0; while (r > 0) do begin // чертим многоугольник: for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc + r * Sin(DegToRad(alpha)); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else begin var clr := GetGradientColor(id); gw.PenColor := clr; // чертим отрезок: Turtle.MoveTo(x, y); end; alpha += da; end; // уменьшаем радиус описанной окружности: r -= 1; // и изменяем цвет сторон многоугольника: id += 3.3;//32;//16; end; end; begin Prepare; DrawPoly(3); //DrawPoly(4); //DrawPoly(8); //DrawPoly(60); //DrawPoly(12); //DrawPoly(60); end. 372
Рис. 1. Градиентный треугольник 373
Рис. 2. Градиентный квадрат 374
Рис. 3. Градиентный восьмиугольник 375
Рис. 4. Градиентный круг 376
Полосатость многоугольников можно легко изменить, задавая разные приращения индексу цвета (Рис. 5-6). Рис. 5. id += 32 377
Рис. 6. id += 16 378
Вращающиеся многоугольники Если начальное значение угла alpha равно нулю: // текущий угол: var alpha := 0.0; то первая вершина многоугольника лежит на оси Х (это всегда самая правая вершина) (Рис. 1). Если мы зададим начальное значение -90 градусов // текущий угол: var alpha := -90.0; то весь многоугольник повернётся против часовой оси на этот угол, и первая вершина переместится на ось Y и займёт самую верхнюю позицию (Рис. 2). Таким образом, изменяя начальное значение угла alpha, мы можем поворачивать многоугольник вокруг точки (xc, yc). В новой программе мы используем проект Разноцветные многоугольники, но уменьшим максимальное число вершин у многоугольников до 10 и будем поворачивать их заданное число раз. {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; 379
GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Вращающиеся многоугольники'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; end; Основные изменения коснутся только процедуры рисования DrawPoly. Она получает ещё один параметр – число поворотов заданного многоугольника: // ЧЕРТИМ МНОГОУГОЛЬНИКИ procedure DrawPoly(n, nrot: integer); begin // текущий угол: var alpha := -90.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: var r := GW_WIDTH / 2 - 10; // координаты точек: var x := 0.0; var y := 0.0; Цвет линий не меняем, но вы можете выбрать его по своему вкусу. Точно так же, как и толщину линий: // толщина пера: gw.PenWidth := 2;//3; gw.PenColor := 'Lime'; 380
Turtle.Speed := 100; var dcx var var var dcx := -90.0; // 3 := 0.0; // 3 dcy := 0.0; xc := CX + dcx; yc := CY + dcy; for var j := 0 to nrot-1 do begin alpha := j * 360.0 / nrot; // чертим многоугольник: for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc + r * Sin(DegToRad(alpha)); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; end; end; begin Prepare; DrawPoly(3,2); //DrawPoly(4,3); //DrawPoly(5,21); //DrawPoly(3,28); end. Задавая различные значения параметрам этой процедуры, вы получите звёздчатые многоугольники (Рис. 3-6). 381
Рис. 1. Первая вершина лежит на оси Х 382
Рис. 2. Повернули треугольник 383
Рис. 3. DrawPoly(3,2) 384
Рис. 4. DrawPoly(4,3) 385
Рис. 5. DrawPoly(5,21) 386
Рис. 6. DrawPoly(3,28) 387
Цветные вращающиеся многоугольники А теперь давайте добавим колорита нашим одноцветным звёздам! Работы нам предстоит совсем немного, а результаты получатся любопытные (Рис. 1-5): {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Цветные вращающиеся многоугольники'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'Ivory'; end; // ВЫЧИСЛЯЕМ ЦВЕТ ПО ЕГО НОМЕРУ В ГРАДИЕНТЕ function GetGradientColor(id: double) : string; begin var r, g, b : integer; var n1 := integer(id / 256) mod 3; var n2 := integer(id) mod 256; if (n1 = 0) then begin b := 255; r := n2; 388
g := integer(255 - n2); end else if (n1 = 1) then begin r := 255; g := n2; b := integer(255 - n2); end else begin g := 255; b := n2; r := integer(255 - n2); end; Result := gw.GetColorFromRGB(r, g, b); end; // ЧЕРТИМ МНОГОУГОЛЬНИКИ procedure DrawPoly(n, nrot: integer); begin // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: var r := GW_WIDTH / 2 - 10; // координаты точек: var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 4; gw.PenColor := 'Lime'; Turtle.Speed := 100; var dcx := 0.0; dcx := 0.0; var dcy := 0.0; var xc := CX + dcx; var yc := CY + dcy; var id := 256.0; for var j := 0 to nrot-1 do begin alpha := j * 360.0 / nrot; // чертим многоугольник: 389
for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc - r * Sin(DegToRad(alpha)); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; var clr := GetGradientColor(id); // полупрозрачный цвет: //clr := '#A0' + string(Text.GetSubTextToEnd(clr, 2)); gw.PenColor := clr; // изменяем цвет сторон многоугольника: id += nrot; end; end; begin Prepare; // непрозрачные цвета: DrawPoly(3,111); //DrawPoly(4,99); //DrawPoly(3,331); //DrawPoly(4,332); //DrawPoly(3,720); // полупрозрачные цвета: //DrawPoly(3,90); //DrawPoly(4,99); //DrawPoly(3,720); end. Попробуйте поэкспериментировать с полупрозрачными цветами (Рис. 6-7). Для этого раскомментируйте строчку: //clr := '#A0' + string(Text.GetSubTextToEnd(clr, 2)); 390
Рис. 1. DrawPoly(3,111) 391
Рис. 2. DrawPoly(4,99) 392
Рис. 3. DrawPoly(3,331) 393
Рис. 4. DrawPoly(4,332) 394
Рис. 5. DrawPoly(3,720) 395
Рис. 6. DrawPoly(3,90) 396
Рис. 7. DrawPoly(4,99) 397
Зубчатые многоугольники Возьмём правильный многоугольник, сделаем его уменьшенную копию, которую повернём на половину центрального угла стороны многоугольника (Рис. 1, слева). Теперь соединим вершины этих многоугольников так, как показано на Рис. 1, справа. В результате мы получим зубчатый многоугольник. Рис. 1. Зубчатый многоугольник От нас потребуется только небольшая сноровка, чтобы написать новую программу: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; 398
CY = GW_HEIGHT div 2; // координаты вершин: var coordsX, coordsY : array of real; procedure Prepare(); begin gw.Hide(); gw.Title := 'Зубчатые многоугольники'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; end; procedure Draw(n: integer; color: string); begin // создаём массив координат вершин многоугольника: CreateCoords(n); // чертим многоугольник: DrawPoly(n, color); end; begin Prepare; // положительные: Draw(6,'Lime'); //Draw(40,'Lime'); //Draw(100,'Lime'); // отрицательные: //Draw(11,'Lime'); //Draw(32,'Lime'); end. 399
Рисование многоугольника проходит в 2 этапа. Сначала мы создаём массивы координат вершин, а затем соединяем вершины отрезками прямых: // СОЗДАЁМ МАССИВ КООРДИНАТ ВЕРШИН МНОГОУГОЛЬНИКА procedure CreateCoords(n: integer); begin var alpha := 0.0; var da := 360.0 / n; var r := GW_WIDTH / 2 - 10; //var rsmall := r * 0.5; var rsmall := -r * 0.5; var dcx var var var dcx := 0.0; := 0.0; dcy := 0.0; xc := CX + dcx; yc := CY + dcy; // координаты вершин: coordsX := new double[n coordsY := new double[n // вычисляем координаты for var i := 0 to n * 2 coordsX[i] := xc + r coordsY[i] := yc - r * 2]; * 2]; вершин многоугольника: - 1 step 2 do begin * Cos(DegToRad(alpha)); * Sin(DegToRad(alpha)); coordsX[i+1] := xc + rsmall * Cos(DegToRad(alpha + da / 2)); coordsY[i+1] := yc - rsmall * Sin(DegToRad(alpha + da / 2)); alpha += da; end end; Поскольку нам нужны теперь координаты двух концентрических многоугольников, то число точек возрастает вдвое, а радиус описанной окружности меньшего многоугольника rsmall мы полагаем равным половине радиуса большего многоугольника r (вы можете выбрать другое значение). 400
Чтобы вычертить зубчатый многоугольник, последовательно соединяйте все вершины. При этом нужно не забудьте провести линию между первой и последней вершинами, чтобы получилась замкнутая ломаная линия: // ЧЕРТИМ МНОГОУГОЛЬНИК procedure DrawPoly(n: integer; color: string); begin // цвет линий: gw.PenColor := color; // толщина линий: gw.PenWidth := 2; gw.BrushColor := 'ForestGreen'; // чертим многоугольник: Turtle.X := coordsX[0]; Turtle.Y := coordsY[0]; Turtle.CreateFigure; for var i := 0 to n * 2 - 2 do begin Turtle.X := coordsX[i]; Turtle.Y := coordsY[i]; Turtle.MoveTo(coordsX[i + 1], coordsY[i + 1]); end; // замыкаем: Turtle.MoveTo(coordsX[0], coordsY[0]); Turtle.FillFigure; end; Пора пожинать плоды рук своих! Запускаем программу и пытаем её на разных зубчатых многоугольниках (Рис. 2-4). Задавая разные значения параметрам n и rsmall (радиус может быть и отрицательным!), вы получите очень интересные картинки (Рис. 5-6)! 401
Рис. 2. Draw(6,'Lime') 402
Рис. 3. Draw(40,'Lime') 403
Рис. 4. Draw(100,'Lime') 404
Рис. 5. Draw(11,'Lime') 405
Рис. 6. Draw(32,'Lime') 406
Звёздчатые многоугольники Если соединять вершины пятиугольника последовательно 0-1 1-2 2-3 3-4 4-0 то получится обычный правильный многоугольник (Рис. 1, слева). Можно соединить вершины через одну: 0-2 1-3 2-4 3-0 4-1 Тогда получится звёздчатый многоугольник (Рис. 1, справа). Форму правильного пятиугольника имеет всем известное здание Пентагона (что в переводе и значит пятиугольник) (Рис. 2). Пятиугольный звёздчатый многоугольник, или пентаграмма – самая известная фигура этого рода. Пентаграмма известна уже несколько тысячелетий и использовалась как оберегающий знак, знак власти или отличительный знак различных сообществ. Английский учёный Роджер Пенроуз предложил свой вариант пентаграммы. Её можно нарисовать, но невозможно воспроизвести «в натуре» (Рис. 3). 407
Рис. 1. Правильный и звёздчатый многоугольники Рис. 2. Пентагон с высоты птичьего полёта 408
Рис. 3. Невозможная пентаграмма «Пентаграммы» можно найти и в живой природе – это морские звёзды (Рис. 4). Рис. 4. Морская пентаграмма Шестиугольный звёздчатый многоугольник – гексаграмма – также очень известен. Его можно видеть, например, на флаге Израиля (Рис. 5). 409
Рис. 5. Флаг Израиля с гексаграммой И на фасаде базилики Санта-Кроче во Флоренции (Santa Croce exterior Firenze) (Рис. 6). Рис. 6. Церковная гексаграмма 410
Гексаграмму, как и пентаграмму Пенроуза, можно собрать из двух «невозможных» треугольников Пенроуза (Рис. 7). Рис. 7. Невозможная гексаграмма Гексаграмму называют также звездой Давида, звездой Голиафа и печатью Соломона. Поскольку строить правильные многоугольники с числом сторон n мы уже умеем, то нам нужно только научить программу соединять вершины определённым образом, чтобы получать не только правильные выпуклые многоугольники, но и звёздчатые (или просто - звёзды). Принято буквой m обозначать разность номеров соседних вершин звёздчатого многоугольника. Для обыкновенных, правильных многоугольников m = 1, так как стороны соединяют две смежные вершины. Для вычерчивания пентаграммы нужно положить m = 2. Для гексаграммы также m = 2. В процедуре Draw мы сначала создаём массив всех вершин заданного nугольника: 411
{$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // координаты вершин: var coordsX, coordsY : array of real; procedure Prepare(); begin gw.Hide(); gw.Title := 'Зубчатые многоугольники'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; end; // СОЗДАЁМ МАССИВ КООРДИНАТ ВЕРШИН МНОГОУГОЛЬНИКА procedure CreateCoords(n: integer); begin var alpha := 0.0; var da := 360.0 / n; var r := GW_WIDTH / 2 - 10; var dcx := 0.0; dcx := 0.0; var dcy := 0.0; var xc := CX + dcx; var yc := CY + dcy; 412
// координаты вершин: coordsX := new double[n]; coordsY := new double[n]; // вычисляем координаты вершин многоугольника: for var i := 0 to n - 1 do begin coordsX[i] := xc + r * Cos(DegToRad(alpha)); coordsY[i] := yc - r * Sin(DegToRad(alpha)); alpha += da; end end; procedure Draw(n,m: integer); begin // создаём массив координат вершин многоугольника: CreateCoords(n); // чертим многоугольник: DrawPoly(n, m); end; begin Prepare; Draw(5,2); //Draw(6,2); //Draw(18,1); //Draw(18,2); //Draw(18,3); //Draw(18,4); //Draw(18,5); //Draw(18,6); //Draw(18,7); //Draw(18,8); end. А затем, используя этот массив, чертим обычный (m=1) или звёздчатый (m > 1) многоугольник: 413
// БЫСТРЫЙ АЛГОРИТМ ЕВКЛИДА function NOD(n1, n2: integer): integer; begin while (n2 > 0) do begin var n := n1 mod n2; (n1, n2) := (n2, n); end; Result := n1; end; // ЧЕРТИМ МНОГОУГОЛЬНИК procedure DrawPoly(n,m: integer); begin // цвет линий: if (NOD(n, m) = 1) then gw.PenColor := 'Lime' else gw.PenColor := 'Yellow'; // толщина линий: gw.PenWidth := 2; // чертим многоугольник: for var i := 0 to n - 1 do begin Turtle.X := coordsX[i]; Turtle.Y := coordsY[i]; var nv2 := (i + m) mod n; Turtle.MoveTo(coordsX[nv2], coordsY[nv2]); end; end; Как вы видите на Рис. 8-9, мы окрасили звёздчатые многоугольники в зелёный и жёлтый цвета. Так мы разделяем связные и несвязные звёзды. Связные звёзды можно вычертить одним росчерком пера (это уникурсальные фигуры). Несвязные звёзды можно начертить только в несколько приёмов, поскольку они состоят из нескольких одинаковых частей – правильных многоугольников или связных звёзд. 414
Рис. 8. Draw(5,2) 415
Рис. 9. Draw(6,2) 416
Для определения типа звезды мы пользуемся простым правилом:   Если числа n и m взаимно простые, то звезда получится связная. Если числа n и m не взаимно простые, то звезда получится несвязная. Как вы знаете, если числа взаимно простые, то их наибольший общий делитель равен 1. В противном случае – больше 1. Для проверки чисел мы пользуемся методом NOD, реализующим быстрый алгоритм Евклида. Если взять многоугольник с большим числом сторон, то он порождает несколько звёзд разного типа – как связные, так и несвязные (Рис. 10-17). Если m > n/2, то звёзды повторяются, поэтому такие значения использовать нет смысла. 417
Рис. 10. Draw(18,1) 418
Рис. 11. Draw(18,2) 419
Рис. 12. Draw(18,3) 420
Рис. 13. Draw(18,4) 421
Рис. 14. Draw(18,5) 422
Рис. 15. Draw(18,6) 423
Рис. 16. Draw(18,7) 424
Рис. 17. Draw(18,8). Все 18-угольники 425
Рекурсивные квадраты Рекурсия заключается в многократном повторении одних и тех же действий с разными значениями параметров. В этом рекурсивные вызовы методов сходны с циклами. Рекурсивные вызовы, как и циклы, могут стать бесконечными, поэтому в каждом рекурсивном методе нужно предусмотреть условие выхода из него. В этом проекте Черепашка нарисует квадраты. Самый большой из них имеет ширину и высоту в половину ширины окна программы. Под ним лежат 4 квадрата вдвое меньшего размера, под каждым из них также по 4 квадрата – и так далее до бесконечности. Это – теоретически, а практически – размеры сторон квадрата очень быстро достигнут таких размеров, что их будет невозможно показать на экране. Вполне разумно ограничить длину сторон семью пикселями. Достижение этой длины и послужит условием выхода из рекурсии. После запуска программы вызывается процедура Draw, которая, в свою очередь, передаёт управление процедуре DrawRects, сообщая ей координаты центра самого большого квадрата и длину его сторон: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; 426
CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Рекурсивные квадраты'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'Black'; end; procedure DrawRect(x, y, size: integer); begin // x, y - коорд. верхн. лев. угла Turtle.X := x; Turtle.Y := y; Turtle.Angle := 90; Turtle.CreateFigure; loop 4 do begin Turtle.Move(size); Turtle.Turn(90); end; Turtle.FillFigure(); end; Процедура DrawRects не вычерчивает этот квадрат сразу, а вызывает сама себя для построения квадратов меньших размеров, центры которых находятся в вершинах более габаритных квадратов: procedure DrawRects(xc, yc, size: integer); begin if (size <= 6) then exit; var size2 := size div 2; 427
DrawRects(xc DrawRects(xc DrawRects(xc DrawRects(xc + + size2, size2, size2, size2, yc yc yc yc + + size2, size2, size2, size2, size2); size2); size2); size2); DrawRect(xc - size2, yc - size2, size); end; begin Prepare; Turtle.Speed := 100; gw.PenWidth := 1; gw.PenColor := 'White'; gw.BrushColor := 'OrangeRed'; var w := GW_WIDTH div 2; var h := GW_HEIGHT div 2; DrawRects(w, h, Min(w, h)); end. Вызовы процедуры DrawRects продолжаются до тех пор, пока величина сторон квадрата не станет меньше 7 пикселей. И только тогда начнут рисоваться самые маленькие квадратики, за ними – крупнее, ещё крупнее, пока, наконец, не будет нарисован самый большой квадрат в центре окна программы. В итоге большие квадраты всегда рисуются поверх меньших (Рис. 1). Если вы внимательно наблюдали за порханием Черепашки, то заметили, что первым появляется самый маленький квадратик в левом верхнем углу окна программы, затем такой же квадратик ниже, затем справа, затем по диагонали. Начертив 4 маленьких квадратика, Черепашка начертит квадратик побольше – с вершинами в центре маленьких квадратов. После этого будут построены ещё 3 группы таких же квадратов, причём в том же порядке – снизу, справа, по диагонали. Поверх этих групп квадратиков Черепашка начертит квадрат ещё большего размера. 428
Рис. 1. Рекурсивные квадраты 429
Очерёдность рисования квадратов однозначно определяется последовательностью вызовов метода DrawRects: DrawRects(x DrawRects(x DrawRects(x DrawRects(x + + size2, size2, size2, size2, y y y y + + size2, size2, size2, size2, size2); size2); size2); size2); При желании вы можете легко изменить её, переставив строки. Рисование продолжается до тех пор, пока не будет построен самый большой квадрат в половину окна приложения. Пристально наблюдая за этим процессом, вы наверняка лучше разберётесь, как действует рекурсия в этом примере. Слоёный торт Вы научились вычерчивать одноцветные рекурсивные квадраты всех размеров. А было бы куда интереснее каждый слой квадратов окрашивать в свой цвет. Так как размеры сторон квадратов уменьшаются вдвое от половины ширины окна до 7, то для раскрашивания квадратов нам необходимо 7 разных цветов, которые удобно хранить в массиве: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; 430
type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // Цвета квадратов: var iColor := |'Yellow', 'Red', 'Lime', 'Blue', 'Aqua', 'Fuchsia', 'White', 'Navy' |; procedure Prepare(); begin gw.Hide(); gw.Title := 'Слоёный торт'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'Black'; end; procedure DrawRect(x, y, size,id: integer); begin // x, y - коорд. верхн. лев. угла Turtle.X := x; Turtle.Y := y; Turtle.Angle := 90; gw.BrushColor := iColor[id]; 431
Turtle.CreateFigure; loop 4 do begin Turtle.Move(size); Turtle.Turn(90); end; Turtle.FillFigure(); end; begin Prepare; Turtle.Speed := 100; gw.PenWidth := 1.4; gw.PenColor := 'White'; var w := GW_WIDTH div 2; var h := GW_HEIGHT div 2; DrawRects(w, h, Min(w, h), 0); end. Вычерчивая очередной квадрат, мы определяем его размер и цвет в массиве iColor, после чего закрашиваем его этим цветом: procedure DrawRects(xc, yc, size, id: integer); begin if (size <= 6) then exit; var size2 := size div 2; DrawRects(xc DrawRects(xc DrawRects(xc DrawRects(xc + + size2, size2, size2, size2, yc yc yc yc + + size2, size2, size2, size2, size2,id+1); size2,id+1); size2,id+1); size2,id+1); DrawRect(xc - size2, yc - size2, size, id); end; Рекурсивный рисунок теперь напоминает современный коврик (Рис. 1). 432
Рис. 1. Рекурсивный коврик 433
Попробуйте подобрать другие расцветки для этих ковриков! Рекурсивные круги Давайте сделаем рисунок «покруглее», заменив угловатые квадраты геометрически толерантными кругами: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // Цвета: var iColor := |'Yellow', 'Red', 'Lime', 'Blue', 'Aqua', 'Fuchsia', 'White', 'Navy' |; procedure Prepare(); begin 434
gw.Hide(); gw.Title := 'Рекурсивные круги'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'Black'; end; // ЧЕРТИМ ОКРУЖНОСТЬ procedure DrawCircle(xc, yc, size,id: integer); begin // число сторон: var n := 50; // радиус окружности: var r := size * 0.7; // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; gw.BrushColor := iColor[id]; Turtle.X := xc + r * Cos(DegToRad(0)); Turtle.Y := yc - r * Sin(DegToRad(0)); Turtle.CreateFigure; // чертим окружность: for var i := 0 to n+1 do begin var x := xc + r * Cos(DegToRad(alpha)); var y := yc - r * Sin(DegToRad(alpha)); // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; Turtle.FillFigure; end; procedure DrawCircles(xc, yc, size, id: integer); begin 435
if (size <= 16) then exit; var size2 := size div 2; DrawCircles(xc - size2, yc DrawCircles(xc - size2, yc DrawCircles(xc + size2, yc DrawCircles(xc + size2, yc + + size2, size2, size2, size2, size2,id+1); size2,id+1); size2,id+1); size2,id+1); DrawCircle(xc, yc, size, id); end; begin Prepare; Turtle.Speed := 100; gw.PenWidth := 1.4; gw.PenColor := 'White'; var w := GW_WIDTH div 2; var h := GW_HEIGHT div 2; DrawCircles(w, h, Min(w, h), 0); end. Как вы видите, большая часть нашего усердия и энтузиазма ушла на простое переименование процедур. Зато микки-маусы получились весёлые и забавные (Рис. 1)! 436
Рис. 1. Рекурсивные микки-маусы 437
Рекурсивные круграты и квадруги А теперь давайте усложним себе жизнь попеременным рисованием кругов и квадратов. То есть вокруг каждого круга нарисуем по 4 квадрата и наоборот. Проблема легко решается с помощью двух процедур - DrawCircles и DrawRects, которые рекурсивно вызывают друг друга (Рис. 1-2): {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // Цвета: var iColor := |'Yellow', 'Red', 'Lime', 'Blue', 'Aqua', 'Fuchsia', 'White', 'Navy' |; procedure DrawCircles(xc, yc, size, id: integer); forward; procedure DrawRects(xc, yc, size, id: integer); forward; procedure Prepare(); begin gw.Hide(); gw.Title := 'Рекурсивные круграты и квадруги'; 438
gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'Black'; end; // ЧЕРТИМ ОКРУЖНОСТЬ procedure DrawCircle(xc, yc, size,id: integer); begin // число сторон: var n := 50; // радиус окружности: var r := size * 0.7; // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; gw.BrushColor := iColor[id]; Turtle.X := xc + r * Cos(DegToRad(0)); Turtle.Y := yc - r * Sin(DegToRad(0)); Turtle.CreateFigure; // чертим окружность: for var i := 0 to n+1 do begin var x := xc + r * Cos(DegToRad(alpha)); var y := yc - r * Sin(DegToRad(alpha)); // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; Turtle.FillFigure; end; procedure DrawRect(x, y, size,id: integer); begin // x, y - коорд. верхн. лев. угла Turtle.X := x; 439
Turtle.Y := y; Turtle.Angle := 90; gw.BrushColor := iColor[id]; Turtle.CreateFigure; loop 4 do begin Turtle.Move(size); Turtle.Turn(90); end; Turtle.FillFigure(); end; procedure DrawRects(xc, yc, size, id: integer); begin if (size <= 16) then exit; var size2 := size div 2; DrawCircles(xc DrawCircles(xc DrawCircles(xc DrawCircles(xc + + size2, size2, size2, size2, yc yc yc yc + + size2, size2, size2, size2, size2,id+1); size2,id+1); size2,id+1); size2,id+1); DrawRect(xc - size2, yc - size2, size, id); end; procedure DrawCircles(xc, yc, begin if (size <= 16) then exit; var size2 := size div 2; DrawRects(xc - size2, yc DrawRects(xc - size2, yc + DrawRects(xc + size2, yc DrawRects(xc + size2, yc + size, id: integer); size2, size2, size2, size2, size2,id+1); size2,id+1); size2,id+1); size2,id+1); DrawCircle(xc, yc, size, id); end; begin Prepare; Turtle.Speed := 100; gw.PenWidth := 1.4; 440
gw.PenColor := 'White'; var w := GW_WIDTH div 2; var h := GW_HEIGHT div 2; DrawCircles(w, h, Min(w, h), 0); //DrawRects(w, h, Min(w, h),0); end. Последовательность рисования кругов и квадратов легко изменить, оставляя незакомментированной нужную строчку в главном блоке. 441
Рис. 1. Рекурсивная смесь из кругов и квадратов 442
Рис. 2. Рекурсивная смесь из квадратов и кругов 443
Снежинка Коха Со снежинкой Коха вы уже встречались. Её, как вы помните, придумал в 1904 году шведский математик Гельг фон Кох, и это один из первых фракталов, построение которого было описано строго математически. Чтобы внимательно проследить весь процесс построения снежинки Коха разной сложности (она определяется числом итераций), мы последовательно нарисуем все снежинки и выделим их цветом: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720+20; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // Цвета: var iColor := |'Yellow', 'Red', 'Lime', 'Blue', 'Aqua', 'Fuchsia', 'White', 'Navy' |; 444
Если фрактальная Черепашка строила снежинки без нашей помощи, опираясь на свой инстинкт, который записывался очень просто, то в этом проекте нам придётся самостоятельно вычислять координаты вершин всех отрезков, образующих снежинку Коха. За геометрическими подробностями обращайтесь к книгам Дойны Логофату (Doina Logofatu) Grundlegende Algorithmen mit Java или Algorithmen und Problemlösungen mit C++ (Рис. 1). Рис. 1. Две обложки «одной» книги Для вычислений нам понадобятся 2 константы: const Sqrt3 = Sqrt(3.0); Sqrt36 = Sqrt(3.0) / 6.0; 445
В главном блоке цикл for рисует снежинки различной степени детализации разными цветами. После каждого цикла рисования следует двухсекундная пауза, чтобы вы могли перевести дух от восторга и восхищения: begin Prepare; Turtle.Speed := 100; gw.PenWidth := 2; for var i := 1 to 5 do begin gw.PenColor := iColor[i]; Draw(i); Sleep(2000); end; end. Для вычерчивания собственно снежинки вызывается процедура Draw, которой сообщается желательная степень детализации: // РИСУЕМ СНЕЖИНКУ procedure Draw(iter: integer); begin var size := GW_WIDTH / 2; var t := size; var xa := size; var ya := size - t; var xb := size - Sqrt3 * t / 2; var yb := size + t / 2; DrawKoch(iter, xa, ya, xa := size + Sqrt3 * t ya := size + t / 2; DrawKoch(iter, xb, yb, xb := size; yb := size - t; DrawKoch(iter, xa, ya, end; xb, yb); / 2; xa, ya); xb, yb); 446
Процедура Draw, в свою очередь, трижды вызывает процедуру DrawKoch с различными значениями параметров: procedure DrawKoch(n: integer; xa, ya, xb, yb : double); begin if ( n > 1 ) then begin var xm := (xa + xb) / 2 + Sqrt36 * (ya - yb); var ym := (ya + yb) / 2 + Sqrt36 * (xb - xa); var xc := (2 * xa + xb) / 3; var yc := (2 * ya + yb) / 3; var xd := (xa + 2 * xb) / 3; var yd := (ya + 2 * yb) / 3; DrawKoch(n - 1, xa, ya, xc, yc); DrawKoch(n - 1, xc, yc, xm, ym); DrawKoch(n - 1, xm, ym, xd, yd); DrawKoch(n - 1, xd, yd, xb, yb); end else begin Turtle.X := xa; Turtle.Y := ya+10; Turtle.MoveTo(xb, yb+10); end; end; Если число итераций равно единице, то Черепашка начертит равносторонний треугольник, который послужит основой для построения последующих кривых. Красочный итог нашей кропотливой работы вы можете наблюдать на Рис. 2. 447
Рис. 2. Праздничная снежинка Коха 448
Триколор Мы уже освоили шахматную доску, клетки которой окрашены в 2 цвета. Давайте поставим перед собой и решим более общую задачу: пусть клетки окрашиваются в любое число цветов! Тогда, например, мы легко нарисуем российский триколор (Рис. 1). Чтобы оперировать любым числом цветов, заведём для них строковый массив colors: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // Цвета квадратов: var colors := |'White','Blue','Red'|; Процедура Draw с помощью свойства Length узнаёт число цветов и периодически окрашивает «квадраты» в эти цвета: // РИСУЕМ procedure Draw(cellWidth, cellHeight : integer); begin // ширина рисунка: var w := GW_WIDTH div cellWidth * cellWidth; // высота рисунка: var h := GW_HEIGHT div cellHeight * cellHeight; 449
// отступы: var offsetX := (GW_WIDTH - w) div 2; var offsetY := (GW_HEIGHT - h) div 2; // число цветов: var nclr := colors.Length; for var y := 0 to GW_HEIGHT div cellHeight - 1 do begin for var x := 0 to GW_WIDTH div cellWidth - 1 do begin gw.BrushColor := colors[(x + y) mod nclr]; var xr := offsetX + x * cellWidth; var yr := offsetY + y * cellHeight; var wr := cellWidth; var hr := cellHeight; DrawRect(xr, yr, wr, hr); end; end; end; Квадраты наша Черепашка рисовать научилась. У прямоугольников длина горизонтальных и вертикальных сторон могут различаться, поэтому Черепашка рисует их парами: // РИСУЕМ ЗАКРАШЕННЫЙ ПРЯМОУГОЛЬНИК procedure DrawRect(x, y, w, h: integer); begin // x, y - коорд. верхн. лев. угла Turtle.X := x; Turtle.Y := y; Turtle.Angle := 90; Turtle.CreateFigure; loop 2 do begin Turtle.Move(w); Turtle.Turn(90); Turtle.Move(h); Turtle.Turn(90); end; Turtle.FillFigure(); end; 450
Рис. 1. Российский флаг 451
В главном блоке программы вызываем процедуру Draw с желаемыми размерами прямоугольника: begin Prepare; Turtle.Speed := 100; gw.PenWidth := 0; // Российский флаг: Draw(120, 44); end. Записываем в массив все яркие цвета – и получаем великолепный узор (Рис. 2-7): var colors := |'Red', 'Orange', 'Yellow', 'Green', 'DeepSkyBlue', 'Blue', 'Purple'|; Ничуть не труднее получить более модные нынче абстрактные расцветки! Используйте в процедуре Draw «генератор псевдослучайных цветов» и принимайтесь за художества: var clr1 := Random(nclr); while clr1 = clr2 do clr1 := Random(nclr); gw.BrushColor := colors[clr1]; clr2 := clr1; Всего за пару минут вы осчастливите просвещённый мир десятками своих креативных творений (Рис. 3-)! 452
Рис. 2. Разноцветная шахматная доска 453
Рис. 3. Draw(16, 16) 454
Рис. 4. Draw(16, 180) 455
Рис. 5. Draw(720, 2) 456
Рис. 6. Draw(240, 240) 457
Рис. 7. Draw(360, 240) 458
Компьютерная радуга Модель RGB не позволяет легко находить цвет, близкий к заданному, поэтому нарисовать, например, радугу или градиент совсем непросто. А вот в цветовом пространстве HSV мы без особого труда сможем нарисовать почти настоящую радугу! На Рис. 1 видно, что наша модель радуги вполне реалистична, хотя и не совсем точно передаёт настоящие цвета. Рис. 1. Компьютерная радуга о семи цветах Для вычисления цветов мы воспользуемся новой функцией HSV2RGB, которая получает заданное значение тона n * p. А насыщенность и яркость всегда максимальны, чтобы создать «чистые» цвета: 459
// КОНВЕРТИРУЕМ ЦВЕТ HSV В RGB function HSV2RGB(h: double; s: double := 100; v:double := 100): string; begin s := s / 100.0; v := v / 100.0; var r := 0.0; var g := 0.0; var b := 0.0; //оттенки серого: if (s = 0) then begin r := v; g := v; b := v; end else begin var Hi := Ceil(Math.Floor(h / 60)); var f := h / 60 - Hi; //цветовые var p := v var q := v var t := v оси: * (1 - s); * (1 - (s * f)); * (1 - (s * (1 - f))); case Hi of 0: begin r := v; g := t; b := p; end; 1: begin r := q; g := v; b := p; end; 2: begin r := p; g := v; b := t; 460
end; 3: begin r := p; g := q; b := v; end; 4: begin r := t; g := p; b := v; end; 5: begin r := v; g := p; b := q; end; end; end; Result := gw.GetColorFromRGB(Ceil(r * 255), Ceil(g * 255), Ceil(b * 255)); end; // ВОЗВРАЩАЕМ СПИСОК ЦВЕТОВ function GetRainbowColors(num : integer):List<string>; begin var res := new List<string>(num); var p := 360.0 / num; for var n := 0 to num-1 do res.Add(Hsv2Rgb(n * p, 100.0, 100.0)); Result := res; end; Благодаря непрерывному цветовому кольцу HSB мы получаем нужные цвета, пропорционально увеличивая угол (тон) для каждого следующего цвета! Так как цвета в радуге следуют друг за другом, то мы не будем рисовать двухмерную таблицу, а ограничимся всего одной строкой: 461
// РИСУЕМ ЗАКРАШЕННЫЙ ПРЯМОУГОЛЬНИК procedure DrawRect(x, y, w, h: integer); begin // x, y - коорд. верхн. лев. угла Turtle.X := x; Turtle.Y := y; Turtle.Angle := 90; Turtle.CreateFigure; loop 2 do begin Turtle.Move(w); Turtle.Turn(90); Turtle.Move(h); Turtle.Turn(90); end; Turtle.FillFigure(); end; // РИСУЕМ procedure DrawColorGrid(nclr : integer); begin var lstColor := GetRainbowColors(nclr); // число цветов: var count := lstColor.Count; // размеры клеток: var w := GW_WIDTH div count; var h := GW_HEIGHT - 3; // цвет рамки: gw.PenColor := 'Black'; gw.PenWidth := 0; // по всем цветам: for var i := 0 to count-1 do begin var x := i mod count; var y := i div count; gw.BrushColor := lstColor[i]; DrawRect(2 + x * (1 + w), 1 + y * (3 + h), w, h); end; end; 462
begin Prepare; Turtle.Speed := 100; DrawColorGrid(7); //DrawColorGrid(4); //DrawColorGrid(80); end. Для изменения числа цветов в радуге (у нас всё-таки компьютерная радуга, а не настоящая!) мы задаём значения в диапазоне 1..150. Так вы сможете увидеть 4 цвета (Рис. 2). Рис. 2. Четырёхцветная радуга А радуга 80 цветов из у нас получилась очень весёлая (Рис. 3)! 463
Рис. 3. Многоцветная радуга Астроида Астроида – это плоская кривая, напоминающая звезду с четырьмя лучами. Такая форма кривой и дала ей название. Конечно, название кривой дала не сама форма, а австрийский астроном Йозеф Иоганн фон Литров в 1838 году, который назвал её astrois в книге Kurze Anleitung zur gesammten Mathematik, которая вышла в Вене в 1838 году (Рис. 1). Современное название кривой происходит от греческих слов αστρον — звезда и ειδος — вид, что можно перевести как звездообразная. 464
Рис. 1. Свидетельство о рождении Уравнение астроиды в декартовых координатах: X2/3 + y2/3 = R2/3 Астроиду описывает точка окружности радиуса r, которая катится внутри окружности радиуса R = 4r (Рис. 2). Рис. 2. Рождение астроиды Но астроиду можно построить и как огибающую семейства отрезков, соединяющих точки на осях координат. На Рис. 3 хорошо видно, как нужно расставить точки на координатных осях и как затем соединить их. 465
Рис. 3. Построение астроиды 466
Поскольку мы уже знаем теоретические основы построения астроиды, то легко вычертим координатные оси и все отрезки прямых, которые и представят нам во всей красе нашу замечательную кривую: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // толщина линий: PEN_WIDTH = 1.6; procedure Prepare(); begin gw.Hide(); gw.Title := 'Астроида'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'White'; end; // ЧЕРТИМ АСТРОИДУ procedure Draw(); begin // шаг: var step := 10; 467
// толщина линий: gw.PenWidth := 1; gw.PenColor := 'DarkGray'; // проводим линии: for var i := 0 to CY div step do begin // левый верхний квадрант: Turtle.X := 0 + i * step; Turtle.Y := CY; Turtle.MoveTo(CX, CY - i * step); // правый верхний квадрант: Turtle.X := GW_WIDTH - i * step; Turtle.Y := CY; Turtle.MoveTo(CX, CY - i * step); // левый нижний квадрант: Turtle.X := 0 + i * step; Turtle.Y := CY; Turtle.MoveTo(CX, CY + i * step); // правый нижний квадрант: Turtle.X := GW_WIDTH - i * step; Turtle.Y := CY; Turtle.MoveTo(CX, CY + i * step); end; // проводим оси --> gw.PenWidth := PEN_WIDTH; gw.PenColor := 'Black'; // горизонтальная: Turtle.X := 0; Turtle.Y := CY; Turtle.MoveTo(GW_WIDTH, CY); // вертикальная: Turtle.X := CX; Turtle.Y := 0; Turtle.MoveTo(CX, GW_HEIGHT); end; begin Prepare; Turtle.Speed := 100; 468
Draw(); end. Астроида 2 Мы вполне можем отказаться от «сложных» вычислений и геометрических построений, если воспользуемся параметрическим уравнением астроиды: Параметрические кривые мы умеем вычерчивать: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; BACKGROUND = 'DarkGreen'; procedure Prepare(); begin gw.Hide(); gw.Title := 'Астроида 2'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; 469
gw.BackgroundColor := BACKGROUND; end; // КЛАСС КООРДИНАТ ТОЧЕК type Point = auto class x, y : double; end; // СОЗДАЁМ АСТРОИДУ procedure CreateCurve(fill : boolean := false); begin // задаём цвет линии: var clr := 'Lime'; //clr := 'Red'; gw.PenWidth := 2.5; gw.PenColor := clr; // шаг: var step := 0.025; // коэффициент увеличения: var k := 217; // радиус: var R := 1.65; // список точек: var lstPoints := new List<Point>(); // добавляем вершины в список: var t := 0.0; while t < PI * 2 do begin var ct := Cos(t); var x := R * ct * ct * ct; var st := Sin(t); var y := R * st * st * st; x := x * k + CX; y := y * k + CY; lstPoints.Add(new Point(x, y)); t += step; end; // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; Turtle.CreateFigure; foreach var pt in lstPoints do begin 470
var x := pt.x; var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; if fill then Turtle.FillFigure(); end; begin Prepare; Turtle.Speed := 100; //CreateCurve(); gw.BrushColor := 'Yellow'; CreateCurve(true); end. Правда, астроида в параметрическом исполнении выглядит не столь привлекательно, как раньше, даже с цветной раскраской (Рис. 1-2). 471
Рис. 1. Простая астроида 472
Рис. 2. Закрашенная астроида 473
Астроида 3 На самом деле звёзды имеют форму шара, а лучи образуются в хрусталике глаза или в линзах оптических приборов. Их может быть 4, 6 или 8 (Рис. 1). Рис. 1. Многолучевые звёзды Геометрический способ построения астроид мы можем легко распространить на большее число осей. Проще всего добавлять пары осей. Тогда для чётного числа осей мы получим очень простую программу (Рис. 2-5): {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // толщина линий: PEN_WIDTH = 1.6; 474
procedure Prepare(); begin gw.Hide(); gw.Title := 'Астроида 3'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'White'; end; procedure DrawLines(r: integer; step, a1, a2: double); begin // число линий: var n :=Round(CY / step); var rad1 := DegToRad(a1); var rad2 := DegToRad(a2); // проводим линии: for var i := 0 to n do begin var x1 := CX + r / n * i * Cos(rad1); var y1 := CY - r / n * i * Sin(rad1); var x2 := CX + r / n * (n - i) * Cos(rad2); var y2 := CY - r / n * (n - i) * Sin(rad2); Turtle.X := x1; Turtle.Y := y1; Turtle.MoveTo(x2, y2); end; end; // ЧЕРТИМ АСТРОИДУ procedure Draw(n : integer); begin // толщина линий: gw.PenWidth := 1; gw.PenColor := 'DarkGray'; 475
// шаг: var step := 10.0; // число лучей: var star := n; // шаг: var alpha := 360.0 / star; // длина осей: var r := GW_WIDTH div 2; for var i := 0 to star-1 do DrawLines(r, step, i * alpha, alpha * (i + 1)); // проводим оси --> gw.PenWidth := PEN_WIDTH; gw.PenColor := 'Black'; // угол: var a := 0.0; for var i := 0 to star-1 do begin var rad := DegToRad(a); // начало линии: var x1 := CX - r * Cos(rad); var y1 := CY - r * Sin(rad); // конец линии: var x2 := CX + r * Cos(rad); var y2 := CY + r * Sin(rad); Turtle.X := x1; Turtle.Y := y1; Turtle.MoveTo(x2, y2); a += alpha; end; end; begin Prepare; Turtle.Speed := 100; //Draw(6); Draw(8); end. 476
Рис. 2. Шестилучевая астроида 477
Рис. 3. Восьмилучевая астроида 478
Рис. 4. Десятилучевая астроида 479
Рис. 5. Двенадцатилучевая астроида 480
Однако чаще мы представляем себе звезду с пятью лучами (Рис. 6-7). Рис. 6. Правильная звезда 481
Рис. 7. Морская звезда Слегка подработав «чётные» процедуры, мы сможем нарисовать «астроиды» с пятью лучами (Рис. 8): begin Prepare; Turtle.Speed := 100; //Draw(6); //Draw(8); //DrawN(5); //DrawN(9); DrawN(13); end. Эта процедура позволяет вычерчивать астроиды с 9-ю и с 13-ю лучами (Рис. 9-10). 482
Рис. 8. Пятилучевая астроида 483
Рис. 9. Девятилучевая астроида 484
Рис. 10. Тринадцатилучевая астроида 485
А вот 7- и 11-лучевые звёзды так вычертить не удастся. Попробуйте самостоятельно справиться с этой геометрической проблемой! Если обойтись без осей, то нечётные звёзды вычертить даже проще, чем чётные: // ЧЕРТИМ АСТРОИДУ procedure Draw(n : integer; axes: boolean := true); begin // толщина линий: gw.PenWidth := 1; gw.PenColor := 'DarkGray'; // шаг: var step := 10.0; // число лучей: var star := n; // шаг: var alpha := 360.0 / star; // длина осей: var r := GW_WIDTH div 2; for var i := 0 to star-1 do DrawLines(r, step, i * alpha, alpha * (i + 1)); if not axes then exit; // проводим оси --> gw.PenWidth := PEN_WIDTH; gw.PenColor := 'Black'; // угол: var a := 0.0; for var i := 0 to star-1 do begin var rad := DegToRad(a); // начало линии: var x1 := CX - r * Cos(rad); var y1 := CY - r * Sin(rad); 486
// конец линии: var x2 := CX + r * Cos(rad); var y2 := CY + r * Sin(rad); Turtle.X := x1; Turtle.Y := y1; Turtle.MoveTo(x2, y2); a += alpha; end; end; Вот так выглядят «недоступные» 7-лучевые и 11-лучевые звёзды (Рис. 11). 487
Рис. 11. Семилучевая астроида 488
Рис. 12. Одиннадцатилучевая астроида 489
Цветные линии Если соединять пары точек, которые находятся на границах окна программы, а не на осях координат, то Черепашка нарисует картинку из лучей астроиды разного цвета (Рис. 1). Как изменяются координаты начала и конца линии, хорошо видно на рисунке – они скользят по границам клиентской области окна с заданным шагом 10 пикселей. Изменяя значение этого параметра, а также цвет линий, вы сможете получить и другие узоры. // ПРОГРАММА "ЦВЕТНЫЕ ЛИНИИ" {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // толщина линий: PEN_WIDTH = 1.0; procedure Prepare(); begin gw.Hide(); gw.Title := 'Цветные линии'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; 490
gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'Black'; end; Чтобы каждый из четырёх наборов линий имел свой собственный цвет, мы перед рисованием прямой линии макаем перо в чернила нужного цвета: // ЧЕРТИМ procedure Draw(); begin // отношение высоты окна к ширине: var ratio := GW_HEIGHT / GW_WIDTH; var x := 0; // чертим цветные линии while(x <= GW_WIDTH) do begin // красная линия: gw.PenColor := 'Red'; Turtle.X := 0; Turtle.Y := x * ratio; Turtle.MoveTo(GW_WIDTH - x, 0); // жёлтая линия: gw.PenColor := 'Yellow'; Turtle.X := 0; Turtle.Y := (GW_WIDTH - x) * ratio; Turtle.MoveTo(GW_WIDTH-x, GW_WIDTH * ratio); // синяя линия: gw.PenColor := 'Blue'; Turtle.X := GW_WIDTH - x; Turtle.Y := 0; Turtle.MoveTo(GW_WIDTH, (GW_WIDTH - x) * ratio); // зелёная линия: gw.PenColor := 'Green'; Turtle.X := GW_WIDTH - x; Turtle.Y := GW_WIDTH * ratio; 491
Turtle.MoveTo(GW_WIDTH, x * ratio); // смещение: x += 10; end; // While end; begin Prepare; Turtle.Speed := 100; Draw(); end. 492
Рис. 1. Четыре четырки от астроиды 493
Новые розетки Продолжаем глумиться над окружностями. На этот раз мы будем чертить одинаковые окружности, центр которых удалён от центра канвы на некоторое расстояние offset: // радиус: var R := 176.0; var offset := 176; Центр каждой следующей окружности поворачивается вокруг центра канвы на заданный угол dangle: var angle := 0.0; var dangle := DegToRad(6); В итоге мы получим множество окружностей, которые образуют фигуру, называемую розеткой. Лучше известны электрические розетки, которые совсем не похожи на розочки (Рис. 1). В старых квартирах, на потолке можно увидеть декоративные розетки с растительным орнаментом (Рис. 2). 494
Рис. 1. Электрическая розетка Рис. 2. Декоративная розетка Наша розетка проще, но тоже чертовски хороша (Рис. 3)! {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // толщина линий: PEN_WIDTH = 1.6; procedure Prepare(); begin gw.Hide(); 495
gw.Title := 'Новые розетки'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; end; // КЛАСС КООРДИНАТ ТОЧЕК type Point = auto class x, y : double; end; // ЧЕРТИМ РОЗЕТКУ procedure DrawRosette(); begin // задаём цвет линии: var clr := 'Lime'; gw.PenColor := clr; gw.PenWidth := 2.0; var angle := 0.0; var dangle := DegToRad(6); // радиус: var R := 176.0; var offset := 176; // список точек: var lstPoints := new List<Point>(); while angle <= PI * 2 do begin // шаг: var step := 0.1; // очищаем список точек: lstPoints.Clear; // добавляем вершины в список: var t := 0.0; while t < PI * 2 do begin var x := CX + Cos(t) * R + offset * Cos(angle); var y := CY + Sin(t) * R + offset * Sin(angle); 496
lstPoints.Add(new Point(x, y)); t += step; end; angle += dangle; // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; foreach var pt in lstPoints do begin var x := pt.x; var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; end; end; begin Prepare; Turtle.Speed := 100; // чертим розетку: DrawRosette(); end. Изменяя значения радиуса окружностей и их удаление от центра, вы будете получать другие красивые розетки (Рис. 4): // ЧЕРТИМ РОЗЕТКУ procedure DrawRosette2(); begin // задаём цвет линии: var clr := 'Lime'; gw.PenColor := clr; gw.PenWidth := 2.0; var angle := 0.0; var dangle := DegToRad(6); // радиус: var k := 1.18; 497
var R := 119 * k; var offset := 180 * k; // список точек: var lstPoints := new List<Point>(); while angle <= PI * 2 do begin // шаг: var step := 0.1; // очищаем список точек: lstPoints.Clear; // добавляем вершины в список: var t := 0.0; while t < PI * 2 do begin var x := CX + Cos(t) * R + offset * Cos(angle); var y := CY + Sin(t) * R + offset * Sin(angle); lstPoints.Add(new Point(x, y)); t += step; end; angle += dangle; // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; foreach var pt in lstPoints do begin var x := pt.x; var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; end; end; Если слегка недовертеть окружности, то, во-первых, хорошо видно, как образуется розетка, а, во-вторых, получается весьма изящная фигура (Рис. 6): // ЧЕРТИМ РОЗЕТКУ procedure DrawRosette3(); begin // задаём цвет линии: var clr := 'Lime'; gw.PenColor := clr; 498
gw.PenWidth := 2.0; var angle := DegToRad(45.0);//0.0; var dangle := DegToRad(6); // радиус: var k := 1.52; var R := 160 * k; var offset := 70 * k; // список точек: var lstPoints := new List<Point>(); while angle <= PI * 2 - DegToRad(45) do begin // шаг: var step := 0.1; // очищаем список точек: lstPoints.Clear; // добавляем вершины в список: var t := 0.0; while t < PI * 2 do begin var x := CX + Cos(t) * R + offset * Cos(angle + DegToRad(-90)); var y := CY + Sin(t) * R + offset * Sin(angle + DegToRad(-90)); lstPoints.Add(new Point(x, y)); t += step; end; angle += dangle; // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; foreach var pt in lstPoints do begin var x := pt.x; var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; end; end; 499
Рис. 3. Компьютерная розетка 500
Рис. 5. Бубличная розетка 501
Рис. 6. Недокрученная розетка 502
Ещё одна симпатичная розетка (Рис. 7): // ЧЕРТИМ РОЗЕТКУ procedure DrawRosette4(); begin // задаём цвет линии: var clr := 'Lime'; gw.PenColor := clr; gw.PenWidth := 2; var angle := 0.0; var dangle := DegToRad(6); // радиус: var k := 1.19; var R := 119 * k; var offset := 180 * k; // список точек: var lstPoints := new List<Point>(); while angle <= PI * 2 - dangle do begin // шаг: var step := 0.1; // очищаем список точек: lstPoints.Clear; // добавляем вершины в список: var t := 0.0; while t < PI * 2 + 0.2 do begin var x := CX + Cos(t) * R + offset * Cos(angle + DegToRad(-107)); var y := CY + Sin(t) * R + offset * Sin(angle + DegToRad(-107)); lstPoints.Add(new Point(x, y)); t += step; end; angle += dangle; // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; foreach var pt in lstPoints do begin var x := pt.x; 503
var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; end; end; Добавим немного смекалки, чтобы получить очаровательную розетку (Рис. 8): // ЧЕРТИМ РОЗЕТКУ procedure DrawRosette5(); begin // задаём цвет линии: var clr := 'Lime'; gw.PenColor := clr; gw.PenWidth := 2; var angle := 0.0; var dangle := DegToRad(6); // радиус: var R := 176; var offset := 176; // список точек: var lstPoints := new List<Point>(); while angle <= PI * 2 - dangle*24 do begin // шаг: var step := 0.1; // очищаем список точек: lstPoints.Clear; // добавляем вершины в список: var t := 0.0; while t < PI * 2 do begin var x := CX + Cos(t) * R + offset * Cos(angle + DegToRad(-15.5)); var y := CY + Sin(t) * R + offset * Sin(angle + DegToRad(-15.5)); lstPoints.Add(new Point(x, y)); t += step; end; 504
angle += dangle; // последняя точка кривой: Turtle.X := lstPoints[lstPoints.Count-1].x; Turtle.Y := lstPoints[lstPoints.Count-1].y; foreach var pt in lstPoints do begin var x := pt.x; var y := pt.y; // чертим отрезок: Turtle.MoveTo(x, y); end; end; end; 505
Рис. 7. Симпатичная розетка 506
Рис. 8. Очаровательная розетка 507
Вращающиеся квадраты Чтобы вам сразу стал понятен замысел проекта, посмотрите на готовую картинку (Рис. 1): {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; procedure Prepare(); begin gw.Hide(); gw.Title := 'Вращающиеся квадраты'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := gw.GetColorFromRGB(240,240,240); end; // ЧЕРТИМ КВАДРАТЫ procedure DrawSquares(n, nrot: integer); begin // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: 508
var r := GW_WIDTH / 1.42 - 10.0; // координаты точек: var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 4; gw.PenColor := 'Black'; // цвет заливки: var clr := 240.0; gw.BrushColor := gw.GetColorFromRGB(clr, clr, clr); var xc := CX; var yc := CY; for var j := 0 to nrot-1 do begin alpha := j * 45.0 - 45; // чертим квадрат: Turtle.X := xc + r * Cos(DegToRad(alpha)); Turtle.Y := yc - r * Sin(DegToRad(alpha)); Turtle.CreateFigure; for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc - r * Sin(DegToRad(alpha)); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; Turtle.FillFigure; gw.PenWidth := 1; // цвет заливки более тёмный: clr /= 1.1; gw.BrushColor := gw.GetColorFromRGB(clr, clr, clr); r := r * Sqrt(2) / 2; end; end; 509
begin Prepare; Turtle.Speed := 100; DrawSquares(4,12); end. Легко увидеть здесь фигуры из проекта Цветные вращающиеся многоугольники, в роли которых выступают исключительно квадраты. Пора приступать к экспериментам. Добавим в процедуру DrawSquares параметр k, который отвечает за уменьшение длины сторон квадратов: // ЧЕРТИМ КВАДРАТЫ procedure DrawSquares(n, nrot: integer; k: double); begin // текущий угол: var alpha := 0.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: var r := GW_WIDTH / 1.42 - 10.0; r /= k; // координаты точек: var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 4; gw.PenColor := 'Black'; // цвет заливки: var clr := 240.0; gw.BrushColor := gw.GetColorFromRGB(clr, clr, clr); var xc := CX; var yc := CY; 510
for var j := 0 to nrot-1 do begin alpha := j * 45.0 - 45; // чертим квадрат: Turtle.X := xc + r * Cos(DegToRad(alpha)); Turtle.Y := yc - r * Sin(DegToRad(alpha)); Turtle.CreateFigure; for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc - r * Sin(DegToRad(alpha)); if i = 0 then begin Turtle.X := x; Turtle.Y := y; end else // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; Turtle.FillFigure; gw.PenWidth := 1; // цвет заливки более тёмный: clr /= 1.1; gw.BrushColor := gw.GetColorFromRGB(clr, clr, clr); //r := r * Sqrt(2) / 2; r /= k; end; end; begin Prepare; Turtle.Speed := 100; //DrawSquares(4,12); При k = 1.18 получается интересная картинка (Рис. 2): DrawSquares(4,20,1.18); end. 511
Рис. 1. Квадратура квадрата 512
Рис. 2. Квадраты уходят вдаль 513
Добавим в процедуру DrawSquares ещё один параметр – rot, который определяет угол поворота следующего квадрата по отношению к текущему: // ЧЕРТИМ КВАДРАТЫ procedure DrawSquares(n, nrot: integer; k, rot: double); begin // текущий угол: var alpha := 90.0; // приращение угла: var da := 360.0 / n; // радиус описанной окружности: var r := GW_WIDTH / 1.42 - 10.0; //r /= k; // координаты точек: var x := 0.0; var y := 0.0; // толщина пера: gw.PenWidth := 4; gw.PenColor := 'Black'; // цвет заливки: var clr := 240.0; gw.BrushColor := gw.GetColorFromRGB(clr, clr, clr); var xc := CX; var yc := CY; for var j := 0 to nrot-1 do begin alpha := j * rot + 45; // чертим квадрат: Turtle.X := xc + r * Cos(DegToRad(alpha)); Turtle.Y := yc - r * Sin(DegToRad(alpha)); Turtle.CreateFigure; for var i := 0 to n do begin x := xc + r * Cos(DegToRad(alpha)); y := yc - r * Sin(DegToRad(alpha)); if i = 0 then begin 514
Turtle.X := x; Turtle.Y := y; alpha += da; continue; end else // чертим отрезок: Turtle.MoveTo(x, y); alpha += da; end; Turtle.FillFigure; gw.PenWidth := 1; // цвет заливки более тёмный: clr /= 1.1; gw.BrushColor := gw.GetColorFromRGB(clr, clr, clr); r /= k; end; end; begin Prepare; Turtle.Speed := 100; //DrawSquares(4,12); //DrawSquares(4,20,1.18); DrawSquares(4,20,1.225, -15); end. Первоначально закрутка квадратов равна 45 градусам. Если вы уменьшите её до 15 градусов, то квадраты образуют вот такую замечательную спираль (Рис. 3). Если вы хотите, чтобы вершины внутренних квадратов лежали на сторонах внешних, то подбирайте значение коэффициента s. 515
Рис. 3. Квадратная спираль 516
Движение объекта к цели В этом проекте Черепашка будет собирать игровые объекты на сцене. Вы можете представлять их как вкусняшки для Черепашки, а мы просто нарисуем красные кружочки, которые хорошо видны как нам, так и Черепашке. Сразу после запуска программы мы создаём первую цель в случайном месте сцены и сразу отправляем к ней нашу Черепашку: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; var targetX, targetY : double; // СОЗДАЁМ ЦЕЛЬ procedure CreateTarget(rnd : boolean := false); begin var r := 32; gw.BrushColor := 'Red'; if rnd then begin targetX := Random(20, GW_WIDTH - 20); targetY := Random(20, GW_HEIGHT - 20); end else begin targetX := double(Mouse.X - gw.Left) - 6; targetY := double(Mouse.Y - gw.Top) - r; end; gw.FillEllipse(targetX-r/2, targetY-r/2, r, r); 517
Turtle.MoveTo(targetX, targetY); end; begin Prepare; Turtle.Speed := 7; Turtle.Show; CreateTarget(true); end. В процедуре Prepare мы назначаем процедуру-обработчик для события нажатия кнопки мышки: procedure Prepare(); begin gw.Hide(); gw.Title := 'Цвижение объекта к цели'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'LightGreen'; gw.MouseDown += OnMouseDown; end; Следующие цели создаются не случайно, а на месте клика мышки, то есть мы сами управляем действиями Черепашки: // МЕТОД-ОБРАБОТЧИК НАЖАТИЯ КНОПКИ МЫШКИ procedure OnMouseDown(); begin CreateTarget(); end; Рис. 1 показывает Черепашку в действии. 518
Рис. 1. Рождённая ползать 519
Мелкие цели не будят энтузиазм, но тогда и сама Черепашка мелковата для столь возвышенных целей. Не дожидаясь результатов естественной эволюции, мы можем увеличить Черепашку до подобающих случаю размеров. Для этого у Черепашки есть свойства Width и Height: begin Prepare; Turtle.Show; Turtle.Speed := 7; Turtle.Width := 24; Turtle.Height := 24; CreateTarget(true); end. Теперь Черепашка вполне гармонирует с размерами целей и возложенными на неё задачами (Рис. 2). 520
Рис. 2. Крупная Черепашка 521
Все в сборе В этом проекте мы добавим нашей Черепашке толику интеллекта, чтобы она собирал артефакты по всей сцене. Этот процесс можно сравнить с покупкой вкусняшек в разных магазинах. Как известно, уборочной предшествует посевная, поэтому мы начинаем программу с создания некоторого числа целей. Вполне достаточно десятка: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library; type gw = GraphicsWindow; // КЛАСС КООРДИНАТ ТОЧЕК type Point = auto class x, y : double; end; // размеры окна: const GW_WIDTH = 720; GW_HEIGHT = 720; CX = GW_WIDTH div 2; CY = GW_HEIGHT div 2; // число целей: var nTargets := 10; Для покупок Черепашке нужен список целей: // список целей: listTargets := new List<Point>(); 522
После запуска программа создаёт цели в случайных местах сцены и помещает их в список: procedure Prepare(); begin gw.Hide(); gw.Title := 'Все в сборе'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'LightGreen'; end; // СОЗДАЁМ ЦЕЛИ procedure CreateTargets(); begin var r := 32; gw.BrushColor := 'Red'; // создаём цели: loop nTargets do begin var x := Random(20, GW_WIDTH - 20); var y := Random(20, GW_HEIGHT - 20); listTargets.Add(new Point(x,y)); gw.FillEllipse(x-r/2, y-r/2, r, r); end; end; После создания целей Черепашка отправляется на их собирательство: // ЧЕРЕПАШКА ОБХОДИТ ЦЕЛИ procedure Собирайка; begin foreach var p in listTargets do begin // координаты очередной цели: var x := p.x; var y := p.y; 523
Turtle.MoveTo(x,y); end; end; begin Prepare; Turtle.Show; Turtle.Speed := 7; Turtle.Width := 24; Turtle.Height := 24; CreateTargets(); Собирайка; end. В процедуре Собирайка Черепашка последовательно получает координаты целей в порядке их создания и отправляется к очередной цели. Запускаем программу, и наша Черепашка быстро и аккуратно собирает все цели на сцене (Рис. 1). 524
Рис. 1. Не поспешишь – людей не насмешишь 525
Все в сборе 2 Поведение нашей Черепашки станет более разумным, если она будет собирать цели не точно по списку, а каждый раз находить ближайшую цель. Число целей я увеличил до 20, чтобы напрячь интеллект Черепашки до предела: // число целей: var nTargets := 20; Весь искусственный разум Черепашки мы записали в процедуру Собирайка: // ЧЕРЕПАШКА ОБХОДИТ ЦЕЛИ procedure Собирайка; begin // координаты ближайшей цели: var xb, yb : double; var id := -1; Пока список целей не пустой, мы находим в списке listTargets ближайшую цель: while listTargets.Count > 0 do begin var minDist := double.MaxValue; // координаты Черепашки: var mx := double(Turtle.X); var my := double(Turtle.Y); // ищем ближайшую цель: for var i := 0 to listTargets.Count-1 do begin var p := listTargets[i]; // координаты очередной цели: var x := p.x; var y := p.y; Если очередная цель ближе предыдущих, мы запоминаем квадрат расстояния до неё, координаты этой цели и её индекс в списке: 526
// дистанция: var dist := (mx - x) ** 2 + (my - y) ** 2; if dist < minDist then begin minDist := dist; xb := x; yb := y; id := i; end; end; Если в списке есть цели, то найдётся и ближайшая к Черепашке. Эту цель мы удаляем из списка, и список укорачивается на 1 цель. Черепашка отправляется к выбранной цели, а мы в заголовке окна получаем информацию – сколько целей собрала Черепашка: listTargets.RemoveAt(id); Turtle.MoveTo(xb,yb); gw.Title := $' Найдено целей: {nTargets - listTargets.Count}'; end; end; Запускаем программу и Черепашку. Рис. 1 показывает, что Черепашка ведёт себя не менее разумно, чем мы, если бы вдруг она отправила нас на сбор целей. 527
Рис. 1. Вполне разумно 528
Лабиринт Черепашка охотно и ловко ползает, поэтому в этом проекте мы запустим её в настоящий Лабиринт! Наша задача – построить Лабиринт надлежащих размеров, запустить туда Черепашку и водить её по запутанным коридорам до самого финиша этой книги. При большом желании вы можете научить Черепашку самостоятельно выбираться из Лабиринта, добавить к программе звуковые и прочие эффекты, но это уже слишком далеко от темы этой скромной книги. Начнём издаля – с главного блока программы: begin Prepare; // создаём Лабиринт: MakeLabyrinth; Turtle.Speed := 5; Turtle.Show; colTurtle := 1; rowTurtle := 1; Turtle.X := GetTurtleXY(1,1).x; Turtle.Y := GetTurtleXY(1,1).y; Turtle.Angle := 90; gw.PenColor := 'Blue'; end. В процедуре Prepare новинок не очень много: procedure Prepare(); begin gw.Hide(); gw.Title := 'Лабиринт'; gw.Width := GW_WIDTH; gw.Height := GW_HEIGHT; gw.Show(); 529
gw.Left := (Desktop.Width - gw.Width) / 2; gw.Top := (Desktop.Height - gw.Height) / 2; gw.BackgroundColor := 'DarkGreen'; Поскольку Лабиринт двумерный, то вполне разумно описать его двумерным массивом. Тип элементов массива может быть самым простым: // лабиринт: var Labyrinth : array [,] of integer; // массив Лабиринта: Labyrinth := new integer[nCol, nRow]; Размеры Лабиринта в клетках хранятся в глобальных переменных: // размеры Лабиринта в клетках по умолчанию: var nCol := 49; var nRow := 39; Для ручного управления Черепашкой нам нужна соответствующая процедура: // назначаем процедуру для обработки // события нажатия на клавишу: gw.KeyDown += OnKeyDown; end; Черепашка должна появиться в левом верхнем углу Лабиринта (Рис. 1). Эта её стартовая позиция отмечена красным цветом. Выход из Лабиринта находится в правом нижнем углу. Финишная клетка окрашена в зелёный цвет. Важно отметить, что Лабиринт со всех сторон окружён непроходимой стеной, так что покинуть его без динамита невозможно. Стены выкрашены в коричневый цвет, а проходы/коридоры – в белый. Наша Черепашка не настолько пробивная, чтобы проходить сквозь стены, поэтому должна передвигаться строго по белым клеткам. 530
Рис. 1. На старт! При каждом запуске программы создаётся новый Лабиринт, так что вы можете гонять Черепашку вечно. За случайность у нас отвечает переменная rand: 531
// генератор псевдослучайных чисел: var rand := new Random; Для этого мы подключаем к программе новые динамические библиотеки: {$reference SmallVisualBasicLibrary.dll} uses Microsoft.SmallVisualBasic.Library, System, System.Collections; type gw = GraphicsWindow; // размеры окна: const GW_WIDTH = 800; GW_HEIGHT = 640; Построение Лабиринта начинается с расстановки стен и пустых клеток: // СОЗДАЁМ НОВЫЙ ЛАБИРИНТ procedure MakeLabyrinth(); begin // всего пустых клеток в Лабиринте: var EmptyCells := 0; for var row := 0 to nRow-1 do for var col := 0 to nCol-1 do begin Labyrinth[col,row] := WALL; if (col*row mod 2 = 1) then begin Labyrinth[col,row] := EMPTY; EmptyCells += 1; end; end; Статус клеток мы обозначили константами: // клетки в Лабиринте --> // пустая клетка в лабиринте: 532
const EMPTY = 0; // свободная клетка (проход, коридор): const FREE = 1; // стена: const WALL = 15; После первого этапа строительства Лабиринт совершенно непроходим (Рис. 2). Теперь мы должны пробить проходы между соседними клетками так, чтобы получился Лабиринт, из которого можно выбраться. Для этого есть много алгоритмов, но нам достаточно самого простого алгоритма со стеком. Построение Лабиринта можно начинать с любой пустой клетки, потому что все свободные клетки будут связаны друг с другом проходами. Для разнообразия мы выбираем случайную пустую клетку и объявляем её свободной: // алгоритм построения лабиринта DFS // ---------------------------------// старт - в верхнем левом углу: var col := 1; var row := 1; // старт в произаольной клетке: repeat col := rand.Next(nCol-1) + 1; row := rand.Next(nRow-1) + 1; until (col * row mod 2 = 1); Labyrinth[col,row] := FREE; DrawLabyrinth; На экране свободные клетки окрашены в белый цвет (Рис. 3). Только по таким клеткам может ползать Черепашка. Для алгоритма нужен стек: 533
// очищаем стек: var st := new System.Collections.Stack(); Рис. 2. Выхода нет… 534
Рис. 3. Проходная клетка Кладём на стек первую свободную клетку: // добавляем в него первую клетку: st.Push(col*1000+row); 535
// число готовых клеток: var nDone := 1; Чтобы не мудрить со структурами данных, мы считаем, что на стеке лежат целые числа, поэтому упаковываем 2 координаты клетки в одно число. Сейчас мы находимся в свободной клетке, и хотим пробить стену в соседнюю пустую клетку. Для этого мы ищем возможные направления для продолжения прохода. Все направления запоминаем в массиве Direction: // направления из текущей клетки: var Direction := new integer[5]; // пробиваем проходы между пустыми клетками: while (nDone < EmptyCells) do begin // считаем стены возле текущей клетки, // после которых есть ещё не посещённая клетка --> var cr := integer(st.Peek()); // число стен: var nWall := 0; // сверху: col := cr div 1000; row := cr mod 1000 - 1; if (row > 1) and (Labyrinth[col,row] = WALL) and (Labyrinth[col, (row-1)] = EMPTY) then begin nWall += 1; Direction[nWall] := NORTH; end; // справа: col := cr div 1000 + 1; row := cr mod 1000; if (col < nCol-2) and (Labyrinth[col,row] = WALL) and (Labyrinth[col+1, row] = EMPTY) then begin nWall += 1; Direction[nWall] := EAST; end; // снизу: 536
col := cr div 1000; row := cr mod 1000 + 1; if (row < nRow-2) and (Labyrinth[col,row] = WALL) and (Labyrinth[col,(row+1)] = EMPTY) then begin nWall += 1; Direction[nWall] := SOUTH; end; // слева: col := cr div 1000 - 1; row := cr mod 1000; if (col > 1) and (Labyrinth[col,row] = WALL) and (Labyrinth[col-1,row] = EMPTY) then begin nWall += 1; Direction[nWall] := WEST; end; Направления движения в Лабиринте обозначаем константами: // направления движения в Лабиринте: const NORTH = 0; const EAST = 1; const SOUTH = 2; const WEST = 3; Если у свободной клетки есть стены, а за ними свободная клетка, то мы выбираем случайное направление: // есть стены: if (nWall > 0) then begin nDone += 1; // выбираем случайное направление: var rndDir := rand.Next(nWall)+1; В зависимости от выбранного направления мы пробиваем стену. Клетка с бывшей стеной и следующая за ней пустая клетка становятся свободными: 537
// идём наверх: if (Direction[rndDir] = NORTH) then begin col := cr div 1000; row := cr mod 1000 - 1; Labyrinth[col,row] := FREE; Labyrinth[col,(row-1)] := FREE; DrawCell(col,row, FREE_COLOR); DrawCell(col,row-1,FREE_COLOR); Мы закрашиваем их белым цветом и запоминаем координаты второй из новых свободных клеток на стеке, чтобы потом идти из неё дальше: st.Push(col*1000 + row-1); end // идём направо: else if (Direction[rndDir] = EAST) then begin col := cr div 1000 + 1; row := cr mod 1000; Labyrinth[col,row] := FREE; Labyrinth[col+1,row] := FREE; DrawCell(col,row,FREE_COLOR); DrawCell(col+1,row,FREE_COLOR); st.Push((col+1)*1000 + row); end // идём вниз: else if (Direction[rndDir] = SOUTH) then begin col := cr div 1000; row := cr mod 1000 + 1; Labyrinth[col,row] := FREE; Labyrinth[col, row+1] := FREE; DrawCell(col,row, FREE_COLOR); DrawCell(col,row+1, FREE_COLOR); st.Push(col*1000 + row+1); end // идём влево: else begin col := cr div 1000 - 1; row := cr mod 1000; Labyrinth[col,row] := FREE; 538
Labyrinth[col-1,row] := FREE; DrawCell(col,row, FREE_COLOR); DrawCell(col-1,row, FREE_COLOR); st.Push((col-1)*1000 + row); end end Если у текущей свободной клетки нет стен, то мы извлекаем со стека предпоследнюю свободную клетку, чтобы попытаться продвинуться из неё дальше: // нет стен: else st.Pop(); end; // end Loop Рано или поздно все пустые клетки превратятся в свободные, и на этом построение Лабиринта закончится: DrawCell(1,1, 'Red'); DrawCell(nCol-2,nRow-2, 'Lime'); // Лабиринт построен: //gw.ShowMessage('Лабиринт готов!', 'Лабиринт'); end; К этому времени весь Лабиринт уже должен быть нарисован на экране, и мы должны запрограммировать этот процесс. Весь Лабиринт состоит из клеток. Очень удобно рисовать каждую клетку в отдельной процедуре DrawCell: // РИСУЕМ ОДНУ КЛЕТКУ procedure DrawCell(col, row: integer; color: string); begin // col, row - координаты клетки в Лабиринте // сolor - её цвет 539
// координаты верхнего левого угла клетки: var pos := GetXY(col, row); var x := pos.x; var y := pos.y; gw.BrushColor := color; gw.FillRectangle(x, y, CELL_WIDTH, CELL_HEIGHT); end; Опять же для удобства координаты каждой клетки и Черепашки вычисляем в отдельных процедурах: // ВОЗВРАЩАЕТ ПИКСЕЛЬНЫЕ КООРДИНАТЫ КЛЕТКИ function GetXY(col, row: integer) : Point; begin var offsetX := (GW_WIDTH - (nCol+1) * CELL_WIDTH) / 2; var offsetY := (GW_HEIGHT - (nRow+1) * CELL_HEIGHT) / 2; // координаты верхнего левого угла клетки: var xPosition := offsetX + CELL_WIDTH div 2 + CELL_WIDTH * col; var yPosition := offsetY + CELL_HEIGHT div 2 + CELL_HEIGHT * row; Result := new Point(xPosition, yPosition); end; // ВОЗВРАЩАЕТ ПИКСЕЛЬНЫЕ КООРДИНАТЫ ЧЕРЕПАШКИ function GetTurtleXY(col, row: integer) : Point; begin var x := GetXY(col,row).x + CELL_WIDTH div 2; var y := GetXY(col,row).y + CELL_HEIGHT div 2; Result := new Point(x, y); end; Размеры и цвета клеток храним в константах: // размеры клеток: const CELL_WIDTH = 16; const CELL_HEIGHT = 16; // цвета клеток --> // цвет стен: const WALL_COLOR = 'Brown'; 540
//цвет проходов: const FREE_COLOR = 'White'; //цвет пустых клеток: const EMPTY_COLOR = 'Yellow'; Теперь мы без труда нарисуем и весь Лабиринт целиком: // РИСУЕМ ЛАБИРИНТ НА ЭКРАНЕ procedure DrawLabyrinth(); begin // число клеток в Лабиринте: var nCell := nRow*nCol; for var i := 0 to nCell-1 do begin var row := i div nCol; var col := i - row * nCol; if (Labyrinth[col, row] = WALL) then DrawCell(col, row, WALL_COLOR) else if (Labyrinth[col, row] = FREE) then DrawCell(col, row, FREE_COLOR) else if (Labyrinth[col, row] = EMPTY) then DrawCell(col, row,EMPTY_COLOR); end; end; Лабиринт готов к эксплуатации, а Черепашка заняла своё место на старте. Теперь пришёл наш черёд нажимать клавиши со стрелками: // НАЖИМАЕМ КЛАВИШУ procedure OnKeyDown(); begin var dir := gw.LastKey.ToString; case dir of 'Right': TurtleMoveRight; 'Left': TurtleMoveLeft; 'Up': TurtleMoveUp; 'Down': TurtleMoveDown; end; end; 541
Свойство LastKey хранит данные о последней нажатой клавише. Клавиши со стрелками имеют вполне понятные названия и вызывают вполне понятные процедуры. В этих процедурах мы, в первую очередь, проверяем, допустим ли правилами игры ход в указанном направлении. Если Черепашка уже упёрлась в стену, то она так и останется на своём месте: // ХОД ЧЕРЕПАШКИ procedure TurtleMoveRight; begin var newCol := colTurtle + 1; if Labyrinth[newCol, rowTurtle] = WALL then exit; Если Черепашка может дать ходу, то мы вычисляем её новые координаты и отсылаем её куда поближе: colTurtle := newCol; var xy := GetTurtleXY(colTurtle, rowTurtle); Turtle.MoveTo(xy.x, xy.y); IsGameOver; end; procedure TurtleMoveLeft; begin var newCol := colTurtle - 1; if Labyrinth[newCol, rowTurtle] = WALL then exit; colTurtle := newCol; var xy := GetTurtleXY(colTurtle, rowTurtle); Turtle.MoveTo(xy.x, xy.y); После каждого хода Черепашки проверяем, не закончилась ли игра: IsGameOver; end; 542
procedure TurtleMoveUp; begin var newRow := rowTurtle - 1; if Labyrinth[colTurtle, newRow] = WALL then exit; rowTurtle := newRow; var xy := GetTurtleXY(colTurtle, rowTurtle); Turtle.MoveTo(xy.x, xy.y); IsGameOver; end; procedure TurtleMoveDown; begin Println('TurtleMoveDown'); var newRow := rowTurtle + 1; if Labyrinth[colTurtle, newRow] = WALL then exit; rowTurtle := newRow; var xy := GetTurtleXY(colTurtle, rowTurtle); Turtle.MoveTo(xy.x, xy.y); IsGameOver; end; Наша незамысловатая игра заканчивается в единственном случае – когда Черепашка доберётся до финишной клетки в правом нижнем углу Лабиринта: // ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ ЛИ ИГРА procedure IsGameOver(); begin if (colTurtle = nCol - 2) and (rowTurtle = nRow - 2) then begin gw.Title := 'ИГРА ЗАКОНЧЕНА!'; end; end; В отличие от подслеповатой Черепашки мы видим весь Лабиринт полностью с высоты человеческого взора и разума, поэтому блуждания по Лабиринту ограничиваются только нашей наблюдательностью и прозорливостью. Конечно и естественно, каждый любитель прекрасного и программирования, добравшийся вместе с Черепашкой до финиша, тот ещё проходимец в Лабиринте (Рис. 4). 543
Рис. 4. ПОБЕДА БУДЕТ ЗА НАМИ! 544
Задания для самостоятельного решения Черепашка Напишите такие программы для Черепашки, чтобы она построила следующие фигуры (Рис. 1). Рис. 1. Черепашьи фигуры Черепашка Tortoise Какую фигуру вычертит Черепашка, выполняя следующий код: Tortoise.Move(100); Tortoise.Turn(90); Tortoise.Move(100); 545
А такой код: for var i := 1 to 4 do begin Tortoise.Move(100); Tortoise.Turn(90); end; Полярные кривые Постройте другие кривые в полярных координатах, например, овалы Кассини. Поэкспериментируйте с различными значениями параметров, входящих в формулу. 546
Справочник Здесь вы найдёте полезную информацию о классах библиотеки Microsoft.SmallBasic.Library, которую мы использовали в наших проектах. Таблица <Класс Desktop> Класс Desktop предоставляет методы для определения размеров Рабочего стола. Класс находится в пространстве имён Microsoft.SmallBasic.Library. Методы Описание Width()  integer Возвращает ширину Рабочего стола в пикселях. Height()  integer Возвращает высоту Рабочего стола в пикселях. SetWallPaper()  string fileOrUrl Заменяет текущие обои картинкой, путь к которой задан строкой fileOrUrl. Таблица <Класс GraphicsWindow> Класс GraphicsWindow предоставляет свойства и методы для работы с Графическим окном. Класс находится в пространстве имён Microsoft.SmallBasic.Library. Свойства Описание 547
BackgroundColor  string  string Устанавливает или возвращает цвет фона Графического окна. BrushColor  string  string Устанавливает или возвращает цвет заливки фигур и текста. PenColor  string  string Устанавливает или возвращает цвет контура фигур и линий. PenWidth  double  double Устанавливает или возвращает толщину контура фигур и линий. CanResize  boolean  boolean Если значение свойства равно true, то размеры окна приложения допускается изменять. Если значение свойства равно false, то размеры окна приложения изменять нельзя. FontBold  boolean  boolean Если значение свойства равно true, то надписи выводятся жирным шрифтом. Если значение свойства равно false, то обычным шрифтом. FontItalic  boolean  boolean Если значение свойства равно true, то надписи выводятся наклонным шрифтом. Если значение свойства равно false, то прямым шрифтом. FontName  string Устанавливает или возвращает название текущего шрифта. 548
 string FontSize  integer  integer Устанавливает или возвращает размер текущего шрифта. Height  integer  integer Устанавливает или возвращает высоту Графического окна. Width  integer  integer Устанавливает или возвращает ширину Графического окна. Left  integer  integer Устанавливает или возвращает x-координату левого верхнего угла Графического окна. Top  integer  integer Устанавливает или возвращает y-координату левого верхнего угла Графического окна. Title  string  string Устанавливает или возвращает заголовок Графического окна. LastKey  string Возвращает название последней нажатой клавиши. LastText  string Возвращает символ последней нажатой клавиши. 549
MouseX  integer Возвращает x-координату курсора мышки в Графическом окне. MouseY  integer Возвращает y-координату курсора мышки в Графическом окне. Методы Описание Clear() Стирает Графическое окно, заливая его цветом фона. DrawText()  double х  double y  string text Печатает текст text текущим шрифтом, начиная с указанных координат (x,y) верхнего левого угла описанного прямоугольника. DrawBoundText()  double х  double y  double width  string text Печатает текст text текущим шрифтом, начиная с указанных координат (x,y) верхнего левого угла описанного прямоугольника, ширина которого равна width. DrawEllipse()  double х  double y  double width  double height Чертит эллипс с осями width и height. Координаты верхнего левого угла описанного прямоугольника находятся в заданной точке (x,y). Цвет символов определяется текущим значением свойства BrushColor. Цвет символов определяется текущим значением свойства BrushColor. 550
Цвет контура определяется текущим значением свойства PenColor. Толщина контура определяется текущим значением свойства PenWidth. DrawImage()  string imageName  double х  double y Печатает заданную картинку imageName так, что координаты её верхнего левого угла находятся в заданной точке (x,y), а размеры не изменяются. DrawResizedImage()  string imageName  double х  double y  double width  double height Печатает заданную картинку imageName так, что координаты её верхнего левого угла находятся в заданной точке (x,y), а размеры - width х height DrawLine()  double х1  double y1  double х2  double y2 Чертит отрезок прямой с началом в точке (x1,y1) и концом в точке (x2,y2) . DrawRectangle()  double х  double y  double width  double height Чертит прямоугольник размером width х height. Координаты верхнего левого угла прямоугольника находятся в точке (x,y). Цвет линии определяется текущим значением свойства PenColor. Толщина линии определяется текущим значением свойства PenWidth. Цвет контура определяется текущим значением свойства PenColor. Толщина контура определяется текущим значением свойства PenWidth. 551
DrawTriangle()  double х1  double y1  double х2  double y2  double х3  double y3 Чертит треугольник с заданными координатами вершин (x1,y1), (x2,y2) и (x3,y3). FillEllipse()  double х  double y  double width  double height Рисует эллипс с осями width и height. Координаты верхнего левого угла описанного прямоугольника находятся в заданной точке (x,y). FillRectangle()  double х  double y  double width  double height Рисует прямоугольник размером width х height. Координаты верхнего левого угла прямоугольника находятся в точке (x,y). FillTriangle()  double х1  double y1  double х2  double y2  double х3  double y3 Рисует треугольник с заданными координатами вершин (x1,y1), (x2,y2) и (x3,y3). GetColorFromRGB()  double red Возвращает цвет, заданный цветовыми составляющими red, green, blue. Цвет контура определяется текущим значением свойства PenColor. Толщина контура определяется текущим значением свойства PenWidth. Цвет заливки определяется текущим значением свойства BrushColor. Цвет заливки определяется текущим значением свойства BrushColor. Цвет заливки определяется текущим значением свойства BrushColor. 552
 double green  double blue  string GetRandomColor()  string Возвращает случайный цвет. GetPixel()  integer х  integer y  string Возвращает цвет пикселя с координатами (x,y). SetPixel()  integer х  integer y  string color Окрашивает пиксель с координатами (x,y) в цвет color. Hide() Скрывает Графическое окно. Show() Показывает Графическое окно. ShowMessage()  string text  string title Показывает диалоговое окно с кнопкой ОК и сообщением text. События Заголовок окна определяется значением параметра title. Описание KeyDown Возникает при нажатии на клавишу. KeyUp Возникает при отпускании клавиши. 553
MouseDown Возникает при нажатии на кнопку мышки. MouseUp Возникает при отпускании кнопки мышки. MouseUp Возникает при перемещении мышки. TextInput Возникает при вводе текста с клавиатуры. Таблица <Класс Timer> Класс Timer предоставляет свойства и методы для работы с системными часами. Класс находится в пространстве имён Microsoft.SmallBasic.Library. Свойства Interval  integer interval  integer Описание Устанавливает заданный интервал interval в миллисекундах между срабатываниями (тиками) таймера. Возвращает интервал в миллисекундах между срабатываниями таймера. Методы Описание Pause() Приостанавливает работу таймера. Resume() Возобновляет работу таймера. События Описание 554
Tick Возникает при каждом срабатывании (тике) таймера. Таблица <Класс Turtle> Статический класс Turtle предоставляет свойства и методы для работы с Черепашкой. Класс находится в пространстве имён Microsoft.SmallBasic.Library. Свойства Angle  double angle  double Описание Устанавливает заданный угол angle в градусах. Положительные углы отсчитываются против часовой стрелки, отрицательные – по часовой стрелке. При значении свойства Angle = 0 Черепашка смотрит вверх. Возвращает угол поворота Черепашки в градусах. X  double x  double Устанавливает Черепашку в точку канвы, с x-координатой, равной x. Y  double y  double Устанавливает Черепашку в точку канвы, с y-координатой, равной y. Возвращает x-координату Черепашки в пикселях. Возвращает y-координату Черепашки в пикселях. 555
Speed  integer speed  integer Устанавливает относительную скорость перемещения Черепашки в диапазоне от 1 (очень медленно) до 10 (очень быстро). Возвращает относительную скорость перемещения Черепашки. Методы Описание Hide() Скрывает Черепашку. Show() Показывает Черепашку. Move() Перемещает Черепашку на заданное расстояние distance  double dis- в пикселях в направлении текущего поворота Черепашки tance (вперёд). MoveTo()  double x  double y Перемещает Черепашку в точку канвы, заданную координатами (x,y). TurnLeft() Поворачивает Черепашку на 90 градусов против часовой стрелки (влево) относительно текущего положения. TurnRight() Поворачивает Черепашку на 90 градусов по часовой стрелке (вправо) относительно текущего положения. Turn()  double angle Поворачивает Черепашку на заданный угол angle в градусах относительно текущего положения. 556
Если угол angle положительный, то Черепашка поворачивается вокруг своей оси против часовой стрелки, если отрицательный – по часовой стрелке. PenDown() Опускает карандаш – Черепашка при перемещении чертит линии. PenUp() Поднимает карандаш – Черепашка при перемещении не чертит линии. Таблица <Класс Tortoise> Статический класс Tortoise предназначен для управления Черепашкой. Класс находится в библиотеке SmallBasicFun. Методы Описание GetAngle() double Возвращает текущий угол поворота Черепашки. GetPenColor() string Возвращает цвет линий. GetPenWidth() double Возвращает толщину линий в пикселях. GetSpeed() integer Возвращает текущую скорость перемещения Черепашки. 557
GetX() double Возвращает x-координату Черепашки. GetY() double Возвращает y-координату Черепашки. Hide() Скрывает Черепашку. InstantSpeed()  boolean goFast Устанавливает максимально возможную скорость Черепашки, если значение параметра goFast равно true. Show() Показывает Черепашку на экране. Move()  double distance Перемещает Черепашку на заданное расстояние distance в пикселях в направлении текущего поворота Черепашки (вперёд). MoveTo()  double x  double y Перемещает Черепашку в точку канвы, заданную координатами (x,y). Reset() Создаёт Черепашку с параметрами по умолчанию. SetAngle()  double angle Поворачивает Черепашку в заданное направление. SetOrientation()  int x  int y Одновременно перемещает Черепашку в точку (x,y) и поворачивает её в заданное направление. Если angle = 0, то Черепашка смотрит вверх. Если angle = 180, то Черепашка смотрит вниз. 558
 double angle SetPenColor()  string color Устанавливает цвет линий. SetPenWidth()  double width Устанавливает толщину линий в пикселях. SetPosition()  integer x  integer y Переносит Черепашку в указанную точку (x,y). SetSpeed()  integer speed Устанавливает относительную скорость перемещения Черепашки в диапазоне от 1 (очень медленно) до 10 (очень быстро). SetX()  double x Задаёт Черепашке x-координату. SetY()  double y Задаёт Черепашке y-координату. Turn()  double angle Поворачивает Черепашку на заданный угол angle в градусах относительно текущего положения. Если угол angle положительный, то Черепашка поворачивается вокруг своей оси против часовой стрелки, если отрицательный – по часовой стрелке. PenDown() Опускает карандаш – Черепашка при перемещении чертит линии. 559
PenUp() Поднимает карандаш – Черепашка при перемещении не чертит линии. 560
Литература [ГМ93] Гарднер Мартин От мозаик Пенроуза к надёжным шифрам М.: Мир, 1993. – 417 с. ISBN 5-03-001991-Х [ММ05] Максим Мозговой Занимательное программирование Питер, 2005. – 208 с. Броуновское движение – стр. 20-23. Черепашья графика – стр. 45 -48. 561
[HR81] [HR74] Ross Honsberger Mathematische Edelsteine der elementaren Kombinatorik, Zahlentheorie und Geometrie Friedr. Vieweg & Sohn - Braunschweig; Wiesbadeo: Vieweg, 1981. Ross Honsberger Mathematical Gems I: The Dolciani Mathematical Expositions from elementary combinatorics, number theory, and geometry The Mathematical Assotiation of America, 1974. - 176 c. 562