|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
|
|
<meta charset="utf-8"> |
|
|
|
<title>Lindenmayer System Generator</title> |
|
|
|
|
|
|
|
<!-- D3.js --> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
|
|
<!-- Latest compiled and minified CSS --> |
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"> |
|
|
|
|
|
<style type="text/css"> |
|
form { |
|
display: table; |
|
} |
|
|
|
p { |
|
display: table-row; |
|
} |
|
|
|
label { |
|
display: table-cell; |
|
text-align: right; |
|
} |
|
|
|
input { |
|
display: table-cell; |
|
} |
|
|
|
submit { |
|
display: table-cell; |
|
} |
|
|
|
#formContainer { |
|
float: left; |
|
width: 250; |
|
} |
|
|
|
#svgContainer { |
|
margin-left: 250; |
|
} |
|
|
|
.line { |
|
fill: none; |
|
stroke: steelblue; |
|
stroke-width: 1px; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div id=formContainer> |
|
<form name="userForm" method="POST" onclick="return false;"> |
|
<p> |
|
<label></label> |
|
<b>Axiom</b> |
|
</p> |
|
<p> |
|
<label for="axiom"></label> |
|
<input id="axiom" type="text" value="A"> |
|
</p> |
|
<p> |
|
<label></label> |
|
<b>Drawing Rules</b> |
|
</p> |
|
<p> |
|
<label for="a">A=</label> |
|
<input id="a" type="text" value="+B-A-B+"> |
|
</p> |
|
<p> |
|
<label for="b">B=</label> |
|
<input id="b" type="text" value="-A+B+A-"> |
|
</p> |
|
<p> |
|
<label for="c">C=</label> |
|
<input id="c" type="text"> |
|
</p> |
|
<p> |
|
<label for="d">D=</label> |
|
<input id="d" type="text"> |
|
</p> |
|
<p> |
|
<label for="e">E=</label> |
|
<input id="e" type="text"> |
|
</p> |
|
<p> |
|
<label for="f">F=</label> |
|
<input id="f" type="text"> |
|
</p> |
|
<p> |
|
<label for="g">G=</label> |
|
<input id="g" type="text"> |
|
</p> |
|
<p> |
|
<label></label> |
|
<b>Constant Rules</b> |
|
</p> |
|
<p> |
|
<label for="x">X=</label> |
|
<input id="x" type="text"> |
|
</p> |
|
<p> |
|
<label for="y">Y=</label> |
|
<input id="y" type="text"> |
|
</p> |
|
<p> |
|
<label for="z">Z=</label> |
|
<input id="z" type="text"> |
|
</p> |
|
<p> |
|
<label></label> |
|
<b>Angle (in degrees)</b> |
|
</p> |
|
<p> |
|
<label for="theta">θ=</label> |
|
<input id="theta" type="text" value="60"> |
|
</p> |
|
<p> |
|
<label for="submit"></label> |
|
<input type="submit" id="submit"> |
|
</p> |
|
</form> |
|
</div> |
|
<div> |
|
Click to Iterate |
|
</div> |
|
<div id=svgContainer></div> |
|
|
|
<script type="text/javascript"> |
|
//Width and height of the visualization |
|
var w = 500; |
|
var h = 300; |
|
|
|
//padding creates a buffer of white space around the chart to make it a little easier to look at |
|
var padding = 15; |
|
|
|
|
|
//Create a canvas to display the chart on |
|
var svg = d3.select("#svgContainer").append("svg") |
|
.attr("width", '75%') |
|
.attr("height", '75%') |
|
.attr("viewBox", "0 0 " + w + " " + h); |
|
|
|
//Create group elements to layer the svg |
|
//This is an easy way to make sure things you want on top are on top, and things you want behind stay behind |
|
//Otherwise it all depends on when things are drawn |
|
var layer1 = svg.append('g'); |
|
var layer2 = svg.append('g'); |
|
var layer3 = svg.append('g'); |
|
|
|
//Initialize |
|
var path_pts; |
|
var rules; |
|
//Rules for Drawn segments |
|
var A_rule; |
|
var B_rule; |
|
var C_rule; |
|
var D_rule; |
|
var E_rule; |
|
var F_rule; |
|
var G_rule; |
|
//Rules for constants (not drawn) |
|
var X_rule; |
|
var Y_rule; |
|
var Z_rule; |
|
//Angle corresponding to + (- corresponds to negative angle) |
|
var angle; |
|
//hold the current position and the angle currently pointed in |
|
var x; |
|
var y; |
|
var theta; |
|
//For the scale, we will calculate the min/max x,y |
|
var min_x; |
|
var max_x; |
|
var min_y; |
|
var max_y; |
|
//We will display the iteration number |
|
var it_no; |
|
|
|
//Line generator |
|
var line = d3.line() |
|
.defined(function(d) { |
|
return !isNaN(d[1]); |
|
}) |
|
//.curve(d3.curveBasis) |
|
.x(function(d) { |
|
return xScale(d[0]); |
|
}) |
|
.y(function(d) { |
|
return yScale(d[1]); |
|
}); |
|
|
|
//This function will iterate the rules string to its next state |
|
function update_rules() { |
|
rules = rules.toLowerCase(); |
|
rules = rules.split('a').join(A_rule); |
|
rules = rules.split('b').join(B_rule); |
|
rules = rules.split('c').join(C_rule); |
|
rules = rules.split('d').join(D_rule); |
|
rules = rules.split('e').join(E_rule); |
|
rules = rules.split('f').join(F_rule); |
|
rules = rules.split('g').join(G_rule); |
|
rules = rules.split('x').join(X_rule); |
|
rules = rules.split('y').join(Y_rule); |
|
rules = rules.split('z').join(Z_rule); |
|
rules = rules.toUpperCase(); |
|
} |
|
|
|
//This function updates the x,y,theta positions |
|
//it also updates the display (aka x,y points along the path) |
|
function update_data() { |
|
path_pts = [ |
|
[0, 0] |
|
]; |
|
x = 0; |
|
y = 0; |
|
theta = 0; |
|
|
|
//Holds the x,y,theta when a '(' is reached |
|
var store_pos = []; |
|
//Holds the popped value of store_pos when ')' is reached |
|
var restore_pos = []; |
|
|
|
//for updating the dataset, we can remove all the constants, because they don't change the x,y,theta coordinates |
|
var draw_rules = rules.split('X').join('').split('Y').join(''); |
|
|
|
//convert the rules (excluding constants) into an array and iterate through them to update the position (x,y,theta) |
|
//if x,y change update the path_pts |
|
draw_rules.split('').map(function(str) { |
|
switch (str) { |
|
case 'A': |
|
case 'B': |
|
case 'C': |
|
case 'D': |
|
case 'E': |
|
case 'F': |
|
case 'G': |
|
//For the drawing commands, update (x,y) and path_pts |
|
x = x + Math.cos(theta); |
|
y = y + Math.sin(theta); |
|
path_pts.push([x, y]); |
|
|
|
//update max/min for scales |
|
min_x = d3.min([x, min_x]); |
|
max_x = d3.max([x, max_x]); |
|
min_y = d3.min([y, min_y]); |
|
max_y = d3.max([y, max_y]); |
|
break; |
|
case '+': |
|
theta = theta + angle; |
|
break; |
|
case '-': |
|
theta = theta - angle; |
|
break; |
|
case '[': |
|
case '(': |
|
//corresponds to saving current position on stack |
|
store_pos.push([x, y, theta]); |
|
break; |
|
case ']': |
|
case ')': |
|
//corresponds to getting last position from stack |
|
restore_pos = store_pos.pop(); |
|
x = restore_pos[0]; |
|
y = restore_pos[1]; |
|
theta = restore_pos[2]; |
|
path_pts.push([null]); |
|
path_pts.push([x, y]); |
|
break; |
|
default: |
|
break; |
|
} |
|
return; |
|
}); |
|
} |
|
|
|
function display_fn() { |
|
//Create the scales used to map the set |
|
xScale = d3.scaleLinear() |
|
.domain([min_x, max_x]) |
|
.range([padding, w - padding]); |
|
yScale = d3.scaleLinear() |
|
.domain([min_y, max_y]) |
|
.range([h - padding, padding]); |
|
|
|
var delay_len = Math.max(5000, path_pts.length * 10); |
|
|
|
//determine if the line should be filled |
|
if (rules[0] == "{") { |
|
var fill_type = "steelblue" |
|
} else { |
|
var fill_type = "none" |
|
}; |
|
|
|
//Draw the path |
|
var path = layer2.append("path") |
|
.data(path_pts) |
|
.attr("fill", fill_type) |
|
.attr("d", line(path_pts)) |
|
.attr("class", "line"); |
|
|
|
var totalLength = path.node().getTotalLength(); |
|
|
|
//transition the path as if it was being drawn |
|
path.attr("stroke-dasharray", totalLength + " " + totalLength) |
|
.attr("stroke-dashoffset", totalLength) |
|
.transition().ease(d3.easeLinear) |
|
.duration(delay_len) |
|
.attr("stroke-dashoffset", 0); |
|
|
|
var text = layer1.selectAll(".text").data([0]); |
|
|
|
//update |
|
text.text(it_no); |
|
|
|
//enter |
|
text.enter() |
|
.append("text") |
|
.attr("x", padding) |
|
.attr("y", 30) |
|
.attr("dy", ".5em") |
|
.attr("font-size", "28px") |
|
.text("0") |
|
.attr("class", "text"); |
|
} |
|
|
|
|
|
function step() { |
|
it_no++; |
|
layer2.selectAll(".line").remove(); |
|
update_rules(); |
|
update_data(); |
|
display_fn(); |
|
} |
|
|
|
function initialize() { |
|
//Initialize |
|
//Holds the x,y points along the path that will be displayed |
|
var path_pts = [ |
|
[0, 0] |
|
]; |
|
|
|
//initial state |
|
rules = d3.select("#axiom").node().value; |
|
//Rules for Drawn segments |
|
A_rule = d3.select("#a").node().value; |
|
B_rule = d3.select("#b").node().value; |
|
C_rule = d3.select("#c").node().value; |
|
D_rule = d3.select("#d").node().value; |
|
E_rule = d3.select("#e").node().value; |
|
F_rule = d3.select("#f").node().value; |
|
G_rule = d3.select("#g").node().value; |
|
//Rules for constants (not drawn) |
|
X_rule = d3.select("#x").node().value; |
|
Y_rule = d3.select("#y").node().value; |
|
Z_rule = d3.select("#z").node().value; |
|
//Angle corresponding to + (- corresponds to negative angle) |
|
angle = (d3.select("#theta").node().value / 180) * Math.PI; |
|
|
|
//Sometimes a minus gets pasted in as a dash, convert them all to a single charater |
|
//also convert to upper case |
|
function clean_rules(str) { |
|
return str.split('\u2212').join('-').toUpperCase(); |
|
} |
|
|
|
rules = clean_rules(rules); |
|
A_rule = clean_rules(A_rule); |
|
B_rule = clean_rules(B_rule); |
|
C_rule = clean_rules(C_rule); |
|
D_rule = clean_rules(D_rule); |
|
E_rule = clean_rules(E_rule); |
|
F_rule = clean_rules(F_rule); |
|
G_rule = clean_rules(G_rule); |
|
X_rule = clean_rules(X_rule); |
|
Y_rule = clean_rules(Y_rule); |
|
Z_rule = clean_rules(Z_rule); |
|
|
|
//hold the current position and the angle currently pointed in |
|
x = 0; |
|
y = 0; |
|
theta = 0; |
|
//For the scale, we will calculate the min/max x,y |
|
min_x = 0; |
|
max_x = 0; |
|
min_y = 0; |
|
max_y = 0; |
|
//We will display the iteration number |
|
it_no = 0; |
|
|
|
|
|
//don't need to update the rules for the 0th iteration |
|
layer2.selectAll(".line").remove(); |
|
update_data(); |
|
display_fn(); |
|
} |
|
|
|
initialize(); |
|
|
|
d3.select('#submit') |
|
.html('<input type="submit" id="submit">') |
|
.on('click', initialize); |
|
|
|
d3.select("#svgContainer") |
|
.on("click", step); |
|
|
|
d3.select("#svgContainer") |
|
.on("touchstart", step); |
|
</script> |
|
</body> |
|
|
|
</html> |