Javascript
Квазіреалістичний повністю 3D-процедурний генератор ялинок.
Завдяки: розширена конфігурація з ще більшою кількістю параметрів конфігурації, наявних у коді; стовбур зигзаги; розгалуження гілок; анімація росту; обертання повністю вирощеного дерева.
Не містить: jQuery, Underscore.js або будь-яку іншу бібліотеку; апаратна залежність - потрібна лише підтримка полотна; безладний код (принаймні, таким був намір)
Жива сторінка: http://fiddle.jshell.net/honnza/NMva7/show/
Редагувати сторінку: http://jsfiddle.net/honnza/NMva7/
скріншот:
HTML:
<canvas id=c width=200 height=300 style="display:none"></canvas>
<div id=config></div>
Javascript:
var TAU = 2*Math.PI,
deg = TAU/360,
TRUNK_VIEW = {lineWidth:3, strokeStyle: "brown", zIndex: 1},
BRANCH_VIEW = {lineWidth:1, strokeStyle: "green", zIndex: 2},
TRUNK_SPACING = 1.5,
TRUNK_BIAS_STR = -0.5,
TRUNK_SLOPE = 0.25,
BRANCH_LEN = 1,
BRANCH_P = 0.01,
MIN_SLOPE = -5*deg,
MAX_SLOPE = 20*deg,
INIT_SLOPE = 10*deg,
MAX_D_SLOPE = 5*deg,
DIR_KEEP_BIAS = 10,
GROWTH_MSPF = 10, //ms per frame
GROWTH_TPF = 10, //ticks per frame
ROTATE_MSPF = 10,
ROTATE_RPF = 1*deg; //radians per frame
var configurables = [
// {key: "TRUNK_SPACING", name: "Branch spacing", widget: "number",
// description: "Distance between main branches on the trunk, in pixels"},
{key: "TRUNK_BIAS_STR", name: "Branch distribution", widget: "number",
description: "Angular distribution between nearby branches. High values tend towards one-sided trees, highly negative values tend towards planar trees. Zero means that branches grow in independent directions."},
{key: "TRUNK_SLOPE", name: "Trunk slope", widget: "number",
description: "Amount of horizontal movement of the trunk while growing"},
{key: "BRANCH_P", name: "Branch frequency", widget: "number",
description: "Branch frequency - if =1/100, a single twig will branch off approximately once per 100 px"},
{key: "MIN_SLOPE", name: "Minimum slope", widget: "number", scale: deg,
description: "Minimum slope of a branch, in degrees"},
{key: "MAX_SLOPE", name: "Maximum slope", widget: "number", scale: deg,
description: "Maximum slope of a branch, in degrees"},
{key: "INIT_SLOPE", name: "Initial slope", widget: "number", scale: deg,
description: "Angle at which branches leave the trunk"},
{key: "DIR_KEEP_BIAS", name: "Directional inertia", widget: "number",
description: "Tendency of twigs to keep their horizontal direction"},
{get: function(){return maxY}, set: setCanvasSize, name: "Tree height",
widget:"number"}
];
var config = document.getElementById("config"),
canvas = document.getElementById("c"),
maxX = canvas.width/2,
maxY = canvas.height,
canvasRatio = canvas.width / canvas.height,
c;
function setCanvasSize(height){
if(height === 'undefined') return maxY;
maxY = canvas.height = height;
canvas.width = height * canvasRatio;
maxX = canvas.width/2;
x}
var nodes = [{x:0, y:0, z:0, dir:'up', isEnd:true}], buds = [nodes[0]],
branches = [],
branch,
trunkDirBias = {x:0, z:0};
////////////////////////////////////////////////////////////////////////////////
configurables.forEach(function(el){
var widget;
switch(el.widget){
case 'number':
widget = document.createElement("input");
if(el.key){
widget.value = window[el.key] / (el.scale||1);
el.set = function(value){window[el.key]=value * (el.scale||1)};
}else{
widget.value = el.get();
}
widget.onblur = function(){
el.set(+widget.value);
};
break;
default: throw "unknown widget type";
}
var p = document.createElement("p");
p.textContent = el.name + ": ";
p.appendChild(widget);
p.title = el.description;
config.appendChild(p);
});
var button = document.createElement("input");
button.type = "button";
button.value = "grow";
button.onclick = function(){
button.value = "stop";
button.onclick = function(){clearInterval(interval)};
config.style.display="none";
canvas.style.display="";
c=canvas.getContext("2d");
c.translate(maxX, maxY);
c.scale(1, -1);
interval = setInterval(grow, GROWTH_MSPF);
}
document.body.appendChild(button);
function grow(){
for(var tick=0; tick<GROWTH_TPF; tick++){
var budId = 0 | Math.random() * buds.length,
nodeFrom = buds[budId], nodeTo, branch,
dir, slope, bias
if(nodeFrom.dir === 'up' && nodeFrom.isEnd){
nodeFrom.isEnd = false;
rndArg = Math.random()*TAU;
nodeTo = {
x:nodeFrom.x + TRUNK_SPACING * TRUNK_SLOPE * Math.sin(rndArg),
y:nodeFrom.y + TRUNK_SPACING,
z:nodeFrom.z + TRUNK_SPACING * TRUNK_SLOPE * Math.cos(rndArg),
dir:'up', isEnd:true}
if(nodeTo.y > maxY){
console.log("end");
clearInterval(interval);
rotateInit();
return;
}
nodes.push(nodeTo);
buds.push(nodeTo);
branch = {from: nodeFrom, to: nodeTo, view: TRUNK_VIEW};
branches.push(branch);
renderBranch(branch);
}else{ //bud is not a trunk top
if(!(nodeFrom.dir !== 'up' && Math.random() < BRANCH_P)){
buds.splice(buds.indexOf(nodeFrom), 1)
}
nodeFrom.isEnd = false;
if(nodeFrom.dir === 'up'){
bias = {x:trunkDirBias.x * TRUNK_BIAS_STR,
z:trunkDirBias.z * TRUNK_BIAS_STR};
slope = INIT_SLOPE;
}else{
bias = {x:nodeFrom.dir.x * DIR_KEEP_BIAS,
z:nodeFrom.dir.z * DIR_KEEP_BIAS};
slope = nodeFrom.slope;
}
var rndLen = Math.random(),
rndArg = Math.random()*TAU;
dir = {x: rndLen * Math.sin(rndArg) + bias.x,
z: rndLen * Math.cos(rndArg) + bias.z};
var uvFix = 1/Math.sqrt(dir.x*dir.x + dir.z*dir.z);
dir = {x:dir.x*uvFix, z:dir.z*uvFix};
if(nodeFrom.dir === "up") trunkDirBias = dir;
slope += MAX_D_SLOPE * (2*Math.random() - 1);
if(slope > MAX_SLOPE) slope = MAX_SLOPE;
if(slope < MIN_SLOPE) slope = MIN_SLOPE;
var length = BRANCH_LEN * Math.random();
nodeTo = {
x: nodeFrom.x + length * Math.cos(slope) * dir.x,
y: nodeFrom.y + length * Math.sin(slope),
z: nodeFrom.z + length * Math.cos(slope) * dir.z,
dir: dir, slope: slope, isEnd: true
}
//if(Math.abs(nodeTo.x)/maxX + nodeTo.y/maxY > 1) return;
nodes.push(nodeTo);
buds.push(nodeTo);
branch = {from: nodeFrom, to: nodeTo, view: BRANCH_VIEW};
branches.push(branch);
renderBranch(branch);
}// end if-is-trunk
}// end for-tick
}//end func-grow
function rotateInit(){
branches.sort(function(a,b){
return (a.view.zIndex-b.view.zIndex);
});
interval = setInterval(rotate, ROTATE_MSPF);
}
var time = 0;
var view = {x:1, z:0}
function rotate(){
time++;
view = {x: Math.cos(time * ROTATE_RPF),
z: Math.sin(time * ROTATE_RPF)};
c.fillStyle = "white"
c.fillRect(-maxX, 0, 2*maxX, maxY);
branches.forEach(renderBranch);
c.stroke();
c.beginPath();
}
var prevView = null;
function renderBranch(branch){
if(branch.view !== prevView){
c.stroke();
for(k in branch.view) c[k] = branch.view[k];
c.beginPath();
prevView = branch.view;
}
c.moveTo(view.x * branch.from.x + view.z * branch.from.z,
branch.from.y);
c.lineTo(view.x * branch.to.x + view.z * branch.to.z,
branch.to.y);
}