- PVSM.RU - https://www.pvsm.ru -
Здравствуй Хабр!
Сегодня я хочу рассказать о генерации деревьев на HTML5 Canvas с помощью JavaScript. Сразу поясню, что речь идет не о деревьях ссылок или B-дереьях, а о тех деревья, которые мы каждый день видим у себя за окном, тех, которые делают наш воздух чище и богаче кислородом, тех, что желтеют осенью и теряют листья зимой, вообщем о тех самых живых, лесных, настоящих деревьях, только нарисованных на Canvas и пойдет речь.
Такие вот деревья получаются
Генерация деревьев нужна была мне для моей игры. Но некаких адекватных алгоритмов мне найти так и не удалось. Поэтому я написал свой генератор…
Не хочу ничего читать, хочу сразу результат! [1]
Все работает пресловутых кривых Безье, благодаря им ствол и ветви получаются округленными и кажутся жывыми. Я перепробовал множество способов, на самым производительным и простым оказалось именно использование кривых. Их легко строить, легко добиться от них нужного направления, а также можно рассчитать их траекторию программно.
Структура генератора такова:
Класс TreeGenerator(на схеме Generator) использует объект класса Branch для генерации ветвей и ствола, для генерации листьев TreeGenerator вызывает метод из Drawer. Branch — это абстракция предоставляющая каждую ветку, как объект. Он тоже использует Drawer для отрисовки.
Класс 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 — задает цвет. Значение выбирается рандомом из массива цветов.
Результатом этой функции станет примерно такая ветка:
Скажу честно, пока это не очень похоже на то, что нам нужно. Поэтому, поехали дальше!
Если вы когда нибудь присматривались к маленьким, только проросшим из семени деревцам, то могли заметить, что они представляют собой одну единственную ветку с листиками, затем из этой ветки прорастают другие, а она сама тоже растет и расширяется. К чему это я? А к тому, что ствол — это по сути дела и есть одна большая ветка из которой врастают другие ветки, а из этих веток еще веки, и так далее…
Исходя из этого, нам будет удобнее представлять каждую ветку, ввиде объекта, который будет хранить параметры и методы и информацию об отростках и ответвлениях. Для этого, как я уже говорил, класс 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);
Результат будет таким:
Ну что-ж, отдаленно напоминает ветку, правда? Однако деревья всё время разветвляются и тянутся к солнцу. Для создания нам понадобится своебразная надстройка на 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;
}
Вот что у нас получилось:
Ну вручную нарисовать и так можно, а нужны то нам случайные деревья. Именно для этого и предназначен класс 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 с нужными параметрами, получим примерно такой результат:
Согласитесь, дерево то, растёт! На этом можно было бы поставить точку и радоваться тёмным силуэтам деревьев, тем более, если учитывать, что сейчас зима и за окном деревья ничуть не лучше, а порой и намного хуже, однако, я не остановится и продолжил совершенствовать генератор, чтобы сделать деревья ещё более живыми и интересными. Если вы со мной, то нам пора к следующему пункту.
Когда деревья стоят без листвы можно заметить что они состоят не только из лаконичных, толстых ветвей, вырастающих из вершин таких же ветвей, но и из тех маленьких и не очень веточек которые отрастают от основных в произвольных местах. Делают они это для того для того, чтобы у дерева стало больше листвы, ведь листва выполняет очень важные для нашей планеты вещи — испаряет влагу и преобразует углекислый газ в кислород. Эти веточки в данном посте будут называться отростками. По сути они тоже ветви(branch), как я и говорил растут из произвольных мест, а не только из верхушки. А ветки у нас на кривых Безье! Как же рассчитать, где будет находится отросток? В этом нам поможет сама формула [2] кривых Безье:
На 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];
}
Кривая будет проходить по центру ветви. Визуально это будет вот так:
А теперь пора и генерировать отростки: создадим в 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]);
}
Опробуем? Вот такое стало дерево:
Смотрится не очень красиво, не хватает листьев. Следующий шаг как раз о них.
Листья неотъемлемая часть любого дерева(иголки тоже являются листьями, только девормированными для защиты от холода) и они слишком разные, чтобы генерировать их программно, поэтому мы будем брать один из пяти видов листьев, созданных в ручную. Листья рисовать тоже лучше всего на кривых Безье и хранить в массивах с конечными точками и точкой деформации кривой. Лист — сущность семеричная и нам стоит только нарисовать левый бок, а правы дополнится автоматически.
Для примера возьмем код простейшего листа:
[[
[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);
}
Ну вот и вырасло дерево!
А вам, спасибо за внимание, весь код есть на GitHub [3],
результат моих трудов, можно пощупать тут [4]
Автор: RAZVOR
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/53752
Ссылки в тексте:
[1] Не хочу ничего читать, хочу сразу результат!: #Result
[2] формула: http://en.wikipedia.org/wiki/Bézier_curve
[3] GitHub: https://github.com/RAZVOR/TreeGenerator
[4] тут: http://razvor.github.io/TreeGenerator/
[5] Источник: http://habrahabr.ru/post/210614/
Нажмите здесь для печати.