Skip to content

Instantly share code, notes, and snippets.

@JnBrymn-EB
Last active December 15, 2015 21:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JnBrymn-EB/5c3709ef137e6b009ac5 to your computer and use it in GitHub Desktop.
Save JnBrymn-EB/5c3709ef137e6b009ac5 to your computer and use it in GitHub Desktop.
This is a Pythagorean fractal -- Until you click and drag the control points. Then it's whatever fractal you want it to be! (Built with custom drag-n-drop library called DragonDroppings.)
function DragonDroppings(vis) {
this.registeredNodes = {};
this.selector = null;
this.xStart = -1;
this.yStart = -1;
this.xTrans = 0;
this.yTrans = 0;
this.vis = vis;
vis[0][0].addEventListener('mousemove', DragonDroppings._onmousemoveFactory(this))
}
/**
* Register a draggable item.
* @param selector - css style selector
* @param cx - initial x value
* @param cy - initial x value
* @param onGrab f(x, y) called when the object is grabbed
* @param onDrag f(x, y) called when the object is dragged
* @param onRelease f(x, y) called when the object is released
*/
DragonDroppings.prototype.register = function(selector, cx, cy, onGrab, onDrag, onRelease ) {
this.registeredNodes[selector] = {
'onGrab': onGrab,
'onDrag': onDrag,
'onRelease': onRelease
};
var thing = this.vis.select(selector)
thing.attr('transform','translate('+ cx +','+ cy +')');
thing[0][0].addEventListener('mousedown',DragonDroppings._onmousedownFactory(this, selector))
thing[0][0].addEventListener('mouseup',DragonDroppings._onmouseupFactory(this, selector))
};
DragonDroppings._onmousedownFactory = function(dd, selector) {
var handler = function(evt) {
dd.selector = selector;
dd.xStart = evt.offsetX;
dd.yStart = evt.offsetY;
var trans = dd.vis.select(selector).attr('transform').match(/translate\((\d+),(\d+)\)/);
dd.xTrans = parseFloat(trans[1]);
dd.yTrans = parseFloat(trans[2]);
var onGrab = dd.registeredNodes[selector]['onGrab'];
if(onGrab) {
onGrab(dd.xTrans, dd.yTrans);
}
};
return handler
};
DragonDroppings._onmousemoveFactory = function(dd) {
var handler = function(evt) {
if(dd.selector) {
var x = evt.offsetX - dd.xStart + dd.xTrans;
var y = evt.offsetY - dd.yStart + dd.yTrans;
dd.vis.select(dd.selector).attr('transform','translate('+ x +','+ y +')')
var onDrag = dd.registeredNodes[dd.selector]['onDrag'];
if(onDrag) {
onDrag(x,y);
}
}
};
return handler
};
DragonDroppings._onmouseupFactory = function(dd) {
var handler = function(evt) {
if(dd.selector) {
var trans = dd.vis.select(dd.selector).attr('transform').match(/translate\((\d+),(\d+)\)/);
var x = parseFloat(trans[1]);
var y = parseFloat(trans[2]);
var onRelease = dd.registeredNodes[dd.selector]['onRelease'];
if(onRelease) {
onRelease(x, y);
}
dd.selector = null;
}
};
return handler
};
////////////////////////////////////////////////////////////////////////////////////////
/**
* four points of a quad and the depth the line from point 0 to point 3 is considered the base, the
* line from 1 to 2 is considered the top. (currently this only works with squares. TODO fix)
*/
QuadShape = function(x0, y0, x1, y1, x2, y2, x3, y3, x4, y4, depth) {
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.x3 = x3;
this.y3 = y3;
// apex point
this.x4 = x4;
this.y4 = y4;
if(depth==undefined) {
this.depth = 0;
} else {
this.depth = depth;
}
this.left_scale = 0;
this.left_angle = 0;
this.right_scale = 0;
this.right_angle = 0;
this._calculate_angles_and_scaling()
};
/* used to check args in the change_shape function */
QuadShape._xy_set = {
x0: true,
y0: true,
x1: true,
y1: true,
x2: true,
y2: true,
x3: true,
y3: true,
x4: true,
y4: true,
};
/**
* Change the shape of the QuadShape - just specify the dimensions that must change.
* @param xy_obj
* @returns {string}
*/
QuadShape.prototype.change_shape = function(xy_obj) {
for (var k in xy_obj) {
if (xy_obj.hasOwnProperty(k)) {
if (!QuadShape._xy_set[k]) {
throw "xy_obj must have keys that are one of x0, y0, x1, y1, x2, y2, x3, y3."
}
var num = xy_obj[k];
if (isNaN(parseFloat(num))) {
throw "xy_obj must have numerical values."
}
this[k] = num;
}
}
this._calculate_angles_and_scaling()
};
QuadShape.prototype._calculate_angles_and_scaling = function() {
del_x03 = this.x3-this.x0;
del_y03 = this.y3-this.y0;
r_03 = Math.sqrt(del_x03*del_x03 + del_y03*del_y03);
del_x14 = this.x4-this.x1;
del_y14 = this.y4-this.y1;
r_14 = Math.sqrt(del_x14*del_x14 + del_y14*del_y14);
del_x24 = this.x2-this.x4;
del_y24 = this.y4-this.y2;
r_24 = Math.sqrt(del_x24*del_x24 + del_y24*del_y24);
this.left_scale = r_14/r_03;
this.right_scale = r_24/r_03;
this.left_angle = Math.atan2(del_y14,del_x14);
this.right_angle = - Math.atan2(del_y24,del_x24);
};
QuadShape.prototype.points_string = function() {
return this.x0 +','+ this.y0 +' '+ this.x1 +','+ this.y1 +' '+ this.x2 +','+ this.y2 +' '+ this.x3 +','+ this.y3;
};
QuadShape.prototype.make_fractal = function(depth) {
var quad_shapes = [this];
if(this.depth == depth) {
return quad_shapes
}
var sub_quads = this.make_sub_quad_shapes(this.left_angle, this.left_scale, this.right_angle, this.right_scale);
quad_shapes = quad_shapes.concat(sub_quads[0].make_fractal(depth));
quad_shapes = quad_shapes.concat(sub_quads[1].make_fractal(depth));
return quad_shapes
};
QuadShape.prototype.make_sub_quad_shapes = function(left_angle, left_scale, right_angle, right_scale) {
var del_x01 = this.x1-this.x0;
var del_y01 = this.y1-this.y0;
var del_x12 = this.x2-this.x1;
var del_y12 = this.y2-this.y1;
var del_x23 = this.x3-this.x2;
var del_y23 = this.y3-this.y2;
var left_quad = new QuadShape();
left_quad.depth = this.depth + 1;
left_quad.x0 = this.x1;
left_quad.y0 = this.y1;
left_quad.x1 = left_quad.x0+left_scale*(del_x01*Math.cos(left_angle)-del_y01*Math.sin(left_angle));
left_quad.y1 = left_quad.y0+left_scale*(del_x01*Math.sin(left_angle)+del_y01*Math.cos(left_angle));
left_quad.x2 = left_quad.x1+left_scale*(del_x12*Math.cos(left_angle)-del_y12*Math.sin(left_angle));
left_quad.y2 = left_quad.y1+left_scale*(del_x12*Math.sin(left_angle)+del_y12*Math.cos(left_angle));
left_quad.x3 = left_quad.x2+left_scale*(del_x23*Math.cos(left_angle)-del_y23*Math.sin(left_angle));
left_quad.y3 = left_quad.y2+left_scale*(del_x23*Math.sin(left_angle)+del_y23*Math.cos(left_angle));
left_quad.left_angle = left_angle;
left_quad.left_scale = left_scale;
left_quad.right_angle = right_angle;
left_quad.right_scale = right_scale;
var right_quad = new QuadShape();
right_quad.depth = this.depth + 1;
right_quad.x3 = this.x2;
right_quad.y3 = this.y2;
right_quad.x2 = right_quad.x3-right_scale*(del_x23*Math.cos(right_angle)-del_y23*Math.sin(right_angle));
right_quad.y2 = right_quad.y3-right_scale*(del_x23*Math.sin(right_angle)+del_y23*Math.cos(right_angle));
right_quad.x1 = right_quad.x2-right_scale*(del_x12*Math.cos(right_angle)-del_y12*Math.sin(right_angle));
right_quad.y1 = right_quad.y2-right_scale*(del_x12*Math.sin(right_angle)+del_y12*Math.cos(right_angle));
right_quad.x0 = right_quad.x1-right_scale*(del_x01*Math.cos(right_angle)-del_y01*Math.sin(right_angle));
right_quad.y0 = right_quad.y1-right_scale*(del_x01*Math.sin(right_angle)+del_y01*Math.cos(right_angle));
right_quad.left_angle = left_angle;
right_quad.left_scale = left_scale;
right_quad.right_angle = right_angle;
right_quad.right_scale = right_scale;
return [left_quad, right_quad]
};
var depth = 9;
function draw(vis, quad_shape) {
quad_sel = vis.selectAll('polygon.quad-shape')
.data(quad_shape.make_fractal(depth));
quad_sel.enter()
.append('polygon')
.attr('class', 'quad-shape');
quad_sel.attr('points', function(d) {return d.points_string()});
}
function main() {
var width = 1000;
var height = 600;
var vis = d3.select('div')
.append('svg:svg')
.attr('width', width)
.attr('height', height)
;
var quad_middle = 355;
var quad_width = 90;
var quad_base = 10;
var quad_shape = new QuadShape(
quad_middle - quad_width/2,quad_base,
quad_middle - quad_width/2,quad_base + quad_width,
quad_middle + quad_width/2,quad_base + quad_width,
quad_middle + quad_width/2,quad_base,
quad_middle, quad_base + quad_width*1.5
);
draw(vis,quad_shape)
vis.append('circle')
.attr('class','a')
.attr('fill','red')
.attr('cx',0)
.attr('cy',0)
.attr('r', 7)
vis.append('circle')
.attr('class','b')
.attr('fill','green')
.attr('cx',0)
.attr('cy',0)
.attr('r', 7)
vis.append('circle')
.attr('class','c')
.attr('fill','blue')
.attr('cx',0)
.attr('cy',0)
.attr('r', 7)
vis.append('circle')
.attr('class','e')
.attr('fill','magenta')
.attr('cx',0)
.attr('cy',0)
.attr('r', 7)
function move_a(x,y) {
quad_shape.change_shape({
x0: x,
x3: 2*quad_middle-x
});
draw(vis,quad_shape)
}
function move_b(x,y) {
quad_shape.change_shape({
x1: x,
y1: y
});
draw(vis,quad_shape)
}
function move_c(x,y) {
quad_shape.change_shape({
x2: x,
y2: y
});
draw(vis,quad_shape)
}
function move_e(x,y) {
quad_shape.change_shape({
x4: x,
y4: y
});
draw(vis,quad_shape)
}
var dd = new DragonDroppings(vis);
dd.register('circle.a', quad_middle - quad_width/2, quad_base, move_a, move_a, move_a);
dd.register('circle.b', quad_middle - quad_width/2, quad_base + quad_width, move_b, move_b, move_b);
dd.register('circle.c', quad_middle + quad_width/2, quad_base + quad_width, move_c, move_c, move_c);
dd.register('circle.e', quad_middle, quad_base + quad_width*1.5, move_e, move_e, move_e);
}
<!DOCTYPE html>
<html>
<head>
<title>Pythag</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" charset="utf-8"></script>
<script src="fractal.js" charset="utf-8"></script>
</head>
<body>
<!-- This file lives in public/404.html -->
<div class="dialog"></div>
<script type="text/javascript">
main();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment