Это мини-задание - лирическое отступление о том, как можно рисовать что-нибудь симпатичное и забавное благодаря удобной библиотеке.

Задание является адаптацией данного видео на тему использования библиотеки p5.js для рисования фрактальных деревье. Здесь есть перечень всех доступных функций.

Получить оценку за данное задание можно только на уроке.

0) Поднятие окружения

Скачайте архив библиотечки отсюда и разархивируйте его в какую-нибудь папку.

Там куда вы разархивировали библиотечку зайдите в подпапку p5-zip/empty-example. Откройте sketch.js любым текстовым редактором, например Intellij IDEA (в ней можно нажать File->Open и указать этот файл):

function setup() {

}

function draw() {
  
}

Первая функция исполняется в начале работы приложения, вторая функция - исполняется для отрисовки каждого кадра.

Простой рабочий пример может выглядеть так:

function setup() {
    createCanvas(640, 480);   // Указываем размер холста на котором будем рисовать
    background(50, 50, 100);  // Указываем цвет фона
}

function draw() {
    var x = 100;                           // Пример того, как можно объявить переменную, вместо типа перед названием переменной всегда пишется var - от слова variable (в переводе - переменная)
    var ellipseColor = color(255, 255, 0); // Создаем переменную хранящую цвет
    fill(ellipseColor);                    // Указываем цветом заливки - цвет из переменной ellipseColor
    ellipse(320, 240, 200, 100);           // Рисуем эллипс с центром в точке (320, 240) и размером 200x100
}

Чтобы увидеть результат - достаточно открыть p5-zip/empty-example/index.html в браузере (например дважды кликнув по этому файлу). По мере редактирования sketch.js чтобы увидеть новый результат - достаточно обновить страничку в браузере (Ctrl+R или F5).

1) Функции

Допустим мы хотим сделать функцию “нарисовать человечка”:

function setup() {
    createCanvas(640, 480);   // Указываем размер холста на котором будем рисовать
    background(50, 50, 100);  // Указываем цвет фона
}

function draw() {
    drawHuman(320, 240);      // Рисуем человечка вызовом функции, которая объявлена ниже
}

function drawHuman(x, y) {
    translate(x, y);          // Смещаем начало отсчета координат из текущего положения (верхний левый угол) на x пикселей вправо и y пикселей вниз
    ellipse(0, -40, 20, 20);  // Голова относительно текущей системы координат в точке (0, -40), т.е. на 40 пикселей выше чем (x, y) и с диаметром 20
    line(0, -30, 0, 30);      // Тело идет от нижней точки головы (0, -30) до точки (0, 30)
    line(0, -20, -20, -10);   // Рука слева
    // Доделайте эти вызовы:
    // line(...); // Рука справа
    // line(...); // Нога слева
    // line(...); // Нога справа
}

Доделайте функцию drawHuman, чтобы получился человечек.

Если добавить после вызова translate вызов rotate(PI / 4), то текущая система координат повернется на 45 градусов по часовой стрелке, а значит и дальнейшая отрисовка человечка в этой системе приведет к тому, что человечек повернулся на эти же 45 градусов по часовой стрелке.

Убедитесь что вы поняли, как работает translate и rotate. Что будет если отрисовка частей человечка не симметрична относительно точки (0, 0)? Как он будет крутиться в таком случае?

2) Фрактал

Давайте теперь нарисуем такое дерево-фрактал:

Fractal tree

Функция отрисовки такого дерева - рекурсивная, она должна делать следующее:

функция рисует корневую ветку длины N. В точке где кончается эта ветка функция вызывает отрисовку двух других деревьев:

  • Одно повернуто на 45 градусов по часовой стрелке с длиной первой ветки N/2
  • Второе повернуто на 45 градусов против часовой стрелки с длиной первой ветки N/2

2.1) Правые ветки фрактала

Давайте сначала нарисуем более простую вещь - загогулину-веточку:

Fractal tree

Функция отрисовки такой веточки тоже рекурсивная. Она делает следующее: отрисовывает веточку длины N, двигает систему координат в конец этой ветки, поворачивает систему координат на 45 градусов и вновь вызвает себя для отрисовки на этот раз в два раза более короткой веточки:

function setup() {
    createCanvas(640, 480);
    background(50, 50, 100);
}

function draw() {
    translate(320, 460);      // Сдвигаем систему координат так, чтобы точка отсчета совпадала с корнем дерева-веточки
    drawBranch(200);          // Провоцируем отрисовку, длина первой ветки должна быть 200
}

var coeff = 0.5;              // Эта константа используется ниже - это коэффициент, во сколько раз очередная ветка короче предыдущей

function drawBranch(length) {
    line(0, 0, 0, -length);         // Рисуем ветку переданной длины из центра текущей системы координат вверх данной длиной

    translate(0, -length);          // Двигаем систему координат так, чтобы точка отсчета совпала с концом нарисованной ветки
    rotate(PI / 4);                 // Поворачиваем систему координат
    if (length > 1) {
        drawBranch(length * coeff); // Провоцируем рекурсивную отрисовку более короткой ветки
    }
}

2.2) Интерактивная ветка фрактала

Давайте сделаем интерактивным изменение угла наклона каждой очередной ветки и то, во сколько раз она уменьшается:

Сначала надо открыть текстовым редактором index.html и заменить эту строчку:

  <!--<script language="javascript" src="libraries/p5.dom.js"></script>-->

На такую:

  <script language="javascript" src="libraries/p5.dom.js"></script>

Теперь мы сможем создать слайдер, который сможет интерактивно указывать значение градуса, на который каждый раз делается поворот.

Для этого достаточно доделать код до следующего:

var slider; // Переменная в которой хранится интерактивный объект-ползунок

function setup() {
    createCanvas(640, 480);
    background(50, 50, 100);
    slider = createSlider(0, 2 * PI, PI / 6, 0.01); // Создаем ползунок с аргументами: минимальное и максимальное значения, значение по умолчанию, и шаг изменения значения
}

function draw() {
    translate(320, 460);
    drawBranch(200);
}

var coeff = 0.5;

function drawBranch(length) {
    line(0, 0, 0, -length);

    translate(0, -length);
    rotate(slider.value()); // Вместо того чтобы всегда поворачивать на 45 градусов - поворачиваем на значение, которое в данный момент выбрано ползунком
    if (length > 1) {
        drawBranch(length * coeff);
    }
}

Теперь у нас есть ползунок, который указывает градус поворота от ветки к ветке.

Но есть проблема - старое положение всех веток оставляет след, который хотелось бы очистить - очистка происходит вызововм background, который у нас вызывается только в setup, так что достаточно перенести его вызов из setup в первую строчку метода draw.

2.3) Сам фрактал

Заметьте, что сам фрактал отличается от этой ветки лишь тем, что каждый очередной раз порождается не одна ветка - а две.

Это значит что нам надо вызвать рекурсивную функцию не один раз после поворота по часовой стрелке на 45 градусов, но два раза - один после поворота по часовой стрелке, второй - после поворота против часовой стрелки. Например так:

function drawFractal(length) {
    line(0, 0, 0, -length);

    translate(0, -length);
    
    if (length > 1) {
        rotate(PI / 2);
        drawFractal(length * coeff);

        rotate(-PI / 2);
        drawFractal(length * coeff);
    }
}

Но это внезапно не работает. Почему? Потому что когда мы делаем второй поворот системы координат - она уже модифицированна внутренними поворотами первого рекурсивного вызова, а нам хотелось бы восстановить изначальное (на момент текущего вызова функции) состояние системы координат. Для сохранения и восстановления состояния есть метод push и pop соответственно:

        push();                      // Сохраняем текущую систему координат
        rotate(PI / 2);
        drawFractal(length * coeff);
        pop();                       // Восстанавливаем последнее сохранение системы координат

        push();                      // Сохраняем
        rotate(-PI / 2);
        drawFractal(length * coeff);
        pop();                       // Восстанавливаем

2.4) Развитие фрактала

Теперь можно поиграть со следующими вещами:

  • Добавьте ползунок для определения значения coeff (коэффициент изменения длины ветки)
  • Как можно сделать дерево не симметричным? Например сделать поворот системы координат не на angle и -angle, а на angle+delta и -angle+delta.