Генерация деревьев на HTML5 Canvas

в 15:00, , рубрики: canvas, javascript, деревья, кто читает тэги?, природа, метки: , , , ,

Здравствуй Хабр!
Сегодня я хочу рассказать о генерации деревьев на HTML5 Canvas с помощью JavaScript. Сразу поясню, что речь идет не о деревьях ссылок или B-дереьях, а о тех деревья, которые мы каждый день видим у себя за окном, тех, которые делают наш воздух чище и богаче кислородом, тех, что желтеют осенью и теряют листья зимой, вообщем о тех самых живых, лесных, настоящих деревьях, только нарисованных на Canvas и пойдет речь.

Генерация деревьев на HTML5 Canvas
Такие вот деревья получаются

Генерация деревьев нужна была мне для моей игры. Но некаких адекватных алгоритмов мне найти так и не удалось. Поэтому я написал свой генератор…
Не хочу ничего читать, хочу сразу результат!

И так, что под копотом?

Все работает пресловутых кривых Безье, благодаря им ствол и ветви получаются округленными и кажутся жывыми. Я перепробовал множество способов, на самым производительным и простым оказалось именно использование кривых. Их легко строить, легко добиться от них нужного направления, а также можно рассчитать их траекторию программно.

Структура генератора такова:

Генерация деревьев на HTML5 Canvas

Класс TreeGenerator(на схеме Generator) использует объект класса Branch для генерации ветвей и ствола, для генерации листьев TreeGenerator вызывает метод из Drawer. Branch — это абстракция предоставляющая каждую ветку, как объект. Он тоже использует Drawer для отрисовки.

Шаг 1. Генерация ветви

Класс Drawer — это прослойка между canvas API и Branch. Этот класс выполняет отрисовку листьев и ветвей по заданным параметрам.
А вот и функция по рисованию ветви из Drawer:

//x и y - координаты начала ветви, leng - длинна ветви, w - ширина, deform - кривизна ветви, rotate - поворот
DrawStick = function(x, y, leng, w, deform, rotate) {

	//Сохраняем canvas и применяем трансформацию.   
	this.c.save();
	this.c.translate(x, y);
	this.c.rotate(rotate.degree()); //Метод degree переводит градусы в радианы 

	//Обнуляем x и y точки поворота. 
	x = 0;
	y = w / -2;

	//Рисуем веку из кривых Безье и прямых
	this.c.beginPath();

	//перемещаемся в  x и y
	this.c.moveTo(x, y);

	//Рисуем первую кривую со сгибом в середине от x y до начала верней линии(имеющей ширину w/BRANCH_CONSTRICTION)
	this.c.bezierCurveTo(x, y, x + leng / 2, y + deform, x + leng, y + (w - (w / BRANCH_CONSTRICTION)) / 2);
	//Рисуем линию до второй кривой
	this.c.lineTo(x + leng, y + w / BRANCH_CONSTRICTION + (w - (w / BRANCH_CONSTRICTION)) / 2);

	//Рисуем вторую кривую обратную первой
	this.c.bezierCurveTo(x + leng, y + w / BRANCH_CONSTRICTION + (w - (w / BRANCH_CONSTRICTION)) / 2, x + leng / 2, y + w + deform, x, y + w);

	//Линия в начало
	this.c.lineTo(x, y);

	this.c.closePath();

	//Рисуем круг, чтобы сгладить ветку вверху  
	this.c.arc(x + leng, y + w / BRANCH_CONSTRICTION / 2 + (w - (w / BRANCH_CONSTRICTION)) / 2, w / BRANCH_CONSTRICTION / 2, 0 * Math.PI, 2 * Math.PI, false);

	//Закрашиваем ветку 
	this.c.fillStyle = BRANCH_COLOR;
	this.c.fill();

	//Восстанавливаем canvas
	this.c.restore();
}

Из кода, вы наверное не поняли как задаются верхние точки ветви. Как известно, ветвь дерева в конце немного уже, чем в начале. Насколько уже, задается константой BRANCH_CONSTRICTION. По умолчанию она равна 1.5. BRANCH_COLOR — задает цвет. Значение выбирается рандомом из массива цветов.

Результатом этой функции станет примерно такая ветка:

Генерация деревьев на HTML5 Canvas

Скажу честно, пока это не очень похоже на то, что нам нужно. Поэтому, поехали дальше!

Шаг 2. Генерация дерева из веток

Если вы когда нибудь присматривались к маленьким, только проросшим из семени деревцам, то могли заметить, что они представляют собой одну единственную ветку с листиками, затем из этой ветки прорастают другие, а она сама тоже растет и расширяется. К чему это я? А к тому, что ствол — это по сути дела и есть одна большая ветка из которой врастают другие ветки, а из этих веток еще веки, и так далее…

Исходя из этого, нам будет удобнее представлять каждую ветку, ввиде объекта, который будет хранить параметры и методы и информацию об отростках и ответвлениях. Для этого, как я уже говорил, класс Branch:

var Branch = function(x, y, leng, width, deformation, rotate) {

	this.params = {
		x: x,
		y: y,
		leng: leng,
		width: width,
		deformation: deformation,
		rotate: rotate,
	};

	this.parent = null; //родительский объект, те ветвь, от которой отросла дананная
	this.children = []; //дети объекта, то есть отростки. 

        //Рисование ветки на canvas из параметров
	this.render = function() {
		drawer.DrawStick(this.params.x,
			this.params.y,
			this.params.leng,
			this.params.width,
			this.params.deformation,
			this.params.rotate);

	}
        
        //Получение конечных точек ветви для создания отростка
        this.getEndPoints = function() {
		var ex = this.params.x + this.params.leng * Math.cos(this.params.rotate.degree()),
			ey = this.params.y + this.params.leng * Math.sin(this.params.rotate.degree());

		return [ex, ey];
	}
	
        //Создание отростков, растущих из конца ветви
	this.createChild = function(leng, width, deform, rotate) {
		var exy = this.getEndPoints(); //Получение конечных
		
		//Создание новой ветки из конца данной и помещаем её в children 
		this.children.push(new Branch(exy[0], exy[1], leng, width, deform, rotate));

		//Записываем в созданную ветку информацию
		this.children[this.children.length - 1].parent = this;

		return this.children[this.children.length - 1];
	}


	this.render(); //Вызываем функцию для рандиринга
}

Испробуем новый класс. Вызавем:

new Branch(100,300,200,20,-60,-50).createChild(100,20/1.5,30,-60);

Результат будет таким:
Генерация деревьев на HTML5 Canvas

Ну что-ж, отдаленно напоминает ветку, правда? Однако деревья всё время разветвляются и тянутся к солнцу. Для создания нам понадобится своебразная надстройка на createChild, функция createDivarication. В природе, чаще всего встречаются разделения ветви на 2 отростка, один из которых основной, он следовательно толще, а второй тоньше. В результате некоторых тестов и подборов, я понял, что лучшее отношенее 1.9:1.4. Вы можете использовать другое отношение для своих деревьев.
А вот и код функции createDivarication:

// branches - массив c параметрами вида [{leng:,deform:,rotate:},{}] main - индекс основной ветки
createDivarication = function(branches, main) {
        //Считаем половину ширины 
	var wi = this.params.width / BRANCH_CONSTRICTION / 2;
	for (var i = 0; i < 2; i++) {
		bi = branches[i];
                //Создаём ветвь с параметрами из branches и применяем отношение о котором я писал выше 
		this.createChild(bi.leng, (i == main) ? (1.9 * wi) : wi * 1.4, 
			bi.deform,
			this.params.rotate - bi.rotate //Находим поворот ветви относительно родительской
		);
	}

	return this.children;

}

Вот что у нас получилось:

Генерация деревьев на HTML5 Canvas

Ну вручную нарисовать и так можно, а нужны то нам случайные деревья. Именно для этого и предназначен класс TreeGenerator. Вот и он собственной персоной:

var TreeGenerator = function(){
       
        //Генерация развилок из ветки - branch
	this.genF=function(branch) {
		if (branch.params.width > 1) { //ветви должны быть не тоньше еденицы

			var divarications = [], //Массив для параметров веток 
				dfm = BRANCH_DEFORMATION * branch.params.width / branch.params.leng; //Рассчитываем максимальные пораметры для кривизны ветви

			//Генерируем параметры для веток
			for (var di = 0; di <= 2; di++) {

				divarications.push({
					leng: rand(40, branch.params.leng), //Длинна не длжна привышать длинну родительской ветки
					deform: rand(-dfm, dfm), //Генерация случайной деформации с учетом допустимой
					rotate: (di == 0) ? (rand(-20, -10)) : (rand(10, 20)) //генерация уклонав зависимости от стороны
				});
			}

			//Создание ветки из параметров
			var chld = branch.createDivarication(divarications, Math.floor(rand(0, 2)));

			//Создание ответвлений от новых веток
			for (var ci = 0; ci < 2; ci++) {

				this.genF(chld[ci]);
			}


		} else {
                       //Сдесь будет дополнительный код, о котором пойдет речь в следующих шагах
		}



	}
        
        //Основная функция генератора, запускает генерацию всегодерева
	this.genT=function(x,y){ 
                //Создание ствола дерева с учетом максимальной толщины и ширины 
		var mainTreeBranch = new Branch(x, y, rand(70, BRANCH_MAXLENGTH), rand(10, BRANCH_MAXWIDTH), rand(-40, 40), rand(-120, -70)); 
                //Запускаем рекурсивный генератор развилок
		this.genF(mainTreeBranch);
		//Рисуем полукруг(землю) под деревом 
                drawer.DrawHill(x,y+20);

		return mainTreeBranch;
	}
}

Читая код вы наверное заметили новые константы BRANCH_DEFORMATION — деформация(кривизна) веток (не ствола), BRANCH_MAXLENGTH — максимальная длинна ствола и BRANCH_MAXWIDTH — ширина ствола. В деформации веток также играет роль их толщина по отношению к ширине, чем тоньше ветка тем меньше в итоге девормация, ведь она изначально задается в пикселях. Что касается длинны, то ветвь не может быть длинее той, от которой отросла. Я не стал показывать код функции DrawHill так как она состоит всего из шести строк, и рисует полукруг в точке x и y.

Ну что-ж, самое время опробовать генератор. Вызвав функцию genT с нужными параметрами, получим примерно такой результат:

Генерация деревьев на HTML5 Canvas

Согласитесь, дерево то, растёт! На этом можно было бы поставить точку и радоваться тёмным силуэтам деревьев, тем более, если учитывать, что сейчас зима и за окном деревья ничуть не лучше, а порой и намного хуже, однако, я не остановится и продолжил совершенствовать генератор, чтобы сделать деревья ещё более живыми и интересными. Если вы со мной, то нам пора к следующему пункту.

Шаг 3. Генерация отростков от веток

Когда деревья стоят без листвы можно заметить что они состоят не только из лаконичных, толстых ветвей, вырастающих из вершин таких же ветвей, но и из тех маленьких и не очень веточек которые отрастают от основных в произвольных местах. Делают они это для того для того, чтобы у дерева стало больше листвы, ведь листва выполняет очень важные для нашей планеты вещи — испаряет влагу и преобразует углекислый газ в кислород. Эти веточки в данном посте будут называться отростками. По сути они тоже ветви(branch), как я и говорил растут из произвольных мест, а не только из верхушки. А ветки у нас на кривых Безье! Как же рассчитать, где будет находится отросток? В этом нам поможет сама формула кривых Безье:
image

На js — это будет вот так:

//Функция используется в классе Branch. pointPos позиция точки на кривой в процентах
getPointOnCurve = function(pointPos) {

	//Получаем вершину кривой с учетом поворота ветви
	var ex = this.params.x + this.params.leng / 2 * Math.cos((this.params.rotate + this.params.deformation).degree()),
		ey = this.params.y + this.params.leng / 2 * Math.sin((this.params.rotate + this.params.deformation).degree());

	//t - считаем t для формулы [0,1]
	t = pointPos / 100;

	//Находим координаты конца ветви
	ep = this.getEndPoints();
       
        //Начало и конец по x,y
	x = [this.params.x, ep[0]];
	y = [this.params.y, ep[1]];
       
        Вершина по x,y
	p1 = [ex, ey];

	//Строим кривю
	par1 = Math.pow((1 - t), 2) * x[0] + (1 - t) * 2 * t * p1[0] + Math.pow(t, 2) * x[1]; //по x
	par2 = Math.pow((1 - t), 2) * y[0] + (1 - t) * 2 * t * p1[1] + Math.pow(t, 2) * y[1]; //по y 

	return [par1, par2];

}

Кривая будет проходить по центру ветви. Визуально это будет вот так:

Генерация деревьев на HTML5 Canvas

А теперь пора и генерировать отростки: создадим в Branch новую функцию

//branches - массив объектов с параметрами в 
this.createOutgrowth = function(leng, width, pos, deform, rotate) {
	var startXY = this.getPointOnCurve(pos);
        
        //Записываем в outgrowths(массив отростков) новый отросток с заданными параметрами
	this.outgrowths.push(new Branch(startXY[0], startXY[1], leng, width, deform, this.params.rotate + rotate));

	return this.outgrowths.reverse()[0];
}

Также расширяем генератор:

this.genO = function(branch) {
	if (branch.params.width > 1) { //если ветвь шире 1
		var outgrowthsCount = rand(0, BRANCH_OUTGROWTH); //число отростков BRANCH_OUTGROWTH - макс. число отростков
		for (var io = 0; io < outgrowthsCount; io++) {
                        //Создаем отросток и тут же скармливаем его разветвителю
			this.genF(branch.createOutgrowth(rand(10, branch.params.leng), rand(1, branch.params.width), rand(1, 100), rand(-10, 10), rand(-40, 40)));

		}
	}
}

И расширим функцию genF, заменив это:

//Создание ответвлений от новых веток
for (var ci = 0; ci < 2; ci++) {
      this.genF(chld[ci]);
}

на вот это:

//Создание ответвлений от новых веток и создание отростков
for (var ci = 0; ci < 2; ci++) {
	if (OUTGROWTH_ISSHOWN) { //OUTGROWTH_ISSHOWN показывать ли отростки, по умолчанию true
		if (chld[ci].params.width < OUTGROWTH_BRANCH_WIDTH) { //OUTGROWTH_BRANCH_WIDTH -максимальная ширина ветви от которой идут отроски
			this.genO(chld[ci]); //Вызов генератора отростков
		}
	}

	this.genF(chld[ci]);
}

Опробуем? Вот такое стало дерево:

Генерация деревьев на HTML5 Canvas

Смотрится не очень красиво, не хватает листьев. Следующий шаг как раз о них.

Шаг 4. Генерация листьев

Листья неотъемлемая часть любого дерева(иголки тоже являются листьями, только девормированными для защиты от холода) и они слишком разные, чтобы генерировать их программно, поэтому мы будем брать один из пяти видов листьев, созданных в ручную. Листья рисовать тоже лучше всего на кривых Безье и хранить в массивах с конечными точками и точкой деформации кривой. Лист — сущность семеричная и нам стоит только нарисовать левый бок, а правы дополнится автоматически.
Для примера возьмем код простейшего листа:

[[ 
                        [100, 0], //Конечные точки( начало - вершина листа)
                        [70, 40] //Точка сгиба кривой
                ]],

Также рассмотрим функцию отрисовки из Drawer:

this.DrawLeaf = function(x, y, leafPoints, colors, scale, rotate) {

		//Сохраняем значения x и y
		lx = x;
		ly = y;

		//Создание двух половинок листа
		for (var io = 0; io < 2; io++) {
			this.c.save(); //Сохраняем канвас

			this.c.translate(x, y); //Сдвигаемся
			this.c.rotate((rotate).degree()); //Повороачиваем лист
			this.c.scale(scale, scale); //Изменяем до нужного размера

			if (io == 1) { //Отражаем вторую половинку лист 
				this.c.setTransform(-1, 0, 0, 1, x, y);
				this.c.scale(scale, scale);
				this.c.rotate((-180 - (rotate)).degree());
			}

			x = 100 / -2;
			y = 0;

			this.c.beginPath();
			this.c.moveTo(x, y);

			var lastPair = [0, 0]; //Первая точка - на верхушке листа
			for (var bi in leafPoints) { 
				var bp = leafPoints[bi];
 
                                //Кривая по параметрам
				this.c.bezierCurveTo(x + lastPair[0], y + lastPair[1], 
					x + bp[1][0], y + bp[1][1],
					x + bp[0][0], y + bp[0][1]);
                                //Сохраняем конечные точки для следующей линии
				lastPair = [bp[0][0], bp[0][1]];

			}
                       //Линия в конец листа
			this.c.lineTo(x + LEAF_LENG, y); // LEAF_LENG - длинна листа. По умолчанию 100
			this.c.closePath();
			this.c.fillStyle = colors[1]; //Красим лист
			this.c.fill();

			this.c.strokeStyle = colors[0]; //Кпасим линии листа
			this.c.stroke();

			this.c.restore();

			x = lx;
			y = ly;
		}


	}

А теперь пора пристроить это к генератору. Напишим функцию genL

this.genL = function(branch) {
	
		leafCount = branch.params.leng / (LEAF_LENG * LEAF_SCALE) * LEAF_DENSITY; //Считаем колличество лисов согласно: LEAF_SCALE - размера, LEAF_DENSITY - плотности листвы 


	for (var li = 1; li < leafCount; li++) { //
               
               var lxy=branch.getPointOnCurve(branch.params.leng/leafCount*li); //Находим точку крепления листа
                //Рисуем лист
		drawer.DrawLeaf(lxy[0],
			lxy[1],
			LeafMaps[LEAF_TYPE], ['#353', 'green'],
			LEAF_SCALE,
			branch.params.rotate - 180
		); 

	}
}

Прикрутим эту функцию к genF, заменив комментарий «Сдесь будет дополнительный код, о котором пойдет речь в следующих шагах» на вызов genL — вот так:

if(LEAF_ISSHOWN){ //LEAF_ISSHOWN - определяет, показывать ли листья. По умолчанию true
	this.genL(branch);
}

Ну вот и вырасло дерево!

Генерация деревьев на HTML5 Canvas

А вам, спасибо за внимание, весь код есть на GitHub,
результат моих трудов, можно пощупать тут

Автор: RAZVOR

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js